From f0c3773a196cfe9186bb6ad4eeee1a9a5931138f Mon Sep 17 00:00:00 2001 From: Bart Doekemeijer Date: Thu, 23 Sep 2021 10:27:09 -0600 Subject: [PATCH] Add simple turbine clustering functionality to SciPy yaw optimization (#261) * Add function to cluster turbines and add a yaw optimization class that is a wrapper around the regular YawOptimization class in yaw.py which clusters turbines and then optimizes their yaw angles sequentially. In simple test cases with 10-20 turbines, this cuts down the computation time by about 50% while sometimes also increasing accuracy due to better convergence. * Update docstring in yaw_clustered.py * Add optimization classes for WindRose and WindRoseParallel that include the clustering algorithm * Update class names and dependent in clustered and parallelized yaw optimization in scipy --- .../optimization/scipy/cluster_turbines.py | 187 +++++ .../tools/optimization/scipy/yaw_clustered.py | 288 ++++++++ .../scipy/yaw_wind_rose_clustered.py | 451 ++++++++++++ .../scipy/yaw_wind_rose_parallel_clustered.py | 657 ++++++++++++++++++ 4 files changed, 1583 insertions(+) create mode 100644 floris/tools/optimization/scipy/cluster_turbines.py create mode 100644 floris/tools/optimization/scipy/yaw_clustered.py create mode 100644 floris/tools/optimization/scipy/yaw_wind_rose_clustered.py create mode 100644 floris/tools/optimization/scipy/yaw_wind_rose_parallel_clustered.py diff --git a/floris/tools/optimization/scipy/cluster_turbines.py b/floris/tools/optimization/scipy/cluster_turbines.py new file mode 100644 index 000000000..e596505f3 --- /dev/null +++ b/floris/tools/optimization/scipy/cluster_turbines.py @@ -0,0 +1,187 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + + +import numpy as np +import matplotlib.pyplot as plt + + +def cluster_turbines(fi, wind_direction=None, wake_slope=0.30, plot_lines=False): + """Separate a wind farm into separate clusters in which the turbines in + each subcluster only affects the turbines in its cluster and has zero + interaction with turbines from other clusters, both ways (being waked, + generating wake), This allows the user to separate the control setpoint + optimization in several lower-dimensional optimization problems, for + example. This function assumes a very simplified wake function where the + wakes are assumed to have a linearly diverging profile. In comparisons + with the FLORIS GCH model, the wake_slope matches well with the FLORIS' + wake profiles for a value of wake_slope = 0.5 * turbulence_intensity, where + turbulence_intensity is an input to the FLORIS model at the default + GCH parameterization. Note that does not include wind direction variability. + To be conservative, the user is recommended to use the rule of thumb: + `wake_slope = turbulence_intensity`. Hence, the default value for + `wake_slope=0.30` should be conservative for turbulence intensities up to + 0.30 and is likely to provide valid estimates of which turbines are + downstream until a turbulence intensity of 0.50. This simple model saves + time compared to FLORIS. + + Args: + fi ([floris object]): FLORIS object of the farm of interest. + wind_direction (float): The wind direction in the FLORIS frame + of reference for which the downstream turbines are to be determined. + wake_slope (float, optional): linear slope of the wake (dy/dx) + plot_lines (bool, optional): Enable plotting wakes/turbines. + Defaults to False. + + Returns: + clusters (iterable): A list in which each entry contains a list + of turbine numbers that together form a cluster which + exclusively interact with one another and have zero + interaction with turbines outside of this cluster. + """ + + if wind_direction is None: + wind_direction = np.mean(fi.floris.farm.wind_direction) + + # Get farm layout + x = fi.layout_x + y = fi.layout_y + D = np.array([t.rotor_diameter for t in fi.floris.farm.turbines]) + n_turbs = len(x) + + # Rotate farm and determine freestream/waked turbines + is_downstream = [False for _ in range(n_turbs)] + x_rot = ( + np.cos((wind_direction - 270.0) * np.pi / 180.0) * x + - np.sin((wind_direction - 270.0) * np.pi / 180.0) * y + ) + y_rot = ( + np.sin((wind_direction - 270.0) * np.pi / 180.0) * x + + np.cos((wind_direction - 270.0) * np.pi / 180.0) * y + ) + + if plot_lines: + fig, ax = plt.subplots() + for ii in range(n_turbs): + ax.plot( + x_rot[ii] * np.ones(2), + [y_rot[ii] - D[ii] / 2, y_rot[ii] + D[ii] / 2], + "k", + ) + for ii in range(n_turbs): + ax.text(x_rot[ii], y_rot[ii], "T%03d" % ii) + ax.axis("equal") + + srt = np.argsort(x_rot) + usrt = np.argsort(srt) + x_rot_srt = x_rot[srt] + y_rot_srt = y_rot[srt] + affected_by_turbs = np.tile(False, (n_turbs, n_turbs)) + for ii in range(n_turbs): + x0 = x_rot_srt[ii] + y0 = y_rot_srt[ii] + + def wake_profile_ub_turbii(x): + y = (y0 + D[ii]) + (x - x0) * wake_slope + if isinstance(y, (float, np.float64, np.float32)): + if x < (x0 + 0.01): + y = -np.Inf + else: + y[x < x0 + 0.01] = -np.Inf + return y + + def wake_profile_lb_turbii(x): + y = (y0 - D[ii]) - (x - x0) * wake_slope + if isinstance(y, (float, np.float64, np.float32)): + if x < (x0 + 0.01): + y = -np.Inf + else: + y[x < x0 + 0.01] = -np.Inf + return y + + def determine_if_in_wake(xt, yt): + return (yt < wake_profile_ub_turbii(xt)) & (yt > wake_profile_lb_turbii(xt)) + + # Get most downstream turbine + is_downstream[ii] = not any( + [ + determine_if_in_wake(x_rot_srt[iii], y_rot_srt[iii]) + for iii in range(n_turbs) + ] + ) + # Determine which turbines are affected by this turbine ('ii') + affecting_following_turbs = [ + determine_if_in_wake(x_rot_srt[iii], y_rot_srt[iii]) + for iii in range(n_turbs) + ] + + # Determine by which turbines this turbine ('ii') is affected + for aft in np.where(affecting_following_turbs)[0]: + affected_by_turbs[aft, ii] = True + + if plot_lines: + x1 = np.max(x_rot_srt) + 500.0 + ax.fill_between( + [x0, x1, x1, x0], + [ + wake_profile_ub_turbii(x0 + 0.02), + wake_profile_ub_turbii(x1), + wake_profile_lb_turbii(x1), + wake_profile_lb_turbii(x0 + 0.02), + ], + alpha=0.1, + color="k", + edgecolor=None, + ) + + # Rearrange into initial frame of reference + affected_by_turbs = affected_by_turbs[:, usrt][usrt, :] + for ii in range(n_turbs): + affected_by_turbs[ii, ii] = True # Add self to turb_list_affected + affected_by_turbs = [np.where(c)[0] for c in affected_by_turbs] + + # List of downstream turbines + turbs_downstream = [is_downstream[i] for i in usrt] + turbs_downstream = list(np.where(turbs_downstream)[0]) + + # Initialize one cluster for each turbine and all the turbines its affected by + clusters = affected_by_turbs + + # Iteratively merge clusters if any overlap between turbines + ci = 0 + while ci < len(clusters): + # Compare current row to the ones to the right of it + cj = ci + 1 + merged_column = False + while cj < len(clusters): + if any([y in clusters[ci] for y in clusters[cj]]): + # Merge + clusters[ci] = np.hstack([clusters[ci], clusters[cj]]) + clusters[ci] = np.array(np.unique(clusters[ci]), dtype=int) + clusters.pop(cj) + merged_column = True + else: + cj = cj + 1 + if not merged_column: + ci = ci + 1 + + if plot_lines: + ax.set_title("wind_direction = %.1f deg" % wind_direction) + ax.set_xlim([np.min(x_rot) - 500.0, x1]) + ax.set_ylim([np.min(y_rot) - 500.0, np.max(y_rot) + 500.0]) + for ci, cl in enumerate(clusters): + ax.plot(x_rot[cl], y_rot[cl], 'o', label='cluster %d' % ci) + ax.legend() + + return clusters \ No newline at end of file diff --git a/floris/tools/optimization/scipy/yaw_clustered.py b/floris/tools/optimization/scipy/yaw_clustered.py new file mode 100644 index 000000000..b5c801e2f --- /dev/null +++ b/floris/tools/optimization/scipy/yaw_clustered.py @@ -0,0 +1,288 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +import copy + +import numpy as np +import pandas as pd + +from .yaw import YawOptimization +from .cluster_turbines import cluster_turbines +from ....logging_manager import LoggerBase + + +class YawOptimizationClustered(YawOptimization, LoggerBase): + """ + YawOptimization is a subclass of + :py:class:`~.tools.optimizationscipy.YawOptimization` that is used to + perform optimizations of the yaw angles of all or a subset of wind turbines + in a Floris Farm for a single set of inflow conditions using the scipy + optimization package. This class facilitates the clusterization of the + turbines inside seperate subsets in which the turbines witin each subset + exclusively interact with one another and have no impact on turbines + in other clusters. This may significantly reduce the computational + burden at no loss in performance (assuming the turbine clusters are truly + independent). + """ + + def __init__( + self, + fi, + minimum_yaw_angle=0.0, + maximum_yaw_angle=25.0, + yaw_angles_baseline=None, + x0=None, + bnds=None, + opt_method="SLSQP", + opt_options=None, + include_unc=False, + unc_pmfs=None, + unc_options=None, + turbine_weights=None, + calc_init_power=True, + exclude_downstream_turbines=False, + clustering_wake_slope=0.30, + ): + """ + Instantiate YawOptimization object with a FlorisInterface object + and assign parameter values. + + Args: + fi (:py:class:`~.tools.floris_interface.FlorisInterface`): + Interface used to interact with the Floris object. + minimum_yaw_angle (float, optional): Minimum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 0.0. + maximum_yaw_angle (float, optional): Maximum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 25.0. + yaw_angles_baseline (iterable, optional): The baseline yaw + angles used to calculate the initial and baseline power + production in the wind farm and used to normalize the cost + function. If none are specified, this variable is set equal + to the current yaw angles in floris. Note that this variable + need not meet the yaw constraints specified in self.bnds, + yet a warning is raised if it does to inform the user. + Defaults to None. + x0 (iterable, optional): The initial guess for the optimization + problem. These values must meet the constraints specified + in self.bnds. Note that, if exclude_downstream_turbines=True, + the initial guess for any downstream turbines are ignored + since they are not part of the optimization. Instead, the yaw + angles for those turbines are 0.0 if that meets the lower and + upper bound, or otherwise as close to 0.0 as feasible. If no + values for x0 are specified, x0 is set to be equal to zeros + wherever feasible (w.r.t. the bounds), and equal to the + average of its lower and upper bound for all non-downstream + turbines otherwise. Defaults to None. + bnds (iterable, optional): Bounds for the yaw angles, as tuples of + min, max values for each turbine (deg). One can fix the yaw + angle of certain turbines to a predefined value by setting that + turbine's lower bound equal to its upper bound (i.e., an + equality constraint), as: bnds[ti] = (x, x), where x is the + fixed yaw angle assigned to the turbine. This works for both + zero and nonzero yaw angles. Moreover, if + exclude_downstream_turbines=True, the yaw angles for all + downstream turbines will be 0.0 or a feasible value closest to + 0.0. If none are specified, the bounds are set to + (minimum_yaw_angle, maximum_yaw_angle) for each turbine. Note + that, if bnds is not none, its values overwrite any value given + in minimum_yaw_angle and maximum_yaw_angle. Defaults to None. + opt_method (str, optional): The optimization method used by + scipy.optimize.minize. Defaults to 'SLSQP'. + opt_options (dictionary, optional): Optimization options used by + scipy.optimize.minize. If none are specified, they are set to + {'maxiter': 100, 'disp': False, 'iprint': 1, 'ftol': 1e-7, + 'eps': 0.01}. Defaults to None. + include_unc (bool, optional): Determines whether wind direction or + yaw uncertainty are included. If True, uncertainty in wind + direction and/or yaw position is included when determining + wind farm power. Uncertainty is included by computing the + mean wind farm power for a distribution of wind direction + and yaw position deviations from the intended wind direction + and yaw angles. Defaults to False. + unc_pmfs (dictionary, optional): A dictionary containing + probability mass functions describing the distribution of + wind direction and yaw position deviations when wind direction + and/or yaw position uncertainty is included in the power + calculations. Contains the following key-value pairs: + + - **wd_unc** (*np.array*): The wind direction + deviations from the intended wind direction (deg). + - **wd_unc_pmf** (*np.array*): The probability + of each wind direction deviation in **wd_unc** occuring. + - **yaw_unc** (*np.array*): The yaw angle deviations + from the intended yaw angles (deg). + - **yaw_unc_pmf** (*np.array*): The probability + of each yaw angle deviation in **yaw_unc** occuring. + + If none are specified, default PMFs are calculated using + values provided in **unc_options**. Defaults to None. + unc_options (dictionary, optional): A dictionary containing values + used to create normally-distributed, zero-mean probability mass + functions describing the distribution of wind direction and yaw + position deviations when wind direction and/or yaw position + uncertainty is included. This argument is only used when + **unc_pmfs** is None and contains the following key-value pairs: + + - **std_wd** (*float*): The standard deviation of + the wind direction deviations from the original wind + direction (deg). + - **std_yaw** (*float*): The standard deviation of + the yaw angle deviations from the original yaw angles (deg). + - **pmf_res** (*float*): The resolution in degrees + of the wind direction and yaw angle PMFs. + - **pdf_cutoff** (*float*): The cumulative + distribution function value at which the tails of the + PMFs are truncated. + + If none are specified, default values of + {'std_wd': 4.95, 'std_yaw': 1.75, 'pmf_res': 1.0, + 'pdf_cutoff': 0.995} are used. Defaults to None. + turbine_weights (iterable, optional): weighing terms that allow + the user to emphasize power gains at particular turbines or + completely ignore power gains from other turbines. The array + of turbine powers from floris is multiplied with this array + in the calculation of the objective function. If None, this + is an array with all values 1.0 and length equal to the + number of turbines. Defaults to None. + calc_init_power (bool, optional): If True, calculates initial + wind farm power for each set of wind conditions. Defaults to + True. + exclude_downstream_turbines (bool, optional): If True, + automatically finds and excludes turbines that are most + downstream from the optimization problem. This significantly + reduces computation time at no loss in performance. The yaw + angles of these downstream turbines are fixed to 0.0 deg if + the yaw bounds specified in self.bnds allow that, or otherwise + are fixed to the lower or upper yaw bound, whichever is closer + to 0.0. Defaults to False. + clustering_wake_slope (float, optional): linear slope of the wake + in the simplified linear expansion wake model (dy/dx). This + model is used to derive wake interactions between turbines and + to identify the turbine clusters. A good value is about equal + to the turbulence intensity in FLORIS. Though, since yaw + optimizations may shift the wake laterally, a safer option + is twice the turbulence intensity. The default value is 0.30 + which should be valid for yaw optimizations at wd_std = 0.0 deg + and turbulence intensities up to 15%. Defaults to 0.30. + """ + super().__init__( + fi=fi, + minimum_yaw_angle=minimum_yaw_angle, + maximum_yaw_angle=maximum_yaw_angle, + yaw_angles_baseline=yaw_angles_baseline, + x0=x0, + bnds=bnds, + opt_method=opt_method, + opt_options=opt_options, + include_unc=include_unc, + unc_pmfs=unc_pmfs, + unc_options=unc_options, + turbine_weights=turbine_weights, + calc_init_power=calc_init_power, + exclude_downstream_turbines=exclude_downstream_turbines, + ) + self.clustering_wake_slope = clustering_wake_slope + + + def _cluster_turbines(self): + wind_directions = self.fi.floris.farm.wind_direction + if (np.std(wind_directions) > 0.001): + raise ValueError("Wind directions must be uniform for clustering algorithm.") + self.clusters = cluster_turbines( + fi=self.fi, + wind_direction=self.fi.floris.farm.wind_direction[0], + wake_slope=self.clustering_wake_slope + ) + + def plot_clusters(self): + cluster_turbines( + fi=self.fi, + wind_direction=self.fi.floris.farm.wind_direction[0], + wake_slope=self.clustering_wake_slope, + plot_lines=True + ) + + def optimize(self, verbose=True): + """ + This method solves for the optimum turbine yaw angles for power + production given a fixed set of atmospheric conditions + (wind speed, direction, etc.). + + Returns: + np.array: Optimal yaw angles for each turbine (deg). + """ + if verbose: + print("=====================================================") + print("Optimizing wake redirection control...") + print("Number of parameters to optimize = ", len(self.turbs_to_opt)) + print("=====================================================") + + # Cluster turbines first + self._cluster_turbines() + if verbose: + print("Clustered turbines into %d separate clusters." % len(self.clusters)) + + # Save parameters to a full list + yaw_angles_template_full = copy.copy(self.yaw_angles_template) + yaw_angles_baseline_full = copy.copy(self.yaw_angles_baseline) + turbine_weights_full = copy.copy(self.turbine_weights) + bnds_full = copy.copy(self.bnds) + # nturbs_full = copy.copy(self.nturbs) + x0_full = copy.copy(self.x0) + fi_full = copy.deepcopy(self.fi) + + # Overwrite parameters for each cluster and optimize + opt_yaw_angles = np.zeros_like(x0_full) + for ci, cl in enumerate(self.clusters): + if verbose: + print("=====================================================") + print("Optimizing %d parameters in cluster %d." % (len(cl), ci)) + print("=====================================================") + self.yaw_angles_template = np.array(yaw_angles_template_full)[cl] + self.yaw_angles_baseline = np.array(yaw_angles_baseline_full)[cl] + self.turbine_weights = np.array(turbine_weights_full)[cl] + self.bnds = np.array(bnds_full)[cl] + self.x0 = np.array(x0_full)[cl] + self.fi = copy.deepcopy(fi_full) + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x)[cl], + np.array(fi_full.layout_y)[cl] + ] + ) + opt_yaw_angles[cl] = self._optimize() + + # Restore parameters + self.yaw_angles_template = yaw_angles_template_full + self.yaw_angles_baseline = yaw_angles_baseline_full + self.turbine_weights = turbine_weights_full + self.bnds = bnds_full + self.x0 = x0_full + self.fi = fi_full + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x), + np.array(fi_full.layout_y) + ] + ) + + if verbose and np.sum(np.abs(opt_yaw_angles)) == 0: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + + return opt_yaw_angles \ No newline at end of file diff --git a/floris/tools/optimization/scipy/yaw_wind_rose_clustered.py b/floris/tools/optimization/scipy/yaw_wind_rose_clustered.py new file mode 100644 index 000000000..952a64dc6 --- /dev/null +++ b/floris/tools/optimization/scipy/yaw_wind_rose_clustered.py @@ -0,0 +1,451 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +import copy + +import numpy as np +import pandas as pd + +from .yaw_wind_rose import YawOptimizationWindRose +from .cluster_turbines import cluster_turbines +from ....logging_manager import LoggerBase + + +class YawOptimizationWindRoseClustered(YawOptimizationWindRose, LoggerBase): + """ + YawOptimizationWindRose is a subclass of + :py:class:`~.tools.optimizationscipy.YawOptimizationWindRose` that is used + to perform optimizations of the yaw angles of all or a subset of wind + turbines in a Floris Farm for multiple sets of inflow conditions using the + scipy optimization package. This class facilitates the clusterization of the + turbines inside seperate subsets in which the turbines witin each subset + exclusively interact with one another and have no impact on turbines + in other clusters. This may significantly reduce the computational + burden at no loss in performance (assuming the turbine clusters are truly + independent). + """ + + def __init__( + self, + fi, + wd, + ws, + ti=None, + minimum_yaw_angle=0.0, + maximum_yaw_angle=25.0, + minimum_ws=3.0, + maximum_ws=25.0, + yaw_angles_baseline=None, + x0=None, + bnds=None, + opt_method="SLSQP", + opt_options=None, + include_unc=False, + unc_pmfs=None, + unc_options=None, + turbine_weights=None, + verbose=False, + calc_init_power=True, + exclude_downstream_turbines=False, + clustering_wake_slope=0.30, + ): + """ + Instantiate YawOptimizationWindRose object with a FlorisInterface object + and assign parameter values. + + Args: + fi (:py:class:`~.tools.floris_interface.FlorisInterface`): + Interface used to interact with the Floris object. + wd (iterable) : The wind directions for which the yaw angles are + optimized (deg). + ws (iterable): The wind speeds for which the yaw angles are + optimized (m/s). + ti (iterable, optional): An optional list of turbulence intensity + values for which the yaw angles are optimized. If not + specified, the current TI value in the Floris object will be + used for all optimizations. Defaults to None. + minimum_yaw_angle (float, optional): Minimum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 0.0. + maximum_yaw_angle (float, optional): Maximum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 25.0. + minimum_ws (float, optional): Minimum wind speed at which + optimization is performed (m/s). Assumes zero power generated + below this value. Defaults to 3. + maximum_ws (float, optional): Maximum wind speed at which + optimization is performed (m/s). Assumes optimal yaw offsets + are zero above this wind speed. Defaults to 25. + yaw_angles_baseline (iterable, optional): The baseline yaw + angles used to calculate the initial and baseline power + production in the wind farm and used to normalize the cost + function. If none are specified, this variable is set equal + to the current yaw angles in floris. Note that this variable + need not meet the yaw constraints specified in self.bnds, + yet a warning is raised if it does to inform the user. + Defaults to None. + x0 (iterable, optional): The initial guess for the optimization + problem. These values must meet the constraints specified + in self.bnds. Note that, if exclude_downstream_turbines=True, + the initial guess for any downstream turbines are ignored + since they are not part of the optimization. Instead, the yaw + angles for those turbines are 0.0 if that meets the lower and + upper bound, or otherwise as close to 0.0 as feasible. If no + values for x0 are specified, x0 is set to be equal to zeros + wherever feasible (w.r.t. the bounds), and equal to the + average of its lower and upper bound for all non-downstream + turbines otherwise. Defaults to None. + bnds (iterable, optional): Bounds for the yaw angles, as tuples of + min, max values for each turbine (deg). One can fix the yaw + angle of certain turbines to a predefined value by setting that + turbine's lower bound equal to its upper bound (i.e., an + equality constraint), as: bnds[ti] = (x, x), where x is the + fixed yaw angle assigned to the turbine. This works for both + zero and nonzero yaw angles. Moreover, if + exclude_downstream_turbines=True, the yaw angles for all + downstream turbines will be 0.0 or a feasible value closest to + 0.0. If none are specified, the bounds are set to + (minimum_yaw_angle, maximum_yaw_angle) for each turbine. Note + that, if bnds is not none, its values overwrite any value given + in minimum_yaw_angle and maximum_yaw_angle. Defaults to None. + opt_method (str, optional): The optimization method used by + scipy.optimize.minize. Defaults to 'SLSQP'. + opt_options (dictionary, optional): Optimization options used by + scipy.optimize.minize. If none are specified, they are set to + {'maxiter': 100, 'disp': False, 'iprint': 1, 'ftol': 1e-7, + 'eps': 0.01}. Defaults to None. + include_unc (bool, optional): Determines whether wind direction or + yaw uncertainty are included. If True, uncertainty in wind + direction and/or yaw position is included when determining + wind farm power. Uncertainty is included by computing the + mean wind farm power for a distribution of wind direction + and yaw position deviations from the intended wind direction + and yaw angles. Defaults to False. + unc_pmfs (dictionary, optional): A dictionary containing + probability mass functions describing the distribution of + wind direction and yaw position deviations when wind direction + and/or yaw position uncertainty is included in the power + calculations. Contains the following key-value pairs: + + - **wd_unc** (*np.array*): The wind direction + deviations from the intended wind direction (deg). + - **wd_unc_pmf** (*np.array*): The probability + of each wind direction deviation in **wd_unc** occuring. + - **yaw_unc** (*np.array*): The yaw angle deviations + from the intended yaw angles (deg). + - **yaw_unc_pmf** (*np.array*): The probability + of each yaw angle deviation in **yaw_unc** occuring. + + If none are specified, default PMFs are calculated using + values provided in **unc_options**. Defaults to None. + unc_options (dictionary, optional): A dictionary containing values + used to create normally-distributed, zero-mean probability mass + functions describing the distribution of wind direction and yaw + position deviations when wind direction and/or yaw position + uncertainty is included. This argument is only used when + **unc_pmfs** is None and contains the following key-value pairs: + + - **std_wd** (*float*): The standard deviation of + the wind direction deviations from the original wind + direction (deg). + - **std_yaw** (*float*): The standard deviation of + the yaw angle deviations from the original yaw angles (deg). + - **pmf_res** (*float*): The resolution in degrees + of the wind direction and yaw angle PMFs. + - **pdf_cutoff** (*float*): The cumulative + distribution function value at which the tails of the + PMFs are truncated. + + If none are specified, default values of + {'std_wd': 4.95, 'std_yaw': 1.75, 'pmf_res': 1.0, + 'pdf_cutoff': 0.995} are used. Defaults to None. + turbine_weights (iterable, optional): weighing terms that allow + the user to emphasize power gains at particular turbines or + completely ignore power gains from other turbines. The array + of turbine powers from floris is multiplied with this array + in the calculation of the objective function. If None, this + is an array with all values 1.0 and length equal to the + number of turbines. Defaults to None. + calc_init_power (bool, optional): If True, calculates initial + wind farm power for each set of wind conditions. Defaults to + True. + exclude_downstream_turbines (bool, optional): If True, + automatically finds and excludes turbines that are most + downstream from the optimization problem. This significantly + reduces computation time at no loss in performance. The yaw + angles of these downstream turbines are fixed to 0.0 deg if + the yaw bounds specified in self.bnds allow that, or otherwise + are fixed to the lower or upper yaw bound, whichever is closer + to 0.0. Defaults to False. + clustering_wake_slope (float, optional): linear slope of the wake + in the simplified linear expansion wake model (dy/dx). This + model is used to derive wake interactions between turbines and + to identify the turbine clusters. A good value is about equal + to the turbulence intensity in FLORIS. Though, since yaw + optimizations may shift the wake laterally, a safer option + is twice the turbulence intensity. The default value is 0.30 + which should be valid for yaw optimizations at wd_std = 0.0 deg + and turbulence intensities up to 15%. Defaults to 0.30. + """ + super().__init__( + fi=fi, + wd=wd, + ws=ws, + ti=ti, + minimum_yaw_angle=minimum_yaw_angle, + maximum_yaw_angle=maximum_yaw_angle, + minimum_ws=minimum_ws, + maximum_ws=maximum_ws, + yaw_angles_baseline=yaw_angles_baseline, + x0=x0, + bnds=bnds, + opt_method=opt_method, + opt_options=opt_options, + include_unc=include_unc, + unc_pmfs=unc_pmfs, + unc_options=unc_options, + turbine_weights=turbine_weights, + verbose=verbose, + calc_init_power=calc_init_power, + exclude_downstream_turbines=exclude_downstream_turbines, + ) + self.clustering_wake_slope = clustering_wake_slope + + + def _cluster_turbines(self): + wind_directions = self.fi.floris.farm.wind_direction + if (np.std(wind_directions) > 0.001): + raise ValueError("Wind directions must be uniform for clustering algorithm.") + self.clusters = cluster_turbines( + fi=self.fi, + wind_direction=self.fi.floris.farm.wind_direction[0], + wake_slope=self.clustering_wake_slope + ) + + def plot_clusters(self): + for wd in self.wd: + cluster_turbines( + fi=self.fi, + wind_direction=wd, + wake_slope=self.clustering_wake_slope, + plot_lines=True + ) + + + def optimize(self): + """ + This method solves for the optimum turbine yaw angles for power + production and the resulting power produced by the wind farm for a + series of wind speed, wind direction, and optionally TI combinations. + + Returns: + pandas.DataFrame: A pandas DataFrame with the same number of rows + as the length of the wd and ws arrays, containing the following + columns: + + - **ws** (*float*) - The wind speed values for which the yaw + angles are optimized and power is computed (m/s). + - **wd** (*float*) - The wind direction values for which the + yaw angles are optimized and power is computed (deg). + - **ti** (*float*) - The turbulence intensity values for which + the yaw angles are optimized and power is computed. Only + included if self.ti is not None. + - **power_opt** (*float*) - The total power produced by the + wind farm with optimal yaw offsets (W). + - **turbine_power_opt** (*list* (*float*)) - A list + containing the power produced by each wind turbine with optimal + yaw offsets (W). + - **yaw_angles** (*list* (*float*)) - A list containing + the optimal yaw offsets for maximizing total wind farm power + for each wind turbine (deg). + """ + print("=====================================================") + print("Optimizing wake redirection control...") + print("Number of wind conditions to optimize = ", len(self.wd)) + print("Number of yaw angles to optimize = ", len(self.turbs_to_opt)) + print("=====================================================") + + df_opt = pd.DataFrame() + + for i in range(len(self.wd)): + if self.verbose: + if self.ti is None: + print( + "Computing wind speed, wind direction pair " + + str(i) + + " out of " + + str(len(self.wd)) + + ": wind speed = " + + str(self.ws[i]) + + " m/s, wind direction = " + + str(self.wd[i]) + + " deg." + ) + else: + print( + "Computing wind speed, wind direction, turbulence " + + "intensity set " + + str(i) + + " out of " + + str(len(self.wd)) + + ": wind speed = " + + str(self.ws[i]) + + " m/s, wind direction = " + + str(self.wd[i]) + + " deg, turbulence intensity = " + + str(self.ti[i]) + + "." + ) + + # Optimizing wake redirection control + if (self.ws[i] >= self.minimum_ws) & (self.ws[i] <= self.maximum_ws): + if self.ti is None: + self.fi.reinitialize_flow_field( + wind_direction=[self.wd[i]], wind_speed=[self.ws[i]] + ) + else: + self.fi.reinitialize_flow_field( + wind_direction=[self.wd[i]], + wind_speed=[self.ws[i]], + turbulence_intensity=self.ti[i], + ) + + # Set initial farm power + self.initial_farm_power = self.initial_farm_powers[i] + + # Determine clusters and then optimize by cluster + self._cluster_turbines() + if self.verbose: + print("Clustered turbines into %d separate clusters." % len(self.clusters)) + + # Save parameters to a full list + yaw_angles_template_full = copy.copy(self.yaw_angles_template) + yaw_angles_baseline_full = copy.copy(self.yaw_angles_baseline) + turbine_weights_full = copy.copy(self.turbine_weights) + bnds_full = copy.copy(self.bnds) + # nturbs_full = copy.copy(self.nturbs) + x0_full = copy.copy(self.x0) + fi_full = copy.deepcopy(self.fi) + + # Overwrite parameters for each cluster and optimize + opt_yaw_angles = np.zeros_like(x0_full) + for ci, cl in enumerate(self.clusters): + if self.verbose: + print("=====================================================") + print("Optimizing %d parameters in cluster %d." % (len(cl), ci)) + print("=====================================================") + self.yaw_angles_template = np.array(yaw_angles_template_full)[cl] + self.yaw_angles_baseline = np.array(yaw_angles_baseline_full)[cl] + self.turbine_weights = np.array(turbine_weights_full)[cl] + self.bnds = np.array(bnds_full)[cl] + self.x0 = np.array(x0_full)[cl] + self.fi = copy.deepcopy(fi_full) + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x)[cl], + np.array(fi_full.layout_y)[cl] + ] + ) + opt_yaw_angles[cl] = self._optimize() + + # Restore parameters + self.yaw_angles_template = yaw_angles_template_full + self.yaw_angles_baseline = yaw_angles_baseline_full + self.turbine_weights = turbine_weights_full + self.bnds = bnds_full + self.x0 = x0_full + self.fi = fi_full + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x), + np.array(fi_full.layout_y) + ] + ) + + if np.sum(np.abs(opt_yaw_angles)) == 0: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + + # optimized power + self.fi.calculate_wake(yaw_angles=opt_yaw_angles) + power_opt = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + ) + elif self.ws[i] >= self.maximum_ws: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + if self.ti is None: + self.fi.reinitialize_flow_field( + wind_direction=[self.wd[i]], wind_speed=[self.ws[i]] + ) + else: + self.fi.reinitialize_flow_field( + wind_direction=[self.wd[i]], + wind_speed=[self.ws[i]], + turbulence_intensity=self.ti[i], + ) + opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) + self.fi.calculate_wake(yaw_angles=opt_yaw_angles) + power_opt = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + ) + else: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) + power_opt = self.nturbs * [0.0] + + # Include turbine weighing terms + power_opt = np.multiply(self.turbine_weights, power_opt) + + # add variables to dataframe + if self.ti is None: + df_opt = df_opt.append( + pd.DataFrame( + { + "ws": [self.ws[i]], + "wd": [self.wd[i]], + "power_opt": [np.sum(power_opt)], + "turbine_power_opt": [power_opt], + "yaw_angles": [opt_yaw_angles], + } + ) + ) + else: + df_opt = df_opt.append( + pd.DataFrame( + { + "ws": [self.ws[i]], + "wd": [self.wd[i]], + "ti": [self.ti[i]], + "power_opt": [np.sum(power_opt)], + "turbine_power_opt": [power_opt], + "yaw_angles": [opt_yaw_angles], + } + ) + ) + + df_opt.reset_index(drop=True, inplace=True) + + return df_opt diff --git a/floris/tools/optimization/scipy/yaw_wind_rose_parallel_clustered.py b/floris/tools/optimization/scipy/yaw_wind_rose_parallel_clustered.py new file mode 100644 index 000000000..86b7b61e2 --- /dev/null +++ b/floris/tools/optimization/scipy/yaw_wind_rose_parallel_clustered.py @@ -0,0 +1,657 @@ +# Copyright 2021 NREL + +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# See https://floris.readthedocs.io for documentation + +from itertools import repeat + +import copy +import numpy as np +import pandas as pd +from scipy.optimize import minimize + +from .yaw_wind_rose_clustered import YawOptimizationWindRoseClustered +from ....logging_manager import LoggerBase + + +class YawOptimizationWindRoseParallelClustered(YawOptimizationWindRoseClustered, LoggerBase): + """ + YawOptimizationWindRoseClustered is a subclass of + :py:class:`~.tools.optimizationscipy.YawOptimizationWindRoseClustered` that + is used to perform optimizations of the yaw angles of all turbines in a + Floris Farm for multiple sets of inflow conditions (combinations of wind + speed, wind direction, and optionally turbulence intensity) using the scipy + optimize package. This class additionally facilitates the clusterization of + the turbines into seperate subsets (clusters) in which the turbines witin + each subset exclusively interact with one another and have no impact on turbines + in other clusters. This may significantly reduce the computational + burden at no loss in performance (assuming the turbine clusters are truly + independent). This class additionally facilitates parallel optimization + using the MPIPoolExecutor method of the mpi4py.futures module. + """ + + def __init__( + self, + fi, + wd, + ws, + ti=None, + minimum_yaw_angle=0.0, + maximum_yaw_angle=25.0, + minimum_ws=3.0, + maximum_ws=25.0, + yaw_angles_baseline=None, + x0=None, + bnds=None, + opt_method="SLSQP", + opt_options=None, + include_unc=False, + unc_pmfs=None, + unc_options=None, + turbine_weights=None, + exclude_downstream_turbines=False, + clustering_wake_slope=0.30 + ): + """ + Instantiate YawOptimizationWindRoseParallel object with a + FlorisInterface object and assign parameter values. + + Args: + fi (:py:class:`~.tools.floris_interface.FlorisInterface`): + Interface used to interact with the Floris object. + wd (iterable) : The wind directions for which the yaw angles are + optimized (deg). + ws (iterable): The wind speeds for which the yaw angles are + optimized (m/s). + ti (iterable, optional): An optional list of turbulence intensity + values for which the yaw angles are optimized. If not + specified, the current TI value in the Floris object will be + used for all optimizations. Defaults to None. + minimum_yaw_angle (float, optional): Minimum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 0.0. + maximum_yaw_angle (float, optional): Maximum constraint on yaw + angle (deg). This value will be ignored if bnds is also + specified. Defaults to 25.0. + minimum_ws (float, optional): Minimum wind speed at which + optimization is performed (m/s). Assumes zero power generated + below this value. Defaults to 3. + maximum_ws (float, optional): Maximum wind speed at which + optimization is performed (m/s). Assumes optimal yaw offsets + are zero above this wind speed. Defaults to 25. + yaw_angles_baseline (iterable, optional): The baseline yaw + angles used to calculate the initial and baseline power + production in the wind farm and used to normalize the cost + function. If none are specified, this variable is set equal + to the current yaw angles in floris. Note that this variable + need not meet the yaw constraints specified in self.bnds, + yet a warning is raised if it does to inform the user. + Defaults to None. + x0 (iterable, optional): The initial guess for the optimization + problem. These values must meet the constraints specified + in self.bnds. Note that, if exclude_downstream_turbines=True, + the initial guess for any downstream turbines are ignored + since they are not part of the optimization. Instead, the yaw + angles for those turbines are 0.0 if that meets the lower and + upper bound, or otherwise as close to 0.0 as feasible. If no + values for x0 are specified, x0 is set to be equal to zeros + wherever feasible (w.r.t. the bounds), and equal to the + average of its lower and upper bound for all non-downstream + turbines otherwise. Defaults to None. + bnds (iterable, optional): Bounds for the yaw angles, as tuples of + min, max values for each turbine (deg). One can fix the yaw + angle of certain turbines to a predefined value by setting that + turbine's lower bound equal to its upper bound (i.e., an + equality constraint), as: bnds[ti] = (x, x), where x is the + fixed yaw angle assigned to the turbine. This works for both + zero and nonzero yaw angles. Moreover, if + exclude_downstream_turbines=True, the yaw angles for all + downstream turbines will be 0.0 or a feasible value closest to + 0.0. If none are specified, the bounds are set to + (minimum_yaw_angle, maximum_yaw_angle) for each turbine. Note + that, if bnds is not none, its values overwrite any value given + in minimum_yaw_angle and maximum_yaw_angle. Defaults to None. + opt_method (str, optional): The optimization method used by + scipy.optimize.minize. Defaults to 'SLSQP'. + opt_options (dictionary, optional): Optimization options used by + scipy.optimize.minize. If none are specified, they are set to + {'maxiter': 100, 'disp': False, 'iprint': 1, 'ftol': 1e-7, + 'eps': 0.01}. Defaults to None. + include_unc (bool, optional): Determines whether wind direction or + yaw uncertainty are included. If True, uncertainty in wind + direction and/or yaw position is included when determining + wind farm power. Uncertainty is included by computing the + mean wind farm power for a distribution of wind direction + and yaw position deviations from the intended wind direction + and yaw angles. Defaults to False. + unc_pmfs (dictionary, optional): A dictionary containing + probability mass functions describing the distribution of + wind direction and yaw position deviations when wind direction + and/or yaw position uncertainty is included in the power + calculations. Contains the following key-value pairs: + + - **wd_unc** (*np.array*): The wind direction + deviations from the intended wind direction (deg). + - **wd_unc_pmf** (*np.array*): The probability + of each wind direction deviation in **wd_unc** occuring. + - **yaw_unc** (*np.array*): The yaw angle deviations + from the intended yaw angles (deg). + - **yaw_unc_pmf** (*np.array*): The probability + of each yaw angle deviation in **yaw_unc** occuring. + + If none are specified, default PMFs are calculated using + values provided in **unc_options**. Defaults to None. + unc_options (dictionary, optional): A dictionary containing values + used to create normally-distributed, zero-mean probability mass + functions describing the distribution of wind direction and yaw + position deviations when wind direction and/or yaw position + uncertainty is included. This argument is only used when + **unc_pmfs** is None and contains the following key-value pairs: + + - **std_wd** (*float*): The standard deviation of + the wind direction deviations from the original wind + direction (deg). + - **std_yaw** (*float*): The standard deviation of + the yaw angle deviations from the original yaw angles (deg). + - **pmf_res** (*float*): The resolution in degrees + of the wind direction and yaw angle PMFs. + - **pdf_cutoff** (*float*): The cumulative + distribution function value at which the tails of the + PMFs are truncated. + + If none are specified, default values of + {'std_wd': 4.95, 'std_yaw': 1.75, 'pmf_res': 1.0, + 'pdf_cutoff': 0.995} are used. Defaults to None. + turbine_weights (iterable, optional): weighing terms that allow + the user to emphasize power gains at particular turbines or + completely ignore power gains from other turbines. The array + of turbine powers from floris is multiplied with this array + in the calculation of the objective function. If None, this + is an array with all values 1.0 and length equal to the + number of turbines. Defaults to None. + exclude_downstream_turbines (bool, optional): If True, + automatically finds and excludes turbines that are most + downstream from the optimization problem. This significantly + reduces computation time at no loss in performance. The yaw + angles of these downstream turbines are fixed to 0.0 deg if + the yaw bounds specified in self.bnds allow that, or otherwise + are fixed to the lower or upper yaw bound, whichever is closer + to 0.0. Defaults to False. + clustering_wake_slope (float, optional): linear slope of the wake + in the simplified linear expansion wake model (dy/dx). This + model is used to derive wake interactions between turbines and + to identify the turbine clusters. A good value is about equal + to the turbulence intensity in FLORIS. Though, since yaw + optimizations may shift the wake laterally, a safer option + is twice the turbulence intensity. The default value is 0.30 + which should be valid for yaw optimizations at wd_std = 0.0 deg + and turbulence intensities up to 15%. Defaults to 0.30. + """ + super().__init__( + fi, + wd, + ws, + ti=ti, + minimum_yaw_angle=minimum_yaw_angle, + maximum_yaw_angle=maximum_yaw_angle, + minimum_ws=minimum_ws, + maximum_ws=maximum_ws, + yaw_angles_baseline=yaw_angles_baseline, + x0=x0, + bnds=bnds, + opt_method=opt_method, + opt_options=opt_options, + include_unc=include_unc, + unc_pmfs=unc_pmfs, + unc_options=unc_options, + turbine_weights=turbine_weights, + calc_init_power=False, + exclude_downstream_turbines=exclude_downstream_turbines, + clustering_wake_slope=clustering_wake_slope + ) + self.clustering_wake_slope = clustering_wake_slope + + # Private methods + + def _calc_baseline_power_one_case(self, ws, wd, ti=None): + """ + For a single (wind speed, direction, ti (optional)) combination, finds + the baseline power produced by the wind farm and the ideal power + without wake losses. + + Args: + ws (float): The wind speed used in floris for the yaw optimization. + wd (float): The wind direction used in floris for the yaw + optimization. + ti (float, optional): An optional turbulence intensity value for + the yaw optimization. Defaults to None, meaning TI will not be + included in the AEP calculations. + + Returns: + - **df_base** (*Pandas DataFrame*) - DataFrame with a single row, + containing the following columns: + + - **ws** (*float*) - The wind speed value for the row. + - **wd** (*float*) - The wind direction value for the row. + - **ti** (*float*) - The turbulence intensity value for the + row. Only included if self.ti is not None. + - **power_baseline** (*float*) - The total power produced by + the wind farm with baseline yaw control (W). + - **power_no_wake** (*float*) - The ideal total power produced + by the wind farm without wake losses (W). + - **turbine_power_baseline** (*list* (*float*)) - A + list containing the baseline power without wake steering + for each wind turbine (W). + - **turbine_power_no_wake** (*list* (*float*)) - A list + containing the ideal power without wake losses for each + wind turbine (W). + """ + if ti is None: + print( + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." + ) + else: + print( + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg, turbulence intensity = " + + str(ti) + + "." + ) + + # Find baseline power in FLORIS + + if ws >= self.minimum_ws: + if ti is None: + self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) + else: + self.fi.reinitialize_flow_field( + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + ) + # calculate baseline power + self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline) + power_base = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + ) + # calculate power for no wake case + self.fi.calculate_wake(yaw_angles=self.yaw_angles_baseline, no_wake=True) + power_no_wake = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + no_wake=True, + ) + else: + power_base = self.nturbs * [0.0] + power_no_wake = self.nturbs * [0.0] + + # Add turbine weighing terms + power_base = np.multiply(self.turbine_weights, power_base) + power_no_wake = np.multiply(self.turbine_weights, power_no_wake) + + # add variables to dataframe + if ti is None: + df_base = pd.DataFrame( + { + "ws": [ws], + "wd": [wd], + "power_baseline": [np.sum(power_base)], + "turbine_power_baseline": [power_base], + "power_no_wake": [np.sum(power_no_wake)], + "turbine_power_no_wake": [power_no_wake], + } + ) + else: + df_base = pd.DataFrame( + { + "ws": [ws], + "wd": [wd], + "ti": [ti], + "power_baseline": [np.sum(power_base)], + "turbine_power_baseline": [power_base], + "power_no_wake": [np.sum(power_no_wake)], + "turbine_power_no_wake": [power_no_wake], + } + ) + + return df_base + + def _optimize_one_case(self, ws, wd, initial_farm_power, ti=None): + """ + For a single (wind speed, direction, ti (optional)) combination, finds + the power resulting from optimal wake steering. + + Args: + ws (float): The wind speed used in floris for the yaw optimization. + wd (float): The wind direction used in floris for the yaw + optimization. + ti (float, optional): An optional turbulence intensity value for + the yaw optimization. Defaults to None, meaning TI will not be + included in the AEP calculations. + + Returns: + - **df_opt** (*Pandas DataFrame*) - DataFrame with a single row, + containing the following columns: + + - **ws** (*float*) - The wind speed value for the row. + - **wd** (*float*) - The wind direction value for the row. + - **ti** (*float*) - The turbulence intensity value for the + row. Only included if self.ti is not None. + - **power_opt** (*float*) - The total power produced by the + wind farm with optimal yaw offsets (W). + - **turbine_power_opt** (*list* (*float*)) - A list + containing the power produced by each wind turbine with + optimal yaw offsets (W). + - **yaw_angles** (*list* (*float*)) - A list containing + the optimal yaw offsets for maximizing total wind farm + power for each wind turbine (deg). + """ + if ti is None: + print( + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg." + ) + else: + print( + "Computing wind speed = " + + str(ws) + + " m/s, wind direction = " + + str(wd) + + " deg, turbulence intensity = " + + str(ti) + + "." + ) + + # Optimizing wake redirection control + + if (ws >= self.minimum_ws) & (ws <= self.maximum_ws): + if ti is None: + self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) + else: + self.fi.reinitialize_flow_field( + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + ) + + self.initial_farm_power = initial_farm_power + + # Determine clusters and then optimize by cluster + self._cluster_turbines() + + # Save parameters to a full list + yaw_angles_template_full = copy.copy(self.yaw_angles_template) + yaw_angles_baseline_full = copy.copy(self.yaw_angles_baseline) + turbine_weights_full = copy.copy(self.turbine_weights) + bnds_full = copy.copy(self.bnds) + x0_full = copy.copy(self.x0) + fi_full = copy.deepcopy(self.fi) + + # Overwrite parameters for each cluster and optimize + opt_yaw_angles = np.zeros_like(x0_full) + for ci, cl in enumerate(self.clusters): + if self.verbose: + print("=====================================================") + print("Optimizing %d parameters in cluster %d." % (len(cl), ci)) + print("=====================================================") + self.yaw_angles_template = np.array(yaw_angles_template_full)[cl] + self.yaw_angles_baseline = np.array(yaw_angles_baseline_full)[cl] + self.turbine_weights = np.array(turbine_weights_full)[cl] + self.bnds = np.array(bnds_full)[cl] + self.x0 = np.array(x0_full)[cl] + self.fi = copy.deepcopy(fi_full) + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x)[cl], + np.array(fi_full.layout_y)[cl] + ] + ) + opt_yaw_angles[cl] = self._optimize() + + # Restore parameters + self.yaw_angles_template = yaw_angles_template_full + self.yaw_angles_baseline = yaw_angles_baseline_full + self.turbine_weights = turbine_weights_full + self.bnds = bnds_full + self.x0 = x0_full + self.fi = fi_full + self.fi.reinitialize_flow_field( + layout_array=[ + np.array(fi_full.layout_x), + np.array(fi_full.layout_y) + ] + ) + + if np.sum(np.abs(opt_yaw_angles)) == 0: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + + # optimized power + self.fi.calculate_wake(yaw_angles=opt_yaw_angles) + power_opt = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + ) + elif ws >= self.minimum_ws: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + if ti is None: + self.fi.reinitialize_flow_field(wind_direction=wd, wind_speed=ws) + else: + self.fi.reinitialize_flow_field( + wind_direction=wd, wind_speed=ws, turbulence_intensity=ti + ) + opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) + self.fi.calculate_wake(yaw_angles=opt_yaw_angles) + power_opt = self.fi.get_turbine_power( + include_unc=self.include_unc, + unc_pmfs=self.unc_pmfs, + unc_options=self.unc_options, + ) + else: + print( + "No change in controls suggested for this inflow \ + condition..." + ) + opt_yaw_angles = np.array(self.yaw_angles_template, copy=True) + power_opt = self.nturbs * [0.0] + + # Add turbine weighing terms + power_opt = np.multiply(self.turbine_weights, power_opt) + + # add variables to dataframe + if ti is None: + df_opt = pd.DataFrame( + { + "ws": [ws], + "wd": [wd], + "power_opt": [np.sum(power_opt)], + "turbine_power_opt": [power_opt], + "yaw_angles": [opt_yaw_angles], + } + ) + else: + df_opt = pd.DataFrame( + { + "ws": [ws], + "wd": [wd], + "ti": [ti], + "power_opt": [np.sum(power_opt)], + "turbine_power_opt": [power_opt], + "yaw_angles": [opt_yaw_angles], + } + ) + + return df_opt + + # Public methods + + def calc_baseline_power(self): + """ + This method computes the baseline power produced by the wind farm and + the ideal power without wake losses for a series of wind speed, wind + direction, and optionally TI combinations. The optimization for + different wind condition combinations is parallelized using the mpi4py + futures module. + + Returns: + pandas.DataFrame: A pandas DataFrame with the same number of rows + as the length of the wd and ws arrays, containing the following + columns: + + - **ws** (*float*) - The wind speed values for which power is + computed (m/s). + - **wd** (*float*) - The wind direction value for which power + is calculated (deg). + - **ti** (*float*) - The turbulence intensity value for which + power is calculated. Only included if self.ti is not None. + - **power_baseline** (*float*) - The total power produced by + he wind farm with baseline yaw control (W). + - **power_no_wake** (*float*) - The ideal total power produced + by the wind farm without wake losses (W). + - **turbine_power_baseline** (*list* (*float*)) - A list + containing the baseline power without wake steering for each + wind turbine in the wind farm (W). + - **turbine_power_no_wake** (*list* (*float*)) - A list + containing the ideal power without wake losses for each wind + turbine in the wind farm (W). + """ + try: + from mpi4py.futures import MPIPoolExecutor + except ImportError: + err_msg = ( + "It appears you do not have mpi4py installed. " + + "Please refer to https://mpi4py.readthedocs.io/ for " + + "guidance on how to properly install the module." + ) + self.logger.error(err_msg, stack_info=True) + raise ImportError(err_msg) + + print("=====================================================") + print("Calculating baseline power in parallel...") + print("Number of wind conditions to calculate = ", len(self.wd)) + print("=====================================================") + + df_base = pd.DataFrame() + + with MPIPoolExecutor() as executor: + if self.ti is None: + for df_base_one in executor.map( + self._calc_baseline_power_one_case, self.ws.values, self.wd.values + ): + + # add variables to dataframe + df_base = df_base.append(df_base_one) + else: + for df_base_one in executor.map( + self._calc_baseline_power_one_case, + self.ws.values, + self.wd.values, + self.ti.values, + ): + + # add variables to dataframe + df_base = df_base.append(df_base_one) + + df_base.reset_index(drop=True, inplace=True) + + self.df_base = df_base + return df_base + + def optimize(self): + """ + This method solves for the optimum turbine yaw angles for power + production and the resulting power produced by the wind farm for a + series of wind speed, wind direction, and optionally TI combinations. + The optimization for different wind condition combinations is + parallelized using the mpi4py.futures module. + + Returns: + pandas.DataFrame: A pandas DataFrame with the same number of rows + as the length of the wd and ws arrays, containing the following + columns: + + - **ws** (*float*) - The wind speed values for which the yaw + angles are optimized and power is computed (m/s). + - **wd** (*float*) - The wind direction values for which the + yaw angles are optimized and power is computed (deg). + - **ti** (*float*) - The turbulence intensity values for which + the yaw angles are optimized and power is computed. Only + included if self.ti is not None. + - **power_opt** (*float*) - The total power produced by the + wind farm with optimal yaw offsets (W). + - **turbine_power_opt** (*list* (*float*)) - A list containing + the power produced by each wind turbine with optimal yaw + offsets (W). + - **yaw_angles** (*list* (*float*)) - A list containing the + optimal yaw offsets for maximizing total wind farm power for + each wind turbine (deg). + """ + try: + from mpi4py.futures import MPIPoolExecutor + except ImportError: + err_msg = ( + "It appears you do not have mpi4py installed. " + + "Please refer to https://mpi4py.readthedocs.io/ for " + + "guidance on how to properly install the module." + ) + self.logger.error(err_msg, stack_info=True) + raise ImportError(err_msg) + + print("=====================================================") + print("Optimizing wake redirection control in parallel...") + print("Number of wind conditions to optimize = ", len(self.wd)) + print("Number of yaw angles to optimize = ", len(self.turbs_to_opt)) + print("=====================================================") + + df_opt = pd.DataFrame() + + with MPIPoolExecutor() as executor: + if self.ti is None: + for df_opt_one in executor.map( + self._optimize_one_case, + self.ws.values, + self.wd.values, + self.df_base.power_baseline.values, + ): + + # add variables to dataframe + df_opt = df_opt.append(df_opt_one) + else: + for df_opt_one in executor.map( + self._optimize_one_case, + self.ws.values, + self.wd.values, + self.df_base.power_baseline.values, + self.ti.values, + ): + + # add variables to dataframe + df_opt = df_opt.append(df_opt_one) + + df_opt.reset_index(drop=True, inplace=True) + + return df_opt