Source code for floris.optimization.layout_optimization.layout_optimization_pyoptsparse


import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial.distance import cdist
from shapely.geometry import Point

from .layout_optimization_base import LayoutOptimization, list_depth


[docs] class LayoutOptimizationPyOptSparse(LayoutOptimization): """ This class provides an interface for optimizing the layout of wind turbines using the pyOptSparse optimization library. The optimization objective is to maximize annual energy production (AEP) or annual value production (AVP). Args: fmodel (FlorisModel): A FlorisModel object. boundaries (iterable(float, float)): Pairs of x- and y-coordinates that represent the boundary's vertices (m). min_dist (float, optional): The minimum distance to be maintained between turbines during the optimization (m). If not specified, initializes to 2 rotor diameters. Defaults to None. solver (str, optional): Sets the solver used by pyOptSparse. Defaults to 'SLSQP'. optOptions (dict, optional): Dictionary for setting the optimization options. Defaults to None. timeLimit (float, optional): Variable passed to pyOptSparse optimizer. The maximum amount of time for optimizer to run (seconds). If None, no time limit is imposed. Defaults to None. storeHistory (str, optional): Variable passed to pyOptSparse optimizer. File name of the history file into which the history of the pyOptSparse optimization will be stored. Defaults to "hist.hist". hotStart (str, optional): Variable passed to pyOptSparse optimizer. File name of the history file to “replay” for the optimization. If None, pyOptSparse initializes the optimization from scratch. Defaults to None. enable_geometric_yaw (bool, optional): If True, enables geometric yaw optimization. Defaults to False. use_value (bool, optional): If True, the layout optimization objective is to maximize annual value production using the value array in the FLORIS model's WindData object. If False, the optimization objective is to maximize AEP. Defaults to False. """ def __init__( self, fmodel, boundaries, min_dist=None, solver=None, optOptions=None, timeLimit=None, storeHistory='hist.hist', hotStart=None, enable_geometric_yaw=False, use_value=False, ): if list_depth(boundaries) > 1 and hasattr(boundaries[0][0], "__len__"): raise NotImplementedError( "LayoutOptimizationPyOptSparse is not configured for multiple regions." ) super().__init__( fmodel, boundaries, min_dist=min_dist, enable_geometric_yaw=enable_geometric_yaw, use_value=use_value ) self.x0 = self._norm(self.fmodel.layout_x, self.xmin, self.xmax) self.y0 = self._norm(self.fmodel.layout_y, self.ymin, self.ymax) self.storeHistory = storeHistory self.timeLimit = timeLimit self.hotStart = hotStart self.enable_geometric_yaw = enable_geometric_yaw try: import pyoptsparse except ImportError: err_msg = ( "It appears you do not have pyOptSparse installed. " + "Please refer to https://pyoptsparse.readthedocs.io/ for " + "guidance on how to properly install the module." ) self.logger.error(err_msg, stack_info=True) raise ImportError(err_msg) # Instantiate pyOptSparse optimization object with name and objective function self.optProb = pyoptsparse.Optimization('layout', self._obj_func) self.optProb = self.add_var_group(self.optProb) self.optProb = self.add_con_group(self.optProb) self.optProb.addObj("obj") if solver is not None: self.solver = solver print("Setting up optimization with user's choice of solver: ", self.solver) else: self.solver = "SLSQP" print("Setting up optimization with default solver: SLSQP.") if optOptions is not None: self.optOptions = optOptions else: if self.solver == "SNOPT": self.optOptions = {"Major optimality tolerance": 1e-7} else: self.optOptions = {} exec("self.opt = pyoptsparse." + self.solver + "(options=self.optOptions)") def _optimize(self): if hasattr(self, "_sens"): self.sol = self.opt(self.optProb, sens=self._sens) else: if self.timeLimit is not None: self.sol = self.opt( self.optProb, sens="CDR", storeHistory=self.storeHistory, timeLimit=self.timeLimit, hotStart=self.hotStart ) else: self.sol = self.opt( self.optProb, sens="CDR", storeHistory=self.storeHistory, hotStart=self.hotStart ) return self.sol def _obj_func(self, varDict): # Parse the variable dictionary self.parse_opt_vars(varDict) # Compute turbine yaw angles using PJ's geometric code (if enabled) yaw_angles = self._get_geoyaw_angles() # Update turbine map with turbine locations and yaw angles self.fmodel.set(layout_x=self.x, layout_y=self.y, yaw_angles=yaw_angles) self.fmodel.run() # Compute the objective function funcs = {} if self.use_value: funcs["obj"] = -1 * self.fmodel.get_farm_AVP() / self.initial_AEP_or_AVP else: funcs["obj"] = -1 * self.fmodel.get_farm_AEP() / self.initial_AEP_or_AVP # Compute constraints, if any are defined for the optimization funcs = self.compute_cons(funcs, self.x, self.y) fail = False return funcs, fail # Optionally, the user can supply the optimization with gradients # def _sens(self, varDict, funcs): # funcsSens = {} # fail = False # return funcsSens, fail
[docs] def parse_opt_vars(self, varDict): self.x = self._unnorm(varDict["x"], self.xmin, self.xmax) self.y = self._unnorm(varDict["y"], self.ymin, self.ymax)
[docs] def parse_sol_vars(self, sol): self.x = list(self._unnorm(sol.getDVs()["x"], self.xmin, self.xmax))[0] self.y = list(self._unnorm(sol.getDVs()["y"], self.ymin, self.ymax))[1]
[docs] def add_var_group(self, optProb): optProb.addVarGroup( "x", self.nturbs, varType="c", lower=0.0, upper=1.0, value=self.x0 ) optProb.addVarGroup( "y", self.nturbs, varType="c", lower=0.0, upper=1.0, value=self.y0 ) return optProb
[docs] def add_con_group(self, optProb): optProb.addConGroup("boundary_con", self.nturbs, upper=0.0) optProb.addConGroup("spacing_con", 1, upper=0.0) return optProb
[docs] def compute_cons(self, funcs, x, y): funcs["boundary_con"] = self.distance_from_boundaries(x, y) funcs["spacing_con"] = self.space_constraint(x, y) return funcs
[docs] def space_constraint(self, x, y, rho=500): # Calculate distances between turbines locs = np.vstack((x, y)).T distances = cdist(locs, locs) arange = np.arange(distances.shape[0]) distances[arange, arange] = 1e10 dist = np.min(distances, axis=0) g = 1 - np.array(dist) / self.min_dist # Following code copied from OpenMDAO KSComp(). # Constraint is satisfied when KS_constraint <= 0 g_max = np.max(np.atleast_2d(g), axis=-1)[:, np.newaxis] g_diff = g - g_max exponents = np.exp(rho * g_diff) summation = np.sum(exponents, axis=-1)[:, np.newaxis] KS_constraint = g_max + 1.0 / rho * np.log(summation) return KS_constraint[0][0]
[docs] def distance_from_boundaries(self, x, y): boundary_con = np.zeros(self.nturbs) for i in range(self.nturbs): loc = Point(x[i], y[i]) boundary_con[i] = loc.distance(self._boundary_line) if self._boundary_polygon.contains(loc) is True: boundary_con[i] *= -1.0 return boundary_con
def _get_initial_and_final_locs(self): x_initial = self._unnorm(self.x0, self.xmin, self.xmax) y_initial = self._unnorm(self.y0, self.ymin, self.ymax) x_opt, y_opt = self.get_optimized_locs() return x_initial, y_initial, x_opt, y_opt
[docs] def get_optimized_locs(self): x_opt = self._unnorm(self.sol.getDVs()["x"], self.xmin, self.xmax) y_opt = self._unnorm(self.sol.getDVs()["y"], self.ymin, self.ymax) return x_opt, y_opt