From 5fff07ae32243ec41f0b259ae4b472c0617ced32 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 11:18:09 -0700 Subject: [PATCH 01/11] Update MixedOperationModel to better handle disabling turbines, and remove disabled turbines from default power_setpoints on FlorisModel. --- floris/core/turbine/operation_models.py | 92 ++++++++++++++++--------- floris/floris_model.py | 5 +- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index a6c1ff160..fb33793df 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -382,22 +382,22 @@ def axial_induction( @define class MixedOperationTurbine(BaseOperationModel): + @staticmethod def power( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - # Yaw angles mask all yaw_angles not equal to zero - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) powers = np.zeros_like(power_setpoints) powers[yaw_angles_mask] += CosineLossTurbine.power( @@ -414,21 +414,22 @@ def power( return powers + @staticmethod def thrust_coefficient( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) thrust_coefficients = np.zeros_like(power_setpoints) thrust_coefficients[yaw_angles_mask] += CosineLossTurbine.thrust_coefficient( @@ -445,21 +446,22 @@ def thrust_coefficient( return thrust_coefficients + @staticmethod def axial_induction( yaw_angles: NDArrayFloat, power_setpoints: NDArrayFloat, **kwargs ): - yaw_angles_mask = yaw_angles != 0.0 - power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT - neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) - - if (power_setpoints_mask & yaw_angles_mask).any(): - raise ValueError(( - "Power setpoints and yaw angles are incompatible." - "If yaw_angles entry is nonzero, power_setpoints must be greater than" - " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) - )) + ( + yaw_angles, + power_setpoints, + yaw_angles_mask, + power_setpoints_mask, + neither_mask + ) = MixedOperationTurbine._handle_mixed_operation_setpoints( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints + ) axial_inductions = np.zeros_like(power_setpoints) axial_inductions[yaw_angles_mask] += CosineLossTurbine.axial_induction( @@ -476,6 +478,34 @@ def axial_induction( return axial_inductions + @staticmethod + def _handle_mixed_operation_setpoints( + yaw_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + ): + """ + Check for incompatible yaw angles and power setpoints and raise an error if found. + Return masks and updated setpoints. + """ + # If any turbines are disabled, set their yaw angles to zero + yaw_angles[power_setpoints <= POWER_SETPOINT_DISABLED] = 0.0 + + # Create masks for whether yaw angles and power setpoints are set + yaw_angles_mask = yaw_angles != 0.0 + power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT + neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask) + + # Check for incompatibility and raise error if found. + if (power_setpoints_mask & yaw_angles_mask).any(): + raise ValueError(( + "Power setpoints and yaw angles are incompatible." + "If yaw_angles entry is nonzero, power_setpoints must be greater than" + " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) + )) + + # Return updated setpoints as well as masks + return yaw_angles, power_setpoints, yaw_angles_mask, power_setpoints_mask, neither_mask + @define class AWCTurbine(BaseOperationModel): """ diff --git a/floris/floris_model.py b/floris/floris_model.py index 09a5aa5d0..af8255232 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -453,10 +453,7 @@ def set( # previous setting if not (_yaw_angles == 0).all(): self.core.farm.set_yaw_angles(_yaw_angles) - if not ( - (_power_setpoints == POWER_SETPOINT_DEFAULT) - | (_power_setpoints == POWER_SETPOINT_DISABLED) - ).all(): + if not (_power_setpoints == POWER_SETPOINT_DEFAULT).all(): self.core.farm.set_power_setpoints(_power_setpoints) if _awc_modes is not None: self.core.farm.set_awc_modes(_awc_modes) From c51d61733a6b5f59a0006ad27fc4a6b27b542edd Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 11:47:00 -0700 Subject: [PATCH 02/11] Update yaw optimization to pass through power_setpoints. --- floris/core/turbine/operation_models.py | 4 ++-- .../optimization/yaw_optimization/yaw_optimization_base.py | 6 ++++-- floris/optimization/yaw_optimization/yaw_optimizer_sr.py | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index fb33793df..066e26b04 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -489,7 +489,7 @@ def _handle_mixed_operation_setpoints( """ # If any turbines are disabled, set their yaw angles to zero yaw_angles[power_setpoints <= POWER_SETPOINT_DISABLED] = 0.0 - + # Create masks for whether yaw angles and power setpoints are set yaw_angles_mask = yaw_angles != 0.0 power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT @@ -502,7 +502,7 @@ def _handle_mixed_operation_setpoints( "If yaw_angles entry is nonzero, power_setpoints must be greater than" " or equal to {0}.".format(POWER_SETPOINT_DEFAULT) )) - + # Return updated setpoints as well as masks return yaw_angles, power_setpoints, yaw_angles_mask, power_setpoints_mask, neither_mask diff --git a/floris/optimization/yaw_optimization/yaw_optimization_base.py b/floris/optimization/yaw_optimization/yaw_optimization_base.py index 07a2f7e11..623ba74b0 100644 --- a/floris/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/optimization/yaw_optimization/yaw_optimization_base.py @@ -99,7 +99,7 @@ def __init__( """ # Save turbine object to self - self.fmodel = fmodel.copy() + self.fmodel = copy.deepcopy(fmodel) self.nturbs = len(self.fmodel.layout_x) # # Check floris options @@ -224,7 +224,7 @@ def _reduce_control_problem(self): self.turbs_to_opt = (self.maximum_yaw_angle - self.minimum_yaw_angle >= 0.001) # Initialize subset variables as full set - self.fmodel_subset = self.fmodel.copy() + self.fmodel_subset = copy.deepcopy(self.fmodel) n_findex_subset = copy.deepcopy(self.fmodel.core.flow_field.n_findex) minimum_yaw_angle_subset = copy.deepcopy(self.minimum_yaw_angle) maximum_yaw_angle_subset = copy.deepcopy(self.maximum_yaw_angle) @@ -301,6 +301,7 @@ def _calculate_farm_power( ti_array=None, turbine_weights=None, heterogeneous_speed_multipliers=None, + power_setpoints=None, ): """ Calculate the wind farm power production assuming the predefined @@ -353,6 +354,7 @@ def _calculate_farm_power( wind_speeds=ws_array, turbulence_intensities=ti_array, yaw_angles=yaw_angles, + power_setpoints=power_setpoints, ) fmodel_subset.run() turbine_power = fmodel_subset.get_turbine_powers() diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py index 2b5b7ad1b..13266080e 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -93,6 +93,7 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): wd_array_subset = self.fmodel_subset.core.flow_field.wind_directions ws_array_subset = self.fmodel_subset.core.flow_field.wind_speeds ti_array_subset = self.fmodel_subset.core.flow_field.turbulence_intensities + power_setpoints_subset = self.fmodel_subset.core.farm.power_setpoints turbine_weights_subset = self._turbine_weights_subset # Reformat yaw_angles_subset, if necessary @@ -108,6 +109,7 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): wd_array_subset = np.tile(wd_array_subset, Ny) ws_array_subset = np.tile(ws_array_subset, Ny) ti_array_subset = np.tile(ti_array_subset, Ny) + power_setpoints_subset = np.tile(power_setpoints_subset, (Ny, 1)) turbine_weights_subset = np.tile(turbine_weights_subset, (Ny, 1)) # Initialize empty matrix for floris farm power outputs @@ -143,6 +145,7 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): ti_array=ti_array_subset[~idx], turbine_weights=turbine_weights_subset[~idx, :], yaw_angles=yaw_angles_subset[~idx, :], + power_setpoints=power_setpoints_subset[~idx, :], heterogeneous_speed_multipliers=het_sm ) self.time_spent_in_floris += (timerpc() - start_time) From ba7bd048c253ff61a1a22c3e94c9fcaab92b6586 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 12:51:12 -0700 Subject: [PATCH 03/11] Raise warning for incompatible op model in scipy optimizer --- floris/optimization/yaw_optimization/yaw_optimizer_scipy.py | 6 ++++++ floris/optimization/yaw_optimization/yaw_optimizer_sr.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py index cdde87656..810144c50 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -30,6 +30,12 @@ def __init__( Instantiate YawOptimizationScipy object with a FlorisModel object and assign parameter values. """ + valid_op_models = ["cosine-loss"] + if fmodel.get_operation_model() not in valid_op_models: + raise ValueError( + "YawOptimizationScipy is currently limited to the following operation models: " + + ", ".join(valid_op_models) + ) if opt_options is None: # Default SciPy parameters opt_options = { diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py index 13266080e..127cca01b 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -145,8 +145,8 @@ def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): ti_array=ti_array_subset[~idx], turbine_weights=turbine_weights_subset[~idx, :], yaw_angles=yaw_angles_subset[~idx, :], + heterogeneous_speed_multipliers=het_sm, power_setpoints=power_setpoints_subset[~idx, :], - heterogeneous_speed_multipliers=het_sm ) self.time_spent_in_floris += (timerpc() - start_time) From f1a6760b3ef925db71b3ebd3e24e60f758df5018 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 13:37:11 -0700 Subject: [PATCH 04/11] Enable geomtric yaw optimization with disabled turbines. --- .../yaw_optimization/yaw_optimizer_geometric.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py index ea68204b4..68c687512 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py @@ -1,6 +1,7 @@ import numpy as np +from floris.core.turbine.operation_models import POWER_SETPOINT_DISABLED from floris.utilities import rotate_coordinates_rel_west from .yaw_optimization_base import YawOptimization @@ -46,10 +47,11 @@ def optimize(self): # Loop through every WD individually. WS ignored! wd_array = self.fmodel_subset.core.flow_field.wind_directions + active_turbines = self.fmodel_subset.core.farm.power_setpoints > POWER_SETPOINT_DISABLED for nwdi, wd in enumerate(wd_array): - self._yaw_angles_opt_subset[nwdi, :] = geometric_yaw( - self.fmodel_subset.layout_x, - self.fmodel_subset.layout_y, + self._yaw_angles_opt_subset[nwdi, active_turbines[nwdi]] = geometric_yaw( + self.fmodel_subset.layout_x[active_turbines[nwdi]], + self.fmodel_subset.layout_y[active_turbines[nwdi]], wd, self.fmodel.core.farm.turbine_definitions[0]["rotor_diameter"], top_left_yaw_upper=self.maximum_yaw_angle[0, 0], From fb4313c96f1e184149dff4d3e2564c86a748d792 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 13:37:35 -0700 Subject: [PATCH 05/11] Add example of optimizing with disabled turbines. --- ...008_optimize_yaw_with_disabled_turbines.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py diff --git a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py new file mode 100644 index 000000000..576f83e96 --- /dev/null +++ b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py @@ -0,0 +1,33 @@ +"""Example: Optimizing yaw angles with disabled turbines + +This example demonstrates how to optimize yaw angles in FLORIS, when some turbines are disabled. +The example optimization is run using both YawOptimizerSR and YawOptimizerGeometric, the two +yaw optimizers that support disabling turbines. +""" + +import numpy as np + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + +# Load a 3-turbine model +fmodel = FlorisModel("../inputs/gch.yaml") + +# Set wind conditions to be the same for two cases +fmodel.set(wind_directions=[270.]*2, wind_speeds=[8.]*2, turbulence_intensities=[.06]*2) + +# Disable turbines (different pattern for each of the two cases) +# First case: disable the middle turbine +# Second case: disable the front turbine +fmodel.set_operation_model('mixed') +fmodel.set(disable_turbines=np.array([[False, True, False], [True, False, False]])) + +# Run optimizations and print results +yaw_opt = YawOptimizationSR(fmodel) +df_opt = yaw_opt.optimize() +print("Serial Refine optimized yaw angles [deg]:\n", df_opt.yaw_angles_opt) + +yaw_opt = YawOptimizationGeometric(fmodel) +df_opt = yaw_opt.optimize() +print("\nGeometric optimized yaw angles [deg]:\n", df_opt.yaw_angles_opt) From bd9650b2ce37c25a8b236bef08f5582644a4c94a Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 15 Nov 2024 13:50:26 -0700 Subject: [PATCH 06/11] Update example to include all active case. --- ...008_optimize_yaw_with_disabled_turbines.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py index 576f83e96..95764e9e0 100644 --- a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py +++ b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py @@ -11,23 +11,38 @@ from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + # Load a 3-turbine model fmodel = FlorisModel("../inputs/gch.yaml") # Set wind conditions to be the same for two cases fmodel.set(wind_directions=[270.]*2, wind_speeds=[8.]*2, turbulence_intensities=[.06]*2) +# First run the case where all turbines are active and print results +yaw_opt = YawOptimizationSR(fmodel) +df_opt = yaw_opt.optimize() +print("Serial Refine optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt) + +yaw_opt = YawOptimizationGeometric(fmodel) +df_opt = yaw_opt.optimize() +print("\nGeometric optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt) + # Disable turbines (different pattern for each of the two cases) # First case: disable the middle turbine # Second case: disable the front turbine fmodel.set_operation_model('mixed') fmodel.set(disable_turbines=np.array([[False, True, False], [True, False, False]])) -# Run optimizations and print results +# Rerun optimizations and print results yaw_opt = YawOptimizationSR(fmodel) df_opt = yaw_opt.optimize() -print("Serial Refine optimized yaw angles [deg]:\n", df_opt.yaw_angles_opt) +print( + "\nSerial Refine optimized yaw angles (some turbines disabled) [deg]:\n", + df_opt.yaw_angles_opt +) +# Note that disabled turbines may not have a zero yaw angle, because a disabled turbine's +# yaw angle does not affect the total power output. yaw_opt = YawOptimizationGeometric(fmodel) df_opt = yaw_opt.optimize() -print("\nGeometric optimized yaw angles [deg]:\n", df_opt.yaw_angles_opt) +print("\nGeometric optimized yaw angles (some turbines disabled) [deg]:\n", df_opt.yaw_angles_opt) From 757ffa7ffee2c93fcf9c55b5aa69bc2198814b04 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 10:35:31 -0700 Subject: [PATCH 07/11] Add tests for yaw opt with turbines disabled, as well as basic serial refine operation. --- tests/floris_model_integration_test.py | 21 ++++- tests/serial_refine_unit_test.py | 107 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 tests/serial_refine_unit_test.py diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index fa05d43d3..a09a53ac1 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -10,7 +10,7 @@ TimeSeries, WindRose, ) -from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT +from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT, POWER_SETPOINT_DISABLED TEST_DATA = Path(__file__).resolve().parent / "data" @@ -44,7 +44,24 @@ def test_assign_setpoints(): # power_setpoints and disable_turbines (disable_turbines overrides power_setpoints) fmodel.set(power_setpoints=[[1e6, 2e6]], disable_turbines=[[True, False]]) - assert np.allclose(fmodel.core.farm.power_setpoints, np.array([[0.001, 2e6]])) + assert np.allclose(fmodel.core.farm.power_setpoints, np.array([[POWER_SETPOINT_DISABLED, 2e6]])) + + # Setting sequentially is equivalent to setting together + fmodel.reset_operation() + fmodel.set(disable_turbines=[[True, False]]) + fmodel.set(yaw_angles=[[0, 30]]) + assert np.allclose( + fmodel.core.farm.power_setpoints, + np.array([[POWER_SETPOINT_DISABLED, POWER_SETPOINT_DEFAULT]]) + ) + assert np.allclose(fmodel.core.farm.yaw_angles, np.array([[0, 30]])) + + fmodel.set(disable_turbines=[[True, False]], yaw_angles=[[0, 30]]) + assert np.allclose( + fmodel.core.farm.power_setpoints, + np.array([[POWER_SETPOINT_DISABLED, POWER_SETPOINT_DEFAULT]]) + ) + assert np.allclose(fmodel.core.farm.yaw_angles, np.array([[0, 30]])) def test_set_run(): """ diff --git a/tests/serial_refine_unit_test.py b/tests/serial_refine_unit_test.py new file mode 100644 index 000000000..cfda030a7 --- /dev/null +++ b/tests/serial_refine_unit_test.py @@ -0,0 +1,107 @@ + +import numpy as np +import pandas as pd + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR + + +DEBUG = False +VELOCITY_MODEL = "gauss" +DEFLECTION_MODEL = "gauss" + +# Inputs for basic yaw optimizations +WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] +WIND_SPEEDS = [8.0] * 4 +TURBULENCE_INTENSITIES = [0.06] * 4 +LAYOUT_X = [0.0, 600.0, 1200.0] +LAYOUT_Y = [0.0, 0.0, 0.0] +MAXIMUM_YAW_ANGLE = 25.0 + +def test_basic_optimization(sample_inputs_fixture): + """ + The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw + optimization scheme. This test checks basic properties of the optimization result. + """ + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + + fmodel = FlorisModel(sample_inputs_fixture.core) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + + # Unaligned conditions + assert np.allclose(df_opt.loc[0, "yaw_angles_opt"], 0.0) + assert np.allclose(df_opt.loc[2, "yaw_angles_opt"], 0.0) + + # Check aligned conditions + # Check maximum and minimum are respected + assert (df_opt.loc[1, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[3, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[1, "yaw_angles_opt"] >= 0.0).all() + assert (df_opt.loc[3, "yaw_angles_opt"] >= 0.0).all() + + # Check 90.0 and 270.0 are symmetric + assert np.allclose(df_opt.loc[1, "yaw_angles_opt"], np.flip(df_opt.loc[3, "yaw_angles_opt"])) + + # Check last turbine's angles are zero at 270.0 + assert np.allclose(df_opt.loc[3, "yaw_angles_opt"][-1], 0.0) + + # Check that optimizer reports a power improvement + assert (df_opt["farm_power_opt"] >= df_opt["farm_power_baseline"]).all() + +def test_disabled_turbines(sample_inputs_fixture): + """ + Tests SR when some turbines are disabled and checks that the results are equivalent to removing + those turbines from the wind farm. Need a tight layout to ensure that the front-to-back distance + is not too large. + """ + + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + + fmodel = FlorisModel(sample_inputs_fixture.core) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("mixed") + + # Disable the middle turbine in all wind conditions, run optimization, and extract results + fmodel.set(disable_turbines=[[False, True, False]]*4) + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_disabled = df_opt.loc[3, "yaw_angles_opt"] + farm_power_opt_disabled = df_opt.loc[3, "farm_power_opt"] + + # Set up a new wind farm with the middle turbine removed + fmodel = FlorisModel(sample_inputs_fixture.core) + fmodel.set( + layout_x=np.array(LAYOUT_X)[[0, 2]], + layout_y=np.array(LAYOUT_Y)[[0, 2]], + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + yaw_opt = YawOptimizationSR(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_removed = df_opt.loc[3, "yaw_angles_opt"] + farm_power_opt_removed = df_opt.loc[3, "farm_power_opt"] + + assert np.allclose(yaw_angles_opt_disabled[[0, 2]], yaw_angles_opt_removed) + assert np.allclose(farm_power_opt_disabled, farm_power_opt_removed) From 3d64d6a89fd8530fc609280cdb36ebde9ff264d4 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 10:39:26 -0700 Subject: [PATCH 08/11] Add similar test for Geometric yaw (only farm power comparisons removed). --- tests/geometric_yaw_unit_test.py | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/geometric_yaw_unit_test.py diff --git a/tests/geometric_yaw_unit_test.py b/tests/geometric_yaw_unit_test.py new file mode 100644 index 000000000..429cd9bf2 --- /dev/null +++ b/tests/geometric_yaw_unit_test.py @@ -0,0 +1,103 @@ + +import numpy as np +import pandas as pd + +from floris import FlorisModel +from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric + + +DEBUG = False +VELOCITY_MODEL = "gauss" +DEFLECTION_MODEL = "gauss" + +# Inputs for basic yaw optimizations +WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] +WIND_SPEEDS = [8.0] * 4 +TURBULENCE_INTENSITIES = [0.06] * 4 +LAYOUT_X = [0.0, 600.0, 1200.0] +LAYOUT_Y = [0.0, 0.0, 0.0] +MAXIMUM_YAW_ANGLE = 25.0 + +def test_basic_optimization(sample_inputs_fixture): + """ + The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw + optimization scheme. This test checks basic properties of the optimization result. + """ + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + + fmodel = FlorisModel(sample_inputs_fixture.core) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + + yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + + # Unaligned conditions + assert np.allclose(df_opt.loc[0, "yaw_angles_opt"], 0.0) + assert np.allclose(df_opt.loc[2, "yaw_angles_opt"], 0.0) + + # Check aligned conditions + # Check maximum and minimum are respected + assert (df_opt.loc[1, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[3, "yaw_angles_opt"] <= MAXIMUM_YAW_ANGLE).all() + assert (df_opt.loc[1, "yaw_angles_opt"] >= 0.0).all() + assert (df_opt.loc[3, "yaw_angles_opt"] >= 0.0).all() + + # Check 90.0 and 270.0 are symmetric + assert np.allclose(df_opt.loc[1, "yaw_angles_opt"], np.flip(df_opt.loc[3, "yaw_angles_opt"])) + + # Check last turbine's angles are zero at 270.0 + assert np.allclose(df_opt.loc[3, "yaw_angles_opt"][-1], 0.0) + + # YawOptimizationGeometric does not compute farm powers + +def test_disabled_turbines(sample_inputs_fixture): + """ + Tests SR when some turbines are disabled and checks that the results are equivalent to removing + those turbines from the wind farm. Need a tight layout to ensure that the front-to-back distance + is not too large. + """ + + sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL + sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + + fmodel = FlorisModel(sample_inputs_fixture.core) + + fmodel.set( + layout_x=LAYOUT_X, + layout_y=LAYOUT_Y, + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("mixed") + + # Disable the middle turbine in all wind conditions, run optimization, and extract results + fmodel.set(disable_turbines=[[False, True, False]]*4) + yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_disabled = df_opt.loc[3, "yaw_angles_opt"] + + # Set up a new wind farm with the middle turbine removed + fmodel = FlorisModel(sample_inputs_fixture.core) + fmodel.set( + layout_x=np.array(LAYOUT_X)[[0, 2]], + layout_y=np.array(LAYOUT_Y)[[0, 2]], + wind_directions=WIND_DIRECTIONS, + wind_speeds=WIND_SPEEDS, + turbulence_intensities=TURBULENCE_INTENSITIES + ) + fmodel.set_operation_model("cosine-loss") + yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + df_opt = yaw_opt.optimize() + yaw_angles_opt_removed = df_opt.loc[3, "yaw_angles_opt"] + + assert np.allclose(yaw_angles_opt_disabled[[0, 2]], yaw_angles_opt_removed) From f30dc152733afbf493f09774f1d5a820aeaeff34 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 10:45:32 -0700 Subject: [PATCH 09/11] Formatting fixes. --- tests/geometric_yaw_unit_test.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/geometric_yaw_unit_test.py b/tests/geometric_yaw_unit_test.py index 429cd9bf2..2fb0452bb 100644 --- a/tests/geometric_yaw_unit_test.py +++ b/tests/geometric_yaw_unit_test.py @@ -37,7 +37,11 @@ def test_basic_optimization(sample_inputs_fixture): ) fmodel.set_operation_model("cosine-loss") - yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) df_opt = yaw_opt.optimize() # Unaligned conditions @@ -82,7 +86,11 @@ def test_disabled_turbines(sample_inputs_fixture): # Disable the middle turbine in all wind conditions, run optimization, and extract results fmodel.set(disable_turbines=[[False, True, False]]*4) - yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) df_opt = yaw_opt.optimize() yaw_angles_opt_disabled = df_opt.loc[3, "yaw_angles_opt"] @@ -96,7 +104,11 @@ def test_disabled_turbines(sample_inputs_fixture): turbulence_intensities=TURBULENCE_INTENSITIES ) fmodel.set_operation_model("cosine-loss") - yaw_opt = YawOptimizationGeometric(fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=MAXIMUM_YAW_ANGLE) + yaw_opt = YawOptimizationGeometric( + fmodel, + minimum_yaw_angle=0.0, + maximum_yaw_angle=MAXIMUM_YAW_ANGLE + ) df_opt = yaw_opt.optimize() yaw_angles_opt_removed = df_opt.loc[3, "yaw_angles_opt"] From 7ad2689e719b9d2ce1d942106059f4e0c3829a27 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 13:21:32 -0700 Subject: [PATCH 10/11] Limit yaw angles to zero for disabled turbines. --- .../optimization/yaw_optimization/yaw_optimization_base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/floris/optimization/yaw_optimization/yaw_optimization_base.py b/floris/optimization/yaw_optimization/yaw_optimization_base.py index 623ba74b0..1949d132c 100644 --- a/floris/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/optimization/yaw_optimization/yaw_optimization_base.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd +from floris.core.turbine.operation_models import POWER_SETPOINT_DISABLED from floris.logging_manager import LoggingManager from .yaw_optimization_tools import derive_downstream_turbines @@ -131,6 +132,11 @@ def __init__( self.minimum_yaw_angle = self._unpack_variable(minimum_yaw_angle) self.maximum_yaw_angle = self._unpack_variable(maximum_yaw_angle) + # Limit yaw angles to zero for disabled turbines + active_turbines = fmodel.core.farm.power_setpoints > POWER_SETPOINT_DISABLED + self.minimum_yaw_angle[~active_turbines] = 0.0 + self.maximum_yaw_angle[~active_turbines] = 0.0 + # Set initial condition for optimization if x0 is not None: self.x0 = self._unpack_variable(x0) From 817303bcf7e331781d3f4ad54e62536d36271401 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 18 Nov 2024 13:23:53 -0700 Subject: [PATCH 11/11] Update comment on disabled turbines' yaw angles. --- .../008_optimize_yaw_with_disabled_turbines.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py index 95764e9e0..bd4d80b7b 100644 --- a/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py +++ b/examples/examples_control_optimization/008_optimize_yaw_with_disabled_turbines.py @@ -40,8 +40,8 @@ "\nSerial Refine optimized yaw angles (some turbines disabled) [deg]:\n", df_opt.yaw_angles_opt ) -# Note that disabled turbines may not have a zero yaw angle, because a disabled turbine's -# yaw angle does not affect the total power output. +# Note that disabled turbines are assigned a zero yaw angle, but their yaw angle is arbitrary as it +# does not affect the total power output. yaw_opt = YawOptimizationGeometric(fmodel) df_opt = yaw_opt.optimize()