From ac96a164821415b7cad4390492c144331f57ea05 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 20 Feb 2024 19:47:15 -0700 Subject: [PATCH 01/33] Main infrastrcuture for set() -> run() paradigm on FlorisInterface. --- examples/01_opening_floris_computing_power.py | 20 +-- examples/04_sweep_wind_directions.py | 7 +- floris/tools/floris_interface.py | 167 +++++++++++------- 3 files changed, 113 insertions(+), 81 deletions(-) diff --git a/examples/01_opening_floris_computing_power.py b/examples/01_opening_floris_computing_power.py index 4e7818df6..f39611116 100644 --- a/examples/01_opening_floris_computing_power.py +++ b/examples/01_opening_floris_computing_power.py @@ -19,17 +19,16 @@ fi = FlorisInterface("inputs/gch.yaml") # Convert to a simple two turbine layout -fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) +fi.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) # Single wind speed and wind direction print("\n========================= Single Wind Direction and Wind Speed =========================") # Get the turbine powers assuming 1 wind direction and speed -fi.reinitialize(wind_directions=[270.0], wind_speeds=[8.0]) +# Set the yaw angles to 0 with 1 wind direction and speed, 2 turbines +fi.set(wind_directions=[270.0], wind_speeds=[8.0], yaw_angles=np.zeros([1, 2])) -# Set the yaw angles to 0 -yaw_angles = np.zeros([1, 2]) # 1 wind direction and speed, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() # Get the turbine powers turbine_powers = fi.get_turbine_powers() / 1000.0 @@ -44,9 +43,9 @@ wind_speeds = np.array([8.0, 9.0, 10.0]) wind_directions = np.array([270.0, 270.0, 270.0]) -fi.reinitialize(wind_speeds=wind_speeds, wind_directions=wind_directions) -yaw_angles = np.zeros([3, 2]) # 3 wind directions/ speeds, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +# 3 wind directions/ speeds, 2 turbines +fi.set(wind_speeds=wind_speeds, wind_directions=wind_directions, yaw_angles=np.zeros([3, 2])) +fi.run() turbine_powers = fi.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 3 findex X 2 Turbines") print(turbine_powers) @@ -60,9 +59,8 @@ wind_speeds = np.tile([8.0, 9.0, 10.0], 3) wind_directions = np.repeat([260.0, 270.0, 280.0], 3) -fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) -yaw_angles = np.zeros([9, 2]) # 9 wind directions/ speeds, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +fi.set(wind_directions=wind_directions, wind_speeds=wind_speeds, yaw_angles=np.zeros([9, 2])) +fi.run() turbine_powers = fi.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 9 WD/WS X 2 Turbines") print(turbine_powers) diff --git a/examples/04_sweep_wind_directions.py b/examples/04_sweep_wind_directions.py index a76ff6bb3..6cfa73612 100644 --- a/examples/04_sweep_wind_directions.py +++ b/examples/04_sweep_wind_directions.py @@ -22,12 +22,12 @@ D = 126. layout_x = np.array([0, D*6]) layout_y = [0, 0] -fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # Sweep wind speeds but keep wind direction fixed wd_array = np.arange(250,291,1.) ws_array = 8.0 * np.ones_like(wd_array) -fi.reinitialize(wind_directions=wd_array, wind_speeds=ws_array) +fi.set(wind_directions=wd_array, wind_speeds=ws_array) # Define a matrix of yaw angles to be all 0 # Note that yaw angles is now specified as a matrix whose dimensions are @@ -37,9 +37,10 @@ n_findex = num_wd # Could be either num_wd or num_ws num_turbine = len(layout_x) # Number of turbines yaw_angles = np.zeros((n_findex, num_turbine)) +fi.set(yaw_angles=yaw_angles) # Calculate -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() # Collect the turbine powers turbine_powers = fi.get_turbine_powers() / 1E3 # In kW diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index e9c5aa2f5..091f4eda7 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -107,14 +107,61 @@ def assign_hub_height_to_ref_height(self): def copy(self): """Create an independent copy of the current FlorisInterface object""" return FlorisInterface(self.floris.as_dict()) - - def calculate_wake( + + def set( self, + wind_speeds: list[float] | NDArrayFloat | None = None, + wind_directions: list[float] | NDArrayFloat | None = None, + wind_shear: float | None = None, + wind_veer: float | None = None, + reference_wind_height: float | None = None, + turbulence_intensities: list[float] | NDArrayFloat | None = None, + # turbulence_kinetic_energy=None, + air_density: float | None = None, + # wake: WakeModelManager = None, + layout_x: list[float] | NDArrayFloat | None = None, + layout_y: list[float] | NDArrayFloat | None = None, + turbine_type: list | None = None, + turbine_library_path: str | Path | None = None, + solver_settings: dict | None = None, + heterogenous_inflow_config=None, + wind_data: type[WindDataBase] | None = None, yaw_angles: NDArrayFloat | list[float] | None = None, - # tilt_angles: NDArrayFloat | list[float] | None = None, power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, disable_turbines: NDArrayBool | list[bool] | None = None, - ) -> None: + ): + # Reinitialize the floris object after saving the setpoints + save_yaw_angles = self.floris.farm.yaw_angles + save_power_setpoints = self.floris.farm.power_setpoints + self._reinitialize( + wind_speeds=wind_speeds, + wind_directions=wind_directions, + wind_shear=wind_shear, + wind_veer=wind_veer, + reference_wind_height=reference_wind_height, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + layout_x=layout_x, + layout_y=layout_y, + turbine_type=turbine_type, + turbine_library_path=turbine_library_path, + solver_settings=solver_settings, + heterogenous_inflow_config=heterogenous_inflow_config, + wind_data=wind_data, + ) + if not (save_yaw_angles == 0).all(): + self.floris.farm.yaw_angles = save_yaw_angles + if not (save_power_setpoints == POWER_SETPOINT_DEFAULT).all(): + self.floris.farm.power_setpoints = save_power_setpoints + + # Set the operation + self._set_operation( + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + disable_turbines=disable_turbines, + ) + + def run(self) -> None: """ Wrapper to the :py:meth:`~.Farm.set_yaw_angles` and :py:meth:`~.FlowField.calculate_wake` methods. @@ -130,68 +177,6 @@ def calculate_wake( and the power setpoint at that position is set to 0. Defaults to None """ - if yaw_angles is None: - yaw_angles = np.zeros( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, - ) - ) - self.floris.farm.yaw_angles = yaw_angles - - if power_setpoints is None: - power_setpoints = POWER_SETPOINT_DEFAULT * np.ones( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, - ) - ) - else: - power_setpoints = np.array(power_setpoints) - - # Convert any None values to the default power setpoint - power_setpoints[ - power_setpoints == np.full(power_setpoints.shape, None) - ] = POWER_SETPOINT_DEFAULT - power_setpoints = floris_array_converter(power_setpoints) - - # Check for turbines to disable - if disable_turbines is not None: - - # Force to numpy array - disable_turbines = np.array(disable_turbines) - - # Must have first dimension = n_findex - if disable_turbines.shape[0] != self.floris.flow_field.n_findex: - raise ValueError( - f"disable_turbines has a size of {disable_turbines.shape[0]} " - f"in the 0th dimension, must be equal to " - f"n_findex={self.floris.flow_field.n_findex}" - ) - - # Must have first dimension = n_turbines - if disable_turbines.shape[1] != self.floris.farm.n_turbines: - raise ValueError( - f"disable_turbines has a size of {disable_turbines.shape[1]} " - f"in the 1th dimension, must be equal to " - f"n_turbines={self.floris.farm.n_turbines}" - ) - - # Set power_setpoints and yaw_angles to 0 in all locations where - # disable_turbines is True - yaw_angles[disable_turbines] = 0.0 - power_setpoints[disable_turbines] = 0.001 # Not zero to avoid numerical problems - - self.floris.farm.power_setpoints = power_setpoints - - # # TODO is this required? - # if tilt_angles is not None: - # self.floris.farm.tilt_angles = tilt_angles - # else: - # self.floris.farm.set_tilt_to_ref_tilt( - # self.floris.flow_field.n_findex, - # ) - # Initialize solution space self.floris.initialize_domain() @@ -276,7 +261,7 @@ def calculate_no_wake( # Finalize values to user-supplied order self.floris.finalize() - def reinitialize( + def _reinitialize( self, wind_speeds: list[float] | NDArrayFloat | None = None, wind_directions: list[float] | NDArrayFloat | None = None, @@ -386,6 +371,54 @@ def reinitialize( # Create a new instance of floris and attach to self self.floris = Floris.from_dict(floris_dict) + def _set_operation( + self, + yaw_angles: NDArrayFloat | list[float] | None = None, + power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, + disable_turbines: NDArrayBool | list[bool] | None = None, + ): + # Add operating conditions to the floris object + if yaw_angles is not None: + self.floris.farm.yaw_angles = yaw_angles + + if power_setpoints is not None: + power_setpoints = np.array(power_setpoints) + + # Convert any None values to the default power setpoint + power_setpoints[ + power_setpoints == np.full(power_setpoints.shape, None) + ] = POWER_SETPOINT_DEFAULT + power_setpoints = floris_array_converter(power_setpoints) + + self.floris.farm.power_setpoints = power_setpoints + + # Check for turbines to disable + if disable_turbines is not None: + + # Force to numpy array + disable_turbines = np.array(disable_turbines) + + # Must have first dimension = n_findex + if disable_turbines.shape[0] != self.floris.flow_field.n_findex: + raise ValueError( + f"disable_turbines has a size of {disable_turbines.shape[0]} " + f"in the 0th dimension, must be equal to " + f"n_findex={self.floris.flow_field.n_findex}" + ) + + # Must have first dimension = n_turbines + if disable_turbines.shape[1] != self.floris.farm.n_turbines: + raise ValueError( + f"disable_turbines has a size of {disable_turbines.shape[1]} " + f"in the 1th dimension, must be equal to " + f"n_turbines={self.floris.farm.n_turbines}" + ) + + # Set power_setpoints and yaw_angles to 0 in all locations where + # disable_turbines is True + self.floris.farm.yaw_angles[disable_turbines] = 0.0 + self.floris.farm.power_setpoints[disable_turbines] = 0.001 # Not zero to avoid numerical problems + def get_plane_of_points( self, normal_vector="z", From 21d720c6cce5e1bf95be1078be607e522e88cef8 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 20 Feb 2024 21:43:02 -0700 Subject: [PATCH 02/33] calculate plane methods updated; 02 example runs. --- examples/02_visualizations.py | 6 ++-- floris/tools/floris_interface.py | 55 +++++++++++++++++++------------- floris/tools/visualization.py | 30 +++++++++++------ 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/examples/02_visualizations.py b/examples/02_visualizations.py index a82f84ee8..496f2d41b 100644 --- a/examples/02_visualizations.py +++ b/examples/02_visualizations.py @@ -101,7 +101,7 @@ # Run the wake calculation to get the turbine-turbine interfactions # on the turbine grids -fi.calculate_wake() +fi.run() # Plot the values at each rotor fig, axes, _ , _ = wakeviz.plot_rotor_values( @@ -125,11 +125,11 @@ "type": "turbine_grid", "turbine_grid_points": 10 } -fi.reinitialize(solver_settings=solver_settings) +fi.set(solver_settings=solver_settings) # Run the wake calculation to get the turbine-turbine interfactions # on the turbine grids -fi.calculate_wake() +fi.run() # Plot the values at each rotor fig, axes, _ , _ = wakeviz.plot_rotor_values( diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 091f4eda7..ae16d3fa9 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -510,6 +510,8 @@ def calculate_horizontal_plane( wd=None, ws=None, yaw_angles=None, + power_septoints=None, + disable_turbines=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` @@ -540,8 +542,6 @@ def calculate_horizontal_plane( # Store the current state for reinitialization floris_dict = self.floris.as_dict() - current_yaw_angles = self.floris.farm.yaw_angles - # Set the solver to a flow field planar grid solver_settings = { "type": "flow_field_planar_grid", @@ -550,11 +550,14 @@ def calculate_horizontal_plane( "flow_field_grid_points": [x_resolution, y_resolution], "flow_field_bounds": [x_bounds, y_bounds], } - self.reinitialize(wind_directions=wd, wind_speeds=ws, solver_settings=solver_settings) - - # TODO this has to be done here as it seems to be lost with reinitialize - if yaw_angles is not None: - self.floris.farm.yaw_angles = yaw_angles + self.set( + wind_directions=wd, + wind_speeds=ws, + solver_settings=solver_settings, + yaw_angles=yaw_angles, + power_setpoints=power_septoints, + disable_turbines=disable_turbines, + ) # Calculate wake self.floris.solve_for_viz() @@ -579,7 +582,7 @@ def calculate_horizontal_plane( self.floris = Floris.from_dict(floris_dict) # Run the simulation again for futher postprocessing (i.e. now we can get farm power) - self.calculate_wake(yaw_angles=current_yaw_angles) + self.run() return horizontal_plane @@ -593,6 +596,8 @@ def calculate_cross_plane( wd=None, ws=None, yaw_angles=None, + power_setpoints=None, + disable_turbines=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` @@ -623,7 +628,6 @@ def calculate_cross_plane( # Store the current state for reinitialization floris_dict = self.floris.as_dict() - current_yaw_angles = self.floris.farm.yaw_angles # Set the solver to a flow field planar grid solver_settings = { @@ -633,11 +637,14 @@ def calculate_cross_plane( "flow_field_grid_points": [y_resolution, z_resolution], "flow_field_bounds": [y_bounds, z_bounds], } - self.reinitialize(wind_directions=wd, wind_speeds=ws, solver_settings=solver_settings) - - # TODO this has to be done here as it seems to be lost with reinitialize - if yaw_angles is not None: - self.floris.farm.yaw_angles = yaw_angles + self.set( + wind_directions=wd, + wind_speeds=ws, + solver_settings=solver_settings, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + disable_turbines=disable_turbines, + ) # Calculate wake self.floris.solve_for_viz() @@ -657,7 +664,7 @@ def calculate_cross_plane( self.floris = Floris.from_dict(floris_dict) # Run the simulation again for futher postprocessing (i.e. now we can get farm power) - self.calculate_wake(yaw_angles=current_yaw_angles) + self.run() return cross_plane @@ -671,6 +678,8 @@ def calculate_y_plane( wd=None, ws=None, yaw_angles=None, + power_setpoints=None, + disable_turbines=None, ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` @@ -701,7 +710,6 @@ def calculate_y_plane( # Store the current state for reinitialization floris_dict = self.floris.as_dict() - current_yaw_angles = self.floris.farm.yaw_angles # Set the solver to a flow field planar grid solver_settings = { @@ -711,11 +719,14 @@ def calculate_y_plane( "flow_field_grid_points": [x_resolution, z_resolution], "flow_field_bounds": [x_bounds, z_bounds], } - self.reinitialize(wind_directions=wd, wind_speeds=ws, solver_settings=solver_settings) - - # TODO this has to be done here as it seems to be lost with reinitialize - if yaw_angles is not None: - self.floris.farm.yaw_angles = yaw_angles + self.set( + wind_directions=wd, + wind_speeds=ws, + solver_settings=solver_settings, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + disable_turbines=disable_turbines, + ) # Calculate wake self.floris.solve_for_viz() @@ -735,7 +746,7 @@ def calculate_y_plane( self.floris = Floris.from_dict(floris_dict) # Run the simulation again for futher postprocessing (i.e. now we can get farm power) - self.calculate_wake(yaw_angles=current_yaw_angles) + self.run() return y_plane diff --git a/floris/tools/visualization.py b/floris/tools/visualization.py index d1237c338..50a12f393 100644 --- a/floris/tools/visualization.py +++ b/floris/tools/visualization.py @@ -16,6 +16,7 @@ from scipy.spatial import ConvexHull from floris.simulation import Floris +from floris.simulation.turbine.operation_models import POWER_SETPOINT_DEFAULT from floris.tools.cut_plane import CutPlane from floris.tools.floris_interface import FlorisInterface from floris.type_dec import ( @@ -590,6 +591,8 @@ def calculate_horizontal_plane_with_turbines( wd=None, ws=None, yaw_angles=None, + power_setpoints=None, + disable_turbines=None, ) -> CutPlane: """ This function creates a :py:class:`~.tools.cut_plane.CutPlane` by @@ -630,15 +633,15 @@ def calculate_horizontal_plane_with_turbines( fi.check_wind_condition_for_viz(wd=wd, ws=ws) # Set the ws and wd - fi.reinitialize(wind_directions=wd, wind_speeds=ws) - - # Re-set yaw angles - if yaw_angles is not None: - fi.floris.farm.yaw_angles = yaw_angles - - # Now place the yaw_angles back into yaw_angles - # to be sure not None + fi.set( + wind_directions=wd, + wind_speeds=ws, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + disable_turbines=disable_turbines + ) yaw_angles = fi.floris.farm.yaw_angles + power_setpoints = fi.floris.farm.power_setpoints # Grab the turbine layout layout_x = copy.deepcopy(fi.layout_x) @@ -649,6 +652,7 @@ def calculate_horizontal_plane_with_turbines( layout_x_test = np.append(layout_x,[0]) layout_y_test = np.append(layout_y,[0]) yaw_angles = np.append(yaw_angles, [[0.0]], axis=1) + power_setpoints = np.append(power_setpoints, [[POWER_SETPOINT_DEFAULT]], axis=1) # Get a grid of points test test if x_bounds is None: @@ -680,8 +684,14 @@ def calculate_horizontal_plane_with_turbines( # Place the test turbine at this location and calculate wake layout_x_test[-1] = x layout_y_test[-1] = y - fi.reinitialize(layout_x = layout_x_test, layout_y = layout_y_test) - fi.calculate_wake(yaw_angles=yaw_angles) + fi.set( + layout_x=layout_x_test, + layout_y=layout_y_test, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + disable_turbines=disable_turbines + ) + fi.run() # Get the velocity of that test turbines central point center_point = int(np.floor(fi.floris.flow_field.u[0,-1].shape[0] / 2.0)) From 44dce4675d384363ad08d1053f2f08d67a6bb8c0 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 20 Feb 2024 21:49:58 -0700 Subject: [PATCH 03/33] Ruff. --- floris/tools/floris_interface.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index ae16d3fa9..c11478c06 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -107,7 +107,7 @@ def assign_hub_height_to_ref_height(self): def copy(self): """Create an independent copy of the current FlorisInterface object""" return FlorisInterface(self.floris.as_dict()) - + def set( self, wind_speeds: list[float] | NDArrayFloat | None = None, @@ -153,7 +153,7 @@ def set( self.floris.farm.yaw_angles = save_yaw_angles if not (save_power_setpoints == POWER_SETPOINT_DEFAULT).all(): self.floris.farm.power_setpoints = save_power_setpoints - + # Set the operation self._set_operation( yaw_angles=yaw_angles, @@ -391,7 +391,7 @@ def _set_operation( power_setpoints = floris_array_converter(power_setpoints) self.floris.farm.power_setpoints = power_setpoints - + # Check for turbines to disable if disable_turbines is not None: @@ -414,10 +414,10 @@ def _set_operation( f"n_turbines={self.floris.farm.n_turbines}" ) - # Set power_setpoints and yaw_angles to 0 in all locations where - # disable_turbines is True + # Set power setpoints to small value (non zero to avoid numerical issues) and + # yaw_angles to 0 in all locations where disable_turbines is True self.floris.farm.yaw_angles[disable_turbines] = 0.0 - self.floris.farm.power_setpoints[disable_turbines] = 0.001 # Not zero to avoid numerical problems + self.floris.farm.power_setpoints[disable_turbines] = 0.001 def get_plane_of_points( self, From 08f04f65ec6da7e1371b232823a49d20bedee952 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 20 Feb 2024 22:30:30 -0700 Subject: [PATCH 04/33] Updating examples that called reinitialize() and calculate_wake() directly. --- examples/03_making_adjustments.py | 4 +-- examples/05_sweep_wind_speeds.py | 7 +++-- examples/06_sweep_wind_conditions.py | 7 +++-- .../09_compare_farm_power_with_neighbor.py | 10 +++---- examples/16_heterogeneous_inflow.py | 6 ++-- examples/16b_heterogeneity_multiple_ws_wd.py | 8 +++--- examples/18_check_turbine.py | 17 +++++------ examples/21_demo_time_series.py | 6 ++-- examples/22_get_wind_speed_at_turbines.py | 4 +-- examples/23_visualize_layout.py | 2 +- examples/24_floating_turbine_models.py | 12 ++++---- ...25_tilt_driven_vertical_wake_deflection.py | 4 +-- ...rical_gauss_velocity_deficit_parameters.py | 16 +++++------ ...7_empirical_gauss_deflection_parameters.py | 28 +++++++++++-------- examples/28_extract_wind_speed_at_points.py | 4 +-- examples/30_multi_dimensional_cp_ct.py | 17 ++++++----- examples/31_multi_dimensional_cp_ct_2Hs.py | 12 ++++---- examples/33_specify_turbine_power_curve.py | 4 +-- examples/35_sweep_ti.py | 4 +-- examples/40_test_derating.py | 20 +++++++------ examples/41_test_disable_turbines.py | 7 +++-- 21 files changed, 107 insertions(+), 92 deletions(-) diff --git a/examples/03_making_adjustments.py b/examples/03_making_adjustments.py index e405aea65..5c71bba2d 100644 --- a/examples/03_making_adjustments.py +++ b/examples/03_making_adjustments.py @@ -45,7 +45,7 @@ # Change the wind shear, reset the wind speed, and plot a vertical slice -fi.reinitialize( wind_shear=0.2, wind_speeds=[8.0] ) +fi.set(wind_shear=0.2, wind_speeds=[8.0]) y_plane = fi.calculate_y_plane(crossstream_dist=0.0) wakeviz.visualize_cut_plane( y_plane, @@ -61,7 +61,7 @@ 5.0 * fi.floris.farm.rotor_diameters[0,0] * np.arange(0, N, 1), 5.0 * fi.floris.farm.rotor_diameters[0,0] * np.arange(0, N, 1), ) -fi.reinitialize(layout_x=X.flatten(), layout_y=Y.flatten(), wind_directions=[270.0]) +fi.set(layout_x=X.flatten(), layout_y=Y.flatten(), wind_directions=[270.0]) horizontal_plane = fi.calculate_horizontal_plane(height=90.0) wakeviz.visualize_cut_plane( horizontal_plane, diff --git a/examples/05_sweep_wind_speeds.py b/examples/05_sweep_wind_speeds.py index b5b93e488..b9ce3c317 100644 --- a/examples/05_sweep_wind_speeds.py +++ b/examples/05_sweep_wind_speeds.py @@ -22,12 +22,12 @@ D = 126. layout_x = np.array([0, D*6]) layout_y = [0, 0] -fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # Sweep wind speeds but keep wind direction fixed ws_array = np.arange(5,25,0.5) wd_array = 270.0 * np.ones_like(ws_array) -fi.reinitialize(wind_directions=wd_array,wind_speeds=ws_array) +fi.set(wind_directions=wd_array,wind_speeds=ws_array) # Define a matrix of yaw angles to be all 0 # Note that yaw angles is now specified as a matrix whose dimensions are @@ -37,9 +37,10 @@ n_findex = num_wd # Could be either num_wd or num_ws num_turbine = len(layout_x) yaw_angles = np.zeros((n_findex, num_turbine)) +fi.set(yaw_angles=yaw_angles) # Calculate -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() # Collect the turbine powers turbine_powers = fi.get_turbine_powers() / 1E3 # In kW diff --git a/examples/06_sweep_wind_conditions.py b/examples/06_sweep_wind_conditions.py index 9b6e28902..9debf07ca 100644 --- a/examples/06_sweep_wind_conditions.py +++ b/examples/06_sweep_wind_conditions.py @@ -27,7 +27,7 @@ D = 126.0 layout_x = np.array([0, D*6, D*12, D*18, D*24]) layout_y = [0, 0, 0, 0, 0] -fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # In this case we want to check a grid of wind speed and direction combinations wind_speeds_to_expand = np.arange(6, 9, 1.0) @@ -46,7 +46,7 @@ wd_array = wind_directions_grid.flatten() # Now reinitialize FLORIS -fi.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) +fi.set(wind_speeds=ws_array, wind_directions=wd_array) # Define a matrix of yaw angles to be all 0 # Note that yaw angles is now specified as a matrix whose dimensions are @@ -56,9 +56,10 @@ n_findex = num_wd # Could be either num_wd or num_ws num_turbine = len(layout_x) yaw_angles = np.zeros((n_findex, num_turbine)) +fi.set(yaw_angles=yaw_angles) # Calculate -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() # Collect the turbine powers turbine_powers = fi.get_turbine_powers() / 1e3 # In kW diff --git a/examples/09_compare_farm_power_with_neighbor.py b/examples/09_compare_farm_power_with_neighbor.py index b20359c83..c326eee71 100644 --- a/examples/09_compare_farm_power_with_neighbor.py +++ b/examples/09_compare_farm_power_with_neighbor.py @@ -24,16 +24,16 @@ D = 126. layout_x = np.array([0, D*6, 0, D*6]) layout_y = [0, 0, D*3, D*3] -fi.reinitialize(layout_x = layout_x, layout_y = layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # Define a simple wind rose with just 1 wind speed wd_array = np.arange(0,360,4.) ws_array = 8.0 * np.ones_like(wd_array) -fi.reinitialize(wind_directions=wd_array, wind_speeds=ws_array) +fi.set(wind_directions=wd_array, wind_speeds=ws_array) # Calculate -fi.calculate_wake() +fi.run() # Collect the farm power farm_power_base = fi.get_farm_power() / 1E3 # In kW @@ -41,14 +41,14 @@ # Add a neighbor to the east layout_x = np.array([0, D*6, 0, D*6, D*12, D*15, D*12, D*15]) layout_y = np.array([0, 0, D*3, D*3, 0, 0, D*3, D*3]) -fi.reinitialize(layout_x = layout_x, layout_y = layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # Define the weights to exclude the neighboring farm from calcuations of power turbine_weights = np.zeros(len(layout_x), dtype=int) turbine_weights[0:4] = 1.0 # Calculate -fi.calculate_wake() +fi.run() # Collect the farm power with the neightbor farm_power_neighbor = fi.get_farm_power(turbine_weights=turbine_weights) / 1E3 # In kW diff --git a/examples/16_heterogeneous_inflow.py b/examples/16_heterogeneous_inflow.py index 2ac09ebf0..cc71b80c4 100644 --- a/examples/16_heterogeneous_inflow.py +++ b/examples/16_heterogeneous_inflow.py @@ -37,7 +37,7 @@ fi_2d = FlorisInterface("inputs/gch_heterogeneous_inflow.yaml") # Set shear to 0.0 to highlight the heterogeneous inflow -fi_2d.reinitialize(wind_shear=0.0) +fi_2d.set(wind_shear=0.0) # Using the FlorisInterface functions for generating plots, run FLORIS # and extract 2D planes of data. @@ -105,10 +105,10 @@ # Note that we initialize FLORIS with a homogenous flow input file, but # then configure the heterogeneous inflow via the reinitialize method. fi_3d = FlorisInterface("inputs/gch.yaml") -fi_3d.reinitialize(heterogenous_inflow_config=heterogenous_inflow_config) +fi_3d.set(heterogenous_inflow_config=heterogenous_inflow_config) # Set shear to 0.0 to highlight the heterogeneous inflow -fi_3d.reinitialize(wind_shear=0.0) +fi_3d.set(wind_shear=0.0) # Using the FlorisInterface functions for generating plots, run FLORIS # and extract 2D planes of data. diff --git a/examples/16b_heterogeneity_multiple_ws_wd.py b/examples/16b_heterogeneity_multiple_ws_wd.py index 46cd553a7..58ae507a9 100644 --- a/examples/16b_heterogeneity_multiple_ws_wd.py +++ b/examples/16b_heterogeneity_multiple_ws_wd.py @@ -24,14 +24,14 @@ fi = FlorisInterface("inputs/gch_heterogeneous_inflow.yaml") # Set shear to 0.0 to highlight the heterogeneous inflow -fi.reinitialize( +fi.set( wind_shear=0.0, wind_speeds=[8.0], wind_directions=[270.], layout_x=[0, 0], layout_y=[-299., 299.], ) -fi.calculate_wake() +fi.run() turbine_powers = fi.get_turbine_powers().flatten() / 1000. # Show the initial results @@ -52,12 +52,12 @@ 'x': x_locs, 'y': y_locs, } -fi.reinitialize( +fi.set( wind_directions=[270.0, 275.0], wind_speeds=[8.0, 8.0], heterogenous_inflow_config=heterogenous_inflow_config ) -fi.calculate_wake() +fi.run() turbine_powers = np.round(fi.get_turbine_powers() / 1000.) print('With wind directions now set to 270 and 275 deg') print(f'T0: {turbine_powers[:, 0].flatten()} kW') diff --git a/examples/18_check_turbine.py b/examples/18_check_turbine.py index 423c67e42..419e73439 100644 --- a/examples/18_check_turbine.py +++ b/examples/18_check_turbine.py @@ -20,10 +20,10 @@ fi = FlorisInterface("inputs/gch.yaml") # Make one turbine sim -fi.reinitialize(layout_x=[0], layout_y=[0]) +fi.set(layout_x=[0], layout_y=[0]) # Apply wind speeds -fi.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) +fi.set(wind_speeds=ws_array, wind_directions=wd_array) # Get a list of available turbine models provided through FLORIS, and remove # multi-dimensional Cp/Ct turbine definitions as they require different handling @@ -40,7 +40,7 @@ for t in turbines: # Set t as the turbine - fi.reinitialize(turbine_type=[t]) + fi.set(turbine_type=[t]) # Since we are changing the turbine type, make a matching change to the reference wind height fi.assign_hub_height_to_ref_height() @@ -68,12 +68,12 @@ # Try a few density for density in [1.15,1.225,1.3]: - fi.reinitialize(air_density=density) + fi.set(air_density=density) # POWER CURVE ax = axarr[0] - fi.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) - fi.calculate_wake() + fi.set(wind_speeds=ws_array, wind_directions=wd_array) + fi.run() turbine_powers = fi.get_turbine_powers().flatten() / 1e3 if density == 1.225: ax.plot(ws_array,turbine_powers,label='Air Density = %.3f' % density, lw=2, color='k') @@ -87,10 +87,11 @@ # Power loss to yaw, try a range of yaw angles ax = axarr[1] - fi.reinitialize(wind_speeds=[wind_speed_to_test_yaw], wind_directions=[270.0]) + fi.set(wind_speeds=[wind_speed_to_test_yaw], wind_directions=[270.0]) yaw_result = [] for yaw in yaw_angles: - fi.calculate_wake(yaw_angles=np.array([[yaw]])) + fi.set(yaw_angles=np.array([[yaw]])) + fi.run() turbine_powers = fi.get_turbine_powers().flatten() / 1e3 yaw_result.append(turbine_powers[0]) if density == 1.225: diff --git a/examples/21_demo_time_series.py b/examples/21_demo_time_series.py index 7dfbf78a2..3c489ff45 100644 --- a/examples/21_demo_time_series.py +++ b/examples/21_demo_time_series.py @@ -14,7 +14,7 @@ fi = FlorisInterface("inputs/gch.yaml") # Convert to a simple two turbine layout -fi.reinitialize(layout_x=[0, 500.], layout_y=[0., 0.]) +fi.set(layout_x=[0, 500.], layout_y=[0., 0.]) # Create a fake time history where wind speed steps in the middle while wind direction # Walks randomly @@ -28,10 +28,10 @@ # Now intiialize FLORIS object to this history using time_series flag -fi.reinitialize(wind_directions=wd, wind_speeds=ws) +fi.set(wind_directions=wd, wind_speeds=ws) # Collect the powers -fi.calculate_wake() +fi.run() turbine_powers = fi.get_turbine_powers() / 1000. # Show the dimensions diff --git a/examples/22_get_wind_speed_at_turbines.py b/examples/22_get_wind_speed_at_turbines.py index 6eea39179..b5dfeb7d4 100644 --- a/examples/22_get_wind_speed_at_turbines.py +++ b/examples/22_get_wind_speed_at_turbines.py @@ -10,10 +10,10 @@ fi = FlorisInterface("inputs/gch.yaml") # Create a 4-turbine layouts -fi.reinitialize(layout_x=[0, 0., 500., 500.], layout_y=[0., 300., 0., 300.]) +fi.set(layout_x=[0, 0., 500., 500.], layout_y=[0., 300., 0., 300.]) # Calculate wake -fi.calculate_wake() +fi.run() # Collect the wind speed at all the turbine points u_points = fi.floris.flow_field.u diff --git a/examples/23_visualize_layout.py b/examples/23_visualize_layout.py index 9628ad7f9..b3cc39538 100644 --- a/examples/23_visualize_layout.py +++ b/examples/23_visualize_layout.py @@ -14,7 +14,7 @@ fi = FlorisInterface("inputs/gch.yaml") # Assign a 6-turbine layout -fi.reinitialize(layout_x=[0, 100, 500, 1000, 1200,500], layout_y=[0, 800, 150, 500, 0,500]) +fi.set(layout_x=[0, 100, 500, 1000, 1200,500], layout_y=[0, 800, 150, 500, 0,500]) # Give turbines specific names turbine_names = ['T01', 'T02','T03','S01','X01', 'X02'] diff --git a/examples/24_floating_turbine_models.py b/examples/24_floating_turbine_models.py index 12f731816..db586608f 100644 --- a/examples/24_floating_turbine_models.py +++ b/examples/24_floating_turbine_models.py @@ -40,13 +40,13 @@ # Calculate across wind speeds ws_array = np.arange(3., 25., 1.) wd_array = 270.0 * np.ones_like(ws_array) -fi_fixed.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) -fi_floating.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) -fi_floating_defined_floating.reinitialize(wind_speeds=ws_array, wind_directions=wd_array) +fi_fixed.set(wind_speeds=ws_array, wind_directions=wd_array) +fi_floating.set(wind_speeds=ws_array, wind_directions=wd_array) +fi_floating_defined_floating.set(wind_speeds=ws_array, wind_directions=wd_array) -fi_fixed.calculate_wake() -fi_floating.calculate_wake() -fi_floating_defined_floating.calculate_wake() +fi_fixed.run() +fi_floating.run() +fi_floating_defined_floating.run() # Grab power power_fixed = fi_fixed.get_turbine_powers().flatten()/1000. diff --git a/examples/25_tilt_driven_vertical_wake_deflection.py b/examples/25_tilt_driven_vertical_wake_deflection.py index 69a05ac91..05575a40f 100644 --- a/examples/25_tilt_driven_vertical_wake_deflection.py +++ b/examples/25_tilt_driven_vertical_wake_deflection.py @@ -49,7 +49,7 @@ for i, (fi, tilt) in enumerate(zip([fi_5, fi_15], [5, 15])): # Farm layout and wind conditions - fi.reinitialize( + fi.set( layout_x=[x * 5.0 * D for x in range(num_in_row)], layout_y=[0.0]*num_in_row, wind_speeds=[8.0], @@ -57,7 +57,7 @@ ) # Flow solve and power computation - fi.calculate_wake() + fi.run() powers[i,:] = fi.get_turbine_powers().flatten() # Compute flow slices diff --git a/examples/26_empirical_gauss_velocity_deficit_parameters.py b/examples/26_empirical_gauss_velocity_deficit_parameters.py index 1b48f8543..2dc5bb43e 100644 --- a/examples/26_empirical_gauss_velocity_deficit_parameters.py +++ b/examples/26_empirical_gauss_velocity_deficit_parameters.py @@ -103,7 +103,7 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): # Load input yaml and define farm layout fi = FlorisInterface("inputs/emgauss.yaml") D = fi.floris.farm.rotor_diameters[0] -fi.reinitialize( +fi.set( layout_x=[x*5.0*D for x in range(num_in_row)], layout_y=[0.0]*num_in_row, wind_speeds=[8.0], @@ -114,7 +114,7 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): fi_dict = fi.floris.as_dict() # Run wake calculation -fi.calculate_wake() +fi.run() # Look at the powers of each turbine turbine_powers = fi.get_turbine_powers().flatten()/1e6 @@ -138,12 +138,12 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): fi_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ ['wake_expansion_rates'] = [0.03, 0.015] fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], wind_directions=[270.0] ) -fi.calculate_wake() +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw @@ -165,12 +165,12 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): ['breakpoints_D'] = [5, 10] fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], wind_directions=[270.0] ) -fi.calculate_wake() +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw @@ -187,12 +187,12 @@ def generate_wake_visualization(fi: FlorisInterface, title=None): fi_dict_mod['wake']['wake_velocity_parameters']['empirical_gauss']\ ['mixing_gain_velocity'] = 3.0 fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], wind_directions=[270.0] ) -fi.calculate_wake() +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw diff --git a/examples/27_empirical_gauss_deflection_parameters.py b/examples/27_empirical_gauss_deflection_parameters.py index 1b0095a23..5a24aaec7 100644 --- a/examples/27_empirical_gauss_deflection_parameters.py +++ b/examples/27_empirical_gauss_deflection_parameters.py @@ -107,18 +107,19 @@ def generate_wake_visualization(fi, title=None): # Load input yaml and define farm layout fi = FlorisInterface("inputs/emgauss.yaml") D = fi.floris.farm.rotor_diameters[0] -fi.reinitialize( +fi.set( layout_x=[x*5.0*D for x in range(num_in_row)], layout_y=[0.0]*num_in_row, wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], + yaw_angles=yaw_angles, ) # Save dictionary to modify later fi_dict = fi.floris.as_dict() # Run wake calculation -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() # Look at the powers of each turbine turbine_powers = fi.get_turbine_powers().flatten()/1e6 @@ -144,12 +145,13 @@ def generate_wake_visualization(fi, title=None): ['horizontal_deflection_gain_D'] = 5.0 fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], + yaw_angles=yaw_angles, ) -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw @@ -167,12 +169,13 @@ def generate_wake_visualization(fi, title=None): ['mixing_gain_deflection'] = 100.0 fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], + yaw_angles=yaw_angles, ) -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw @@ -193,12 +196,13 @@ def generate_wake_visualization(fi, title=None): fi_dict_mod['wake']['wake_deflection_parameters']['empirical_gauss']\ ['yaw_added_mixing_gain'] = 1.0 fi = FlorisInterface(fi_dict_mod) -fi.reinitialize( +fi.set( wind_speeds=[8.0], - wind_directions=[270.0] + wind_directions=[270.0], + yaw_angles=yaw_angles, ) -fi.calculate_wake(yaw_angles=yaw_angles) +fi.run() turbine_powers = fi.get_turbine_powers().flatten()/1e6 x = np.array(range(num_in_row))+width*nw diff --git a/examples/28_extract_wind_speed_at_points.py b/examples/28_extract_wind_speed_at_points.py index 04ef2daa5..6e68b988b 100644 --- a/examples/28_extract_wind_speed_at_points.py +++ b/examples/28_extract_wind_speed_at_points.py @@ -30,7 +30,7 @@ # Set up a two-turbine farm D = 126 -fi.reinitialize(layout_x=[0, 3 * D], layout_y=[0, 3 * D]) +fi.set(layout_x=[0, 3 * D], layout_y=[0, 3 * D]) fig, ax = plt.subplots(1,2) fig.set_size_inches(10,4) @@ -39,7 +39,7 @@ # Set the wind direction to run 360 degrees wd_array = np.arange(0, 360, 1) ws_array = 8.0 * np.ones_like(wd_array) -fi.reinitialize(wind_directions=wd_array, wind_speeds=ws_array) +fi.set(wind_directions=wd_array, wind_speeds=ws_array) # Simulate a met mast in between the turbines if met_mast_option == 0: diff --git a/examples/30_multi_dimensional_cp_ct.py b/examples/30_multi_dimensional_cp_ct.py index d1cd15b54..11aa88367 100644 --- a/examples/30_multi_dimensional_cp_ct.py +++ b/examples/30_multi_dimensional_cp_ct.py @@ -45,17 +45,20 @@ fi = FlorisInterface("inputs/gch_multi_dim_cp_ct.yaml") # Convert to a simple two turbine layout -fi.reinitialize(layout_x=[0., 500.], layout_y=[0., 0.]) +fi.set(layout_x=[0., 500.], layout_y=[0., 0.]) # Single wind speed and wind direction print('\n========================= Single Wind Direction and Wind Speed =========================') # Get the turbine powers assuming 1 wind speed and 1 wind direction -fi.reinitialize(wind_directions=[270.0], wind_speeds=[8.0]) +fi.set(wind_directions=[270.0], wind_speeds=[8.0]) # Set the yaw angles to 0 yaw_angles = np.zeros([1, 2]) # 1 wind direction and wind speed, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +fi.set(yaw_angles=yaw_angles) + +# Calculate +fi.run() # Get the turbine powers turbine_powers = fi.get_turbine_powers() / 1000.0 @@ -69,9 +72,9 @@ wind_speeds = np.array([8.0, 9.0, 10.0]) wind_directions = np.array([270.0, 270.0, 270.0]) -fi.reinitialize(wind_speeds=wind_speeds, wind_directions=wind_directions) yaw_angles = np.zeros([3, 2]) # 3 wind directions/ speeds, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +fi.set(wind_speeds=wind_speeds, wind_directions=wind_directions, yaw_angles=yaw_angles) +fi.run() turbine_powers = fi.get_turbine_powers() / 1000.0 print("The turbine power matrix should be of dimensions 3 findex X 2 Turbines") print(turbine_powers) @@ -83,9 +86,9 @@ wind_speeds = np.tile([8.0, 9.0, 10.0], 3) wind_directions = np.repeat([260.0, 270.0, 280.0], 3) -fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) yaw_angles = np.zeros([9, 2]) # 9 wind directions/ speeds, 2 turbines -fi.calculate_wake(yaw_angles=yaw_angles) +fi.set(wind_directions=wind_directions, wind_speeds=wind_speeds, yaw_angles=yaw_angles) +fi.run() turbine_powers = fi.get_turbine_powers()/1000. print("The turbine power matrix should be of dimensions 9 WD/WS X 2 Turbines") print(turbine_powers) diff --git a/examples/31_multi_dimensional_cp_ct_2Hs.py b/examples/31_multi_dimensional_cp_ct_2Hs.py index 032df5fa9..b61fcb0f0 100644 --- a/examples/31_multi_dimensional_cp_ct_2Hs.py +++ b/examples/31_multi_dimensional_cp_ct_2Hs.py @@ -28,18 +28,18 @@ fi_hs_1 = FlorisInterface(fi_dict_mod) # Set both cases to 3 turbine layout -fi.reinitialize(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) -fi_hs_1.reinitialize(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) +fi.set(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) +fi_hs_1.set(layout_x=[0., 500., 1000.], layout_y=[0., 0., 0.]) # Use a sweep of wind speeds wind_speeds = np.arange(5, 20, 1.0) wind_directions = 270.0 * np.ones_like(wind_speeds) -fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) -fi_hs_1.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) +fi.set(wind_directions=wind_directions, wind_speeds=wind_speeds) +fi_hs_1.set(wind_directions=wind_directions, wind_speeds=wind_speeds) # Calculate wakes with baseline yaw -fi.calculate_wake() -fi_hs_1.calculate_wake() +fi.run() +fi_hs_1.run() # Collect the turbine powers in kW turbine_powers = fi.get_turbine_powers()/1000. diff --git a/examples/33_specify_turbine_power_curve.py b/examples/33_specify_turbine_power_curve.py index cf1c5f5bc..9eaae2e8a 100644 --- a/examples/33_specify_turbine_power_curve.py +++ b/examples/33_specify_turbine_power_curve.py @@ -43,14 +43,14 @@ wind_speeds = np.linspace(1, 15, 100) wind_directions = 270 * np.ones_like(wind_speeds) # Replace the turbine(s) in the FLORIS model with the created one -fi.reinitialize( +fi.set( layout_x=[0], layout_y=[0], wind_directions=wind_directions, wind_speeds=wind_speeds, turbine_type=[turbine_dict] ) -fi.calculate_wake() +fi.run() powers = fi.get_farm_power() diff --git a/examples/35_sweep_ti.py b/examples/35_sweep_ti.py index 471a9cb67..23942150e 100644 --- a/examples/35_sweep_ti.py +++ b/examples/35_sweep_ti.py @@ -30,8 +30,8 @@ # Now set up a FLORIS model and initialize it using the time fi = FlorisInterface("inputs/gch.yaml") -fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0], wind_data=time_series) -fi.calculate_wake() +fi.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0], wind_data=time_series) +fi.run() turbine_power = fi.get_turbine_powers() fig, axarr = plt.subplots(2, 1, sharex=True, figsize=(6, 6)) diff --git a/examples/40_test_derating.py b/examples/40_test_derating.py index 542e7963e..7f7f091f3 100644 --- a/examples/40_test_derating.py +++ b/examples/40_test_derating.py @@ -22,22 +22,24 @@ turbine_type["power_thrust_model"] = "simple-derating" # Convert to a simple two turbine layout with derating turbines -fi.reinitialize(layout_x=[0, 1000.0], layout_y=[0.0, 0.0], turbine_type=[turbine_type]) +fi.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0], turbine_type=[turbine_type]) # Set the wind directions and speeds to be constant over n_findex = N time steps N = 50 -fi.reinitialize(wind_directions=270 * np.ones(N), wind_speeds=10.0 * np.ones(N)) -fi.calculate_wake() +fi.set(wind_directions=270 * np.ones(N), wind_speeds=10.0 * np.ones(N)) +fi.run() turbine_powers_orig = fi.get_turbine_powers() # Add derating power_setpoints = np.tile(np.linspace(1, 6e6, N), 2).reshape(2, N).T -fi.calculate_wake(power_setpoints=power_setpoints) +fi.set(power_setpoints=power_setpoints) +fi.run() turbine_powers_derated = fi.get_turbine_powers() # Compute available power at downstream turbine power_setpoints_2 = np.array([np.linspace(1, 6e6, N), np.full(N, None)]).T -fi.calculate_wake(power_setpoints=power_setpoints_2) +fi.set(power_setpoints=power_setpoints_2) +fi.run() turbine_powers_avail_ds = fi.get_turbine_powers()[:,1] # Plot the results @@ -91,12 +93,14 @@ [2e6, None,], [None, 1e6] ]) -fi.reinitialize( +fi.set( wind_directions=270 * np.ones(len(yaw_angles)), wind_speeds=10.0 * np.ones(len(yaw_angles)), - turbine_type=[turbine_type]*2 + turbine_type=[turbine_type]*2, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, ) -fi.calculate_wake(yaw_angles=yaw_angles, power_setpoints=power_setpoints) +fi.run() turbine_powers = fi.get_turbine_powers() print(turbine_powers) diff --git a/examples/41_test_disable_turbines.py b/examples/41_test_disable_turbines.py index d276d8ce1..da514e224 100644 --- a/examples/41_test_disable_turbines.py +++ b/examples/41_test_disable_turbines.py @@ -24,7 +24,7 @@ ) as t: turbine_type = yaml.safe_load(t) turbine_type["power_thrust_model"] = "mixed" -fi.reinitialize(turbine_type=[turbine_type]) +fi.set(turbine_type=[turbine_type]) # Consider a wind farm of 3 aligned wind turbines layout = np.array([[0.0, 0.0], [500.0, 0.0], [1000.0, 0.0]]) @@ -42,15 +42,16 @@ # ------------------------------------------ # Reinitialize flow field -fi.reinitialize( +fi.set( layout_x=layout[:, 0], layout_y=layout[:, 1], wind_directions=wind_directions, wind_speeds=wind_speeds, + disable_turbines=disable_turbines, ) # # Compute wakes -fi.calculate_wake(disable_turbines=disable_turbines) +fi.run() # Results # ------------------------------------------ From f9a1f35f6d0981dea9db37062adc571446458216 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Wed, 21 Feb 2024 13:53:46 -0700 Subject: [PATCH 05/33] Update AEP methods --- floris/tools/floris_interface.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index c11478c06..ab97120ab 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -916,7 +916,6 @@ def get_farm_AEP( freq, cut_in_wind_speed=0.001, cut_out_wind_speed=None, - yaw_angles=None, turbine_weights=None, no_wake=False, ) -> float: @@ -939,10 +938,6 @@ def get_farm_AEP( wind farm is known to produce 0.0 W of power. If None is specified, will assume that the wind farm does not cut out at high wind speeds. Defaults to None. - yaw_angles (NDArrayFloat | list[float] | None, optional): - The relative turbine yaw angles in degrees. If None is - specified, will assume that the turbine yaw angles are all - zero degrees for all conditions. Defaults to None. turbine_weights (NDArrayFloat | list[float] | None, optional): weighing terms that allow the user to emphasize power at particular turbines and/or completely ignore the power @@ -996,10 +991,7 @@ def get_farm_AEP( if np.any(conditions_to_evaluate): wind_speeds_subset = wind_speeds[conditions_to_evaluate] wind_directions_subset = wind_directions[conditions_to_evaluate] - yaw_angles_subset = None - if yaw_angles is not None: - yaw_angles_subset = yaw_angles[conditions_to_evaluate] - self.reinitialize( + self.set( wind_speeds=wind_speeds_subset, wind_directions=wind_directions_subset, ) @@ -1015,7 +1007,7 @@ def get_farm_AEP( aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) # Reset the FLORIS object to the full wind speed array - self.reinitialize(wind_speeds=wind_speeds, wind_directions=wind_directions) + self.set(wind_speeds=wind_speeds, wind_directions=wind_directions) return aep @@ -1024,7 +1016,6 @@ def get_farm_AEP_with_wind_data( wind_data, cut_in_wind_speed=0.001, cut_out_wind_speed=None, - yaw_angles=None, turbine_weights=None, no_wake=False, ) -> float: @@ -1045,10 +1036,6 @@ def get_farm_AEP_with_wind_data( wind farm is known to produce 0.0 W of power. If None is specified, will assume that the wind farm does not cut out at high wind speeds. Defaults to None. - yaw_angles (NDArrayFloat | list[float] | None, optional): - The relative turbine yaw angles in degrees. If None is - specified, will assume that the turbine yaw angles are all - zero degrees for all conditions. Defaults to None. turbine_weights (NDArrayFloat | list[float] | None, optional): weighing terms that allow the user to emphasize power at particular turbines and/or completely ignore the power @@ -1084,7 +1071,6 @@ def get_farm_AEP_with_wind_data( freq, cut_in_wind_speed=cut_in_wind_speed, cut_out_wind_speed=cut_out_wind_speed, - yaw_angles=yaw_angles, turbine_weights=turbine_weights, no_wake=no_wake, ) From 6b6a589459dd68e4a96ce70f91544a715833b770 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 13:54:24 -0700 Subject: [PATCH 06/33] yaw optimizer updates, and simpler yaw opt examples. --- examples/10_opt_yaw_single_ws.py | 2 +- examples/11_opt_yaw_multiple_ws.py | 2 +- examples/12_optimize_yaw.py | 10 +++++----- examples/14_compare_yaw_optimizers.py | 5 +++-- .../yaw_optimization/yaw_optimization_base.py | 7 ++++--- .../yaw_optimization/yaw_optimizer_scipy.py | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/10_opt_yaw_single_ws.py b/examples/10_opt_yaw_single_ws.py index 15d1c31bc..ac39b5b4e 100644 --- a/examples/10_opt_yaw_single_ws.py +++ b/examples/10_opt_yaw_single_ws.py @@ -23,7 +23,7 @@ wd_array = np.arange(0.0, 360.0, 3.0) ws_array = 8.0 * np.ones_like(wd_array) D = 126.0 # Rotor diameter for the NREL 5 MW -fi.reinitialize( +fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, diff --git a/examples/11_opt_yaw_multiple_ws.py b/examples/11_opt_yaw_multiple_ws.py index a3d38d307..798750e0b 100644 --- a/examples/11_opt_yaw_multiple_ws.py +++ b/examples/11_opt_yaw_multiple_ws.py @@ -35,7 +35,7 @@ # Reinitialize as a 3-turbine farm with range of WDs and WSs D = 126.0 # Rotor diameter for the NREL 5 MW -fi.reinitialize( +fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, diff --git a/examples/12_optimize_yaw.py b/examples/12_optimize_yaw.py index a1d676f23..55f1547c8 100644 --- a/examples/12_optimize_yaw.py +++ b/examples/12_optimize_yaw.py @@ -35,7 +35,7 @@ def load_floris(): 5.0 * fi.floris.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), 5.0 * fi.floris.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), ) - fi.reinitialize(layout_x=X.flatten(), layout_y=Y.flatten()) + fi.set(layout_x=X.flatten(), layout_y=Y.flatten()) return fi @@ -63,10 +63,10 @@ def calculate_aep(fi, df_windrose, column_name="farm_power"): wd_array = np.array(df_windrose["wd"], dtype=float) ws_array = np.array(df_windrose["ws"], dtype=float) yaw_angles = np.array(df_windrose[yaw_cols], dtype=float) - fi.reinitialize(wind_directions=wd_array, wind_speeds=ws_array) + fi.set(wind_directions=wd_array, wind_speeds=ws_array, yaw_angles=yaw_angles) # Calculate FLORIS for every WD and WS combination and get the farm power - fi.calculate_wake(yaw_angles) + fi.run() farm_power_array = fi.get_farm_power() # Now map FLORIS solutions to dataframe @@ -90,7 +90,7 @@ def calculate_aep(fi, df_windrose, column_name="farm_power"): # Load FLORIS fi = load_floris() ws_array = 8.0 * np.ones_like(fi.floris.flow_field.wind_directions) - fi.reinitialize(wind_speeds=ws_array) + fi.set(wind_speeds=ws_array) nturbs = len(fi.layout_x) # First, get baseline AEP, without wake steering @@ -109,7 +109,7 @@ def calculate_aep(fi, df_windrose, column_name="farm_power"): start_time = timerpc() wd_array = np.arange(0.0, 360.0, 5.0) ws_array = 8.0 * np.ones_like(wd_array) - fi.reinitialize( + fi.set( wind_directions=wd_array, wind_speeds=ws_array, ) diff --git a/examples/14_compare_yaw_optimizers.py b/examples/14_compare_yaw_optimizers.py index 16d6d9767..98a3937b2 100644 --- a/examples/14_compare_yaw_optimizers.py +++ b/examples/14_compare_yaw_optimizers.py @@ -38,7 +38,7 @@ D = 126.0 # Rotor diameter for the NREL 5 MW wd_array = np.arange(0.0, 360.0, 3.0) ws_array = 8.0 * np.ones_like(wd_array) -fi.reinitialize( +fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, @@ -92,7 +92,8 @@ # Before plotting results, need to compute values for GEOOPT since it doesn't compute # power within the optimization -fi.calculate_wake(yaw_angles=yaw_angles_opt_geo) +fi.set(yaw_angles=yaw_angles_opt_geo) +fi.run() geo_farm_power = fi.get_farm_power().squeeze() diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index 21643bdc5..5964c2ae1 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -349,12 +349,13 @@ def _calculate_farm_power( # Calculate solutions turbine_power = np.zeros_like(self._minimum_yaw_angle_subset[:, :]) - fi_subset.reinitialize( + fi_subset.set( wind_directions=wd_array, wind_speeds=ws_array, - turbulence_intensities=ti_array + turbulence_intensities=ti_array, + yaw_angles=yaw_angles, ) - fi_subset.calculate_wake(yaw_angles=yaw_angles) + fi_subset.run() turbine_power = fi_subset.get_turbine_powers() # Multiply with turbine weighing terms diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py index 204a58ade..735296b58 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -73,7 +73,7 @@ def optimize(self): ti_array = self.fi_subset.floris.flow_field.turbulence_intensities for i, (wd, ws, ti) in enumerate(zip(wd_array, ws_array, ti_array)): - self.fi_subset.reinitialize( + self.fi_subset.set( wind_directions=[wd], wind_speeds=[ws], turbulence_intensities=[ti] From 1f4e6f145825589bb9890aa27f8f9c9c143df954 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 14:30:12 -0700 Subject: [PATCH 07/33] Examples running with get_farm_AEP. --- examples/07_calc_aep_from_rose.py | 2 +- .../13_optimize_yaw_with_neighboring_farm.py | 21 +++++++++++-------- examples/16b_heterogeneity_multiple_ws_wd.py | 4 ++-- examples/29_floating_vs_fixedbottom_farm.py | 8 +++---- examples/34_wind_data.py | 10 ++++----- floris/tools/floris_interface.py | 4 ++-- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/07_calc_aep_from_rose.py b/examples/07_calc_aep_from_rose.py index ea1d8c9b9..18db25a71 100644 --- a/examples/07_calc_aep_from_rose.py +++ b/examples/07_calc_aep_from_rose.py @@ -47,7 +47,7 @@ # Assume a three-turbine wind farm with 5D spacing. We reinitialize the # floris object and assign the layout, wind speed and wind direction arrays. D = fi.floris.farm.rotor_diameters[0] # Rotor diameter for the NREL 5 MW -fi.reinitialize( +fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wind_directions, diff --git a/examples/13_optimize_yaw_with_neighboring_farm.py b/examples/13_optimize_yaw_with_neighboring_farm.py index bd201717b..e388909c2 100644 --- a/examples/13_optimize_yaw_with_neighboring_farm.py +++ b/examples/13_optimize_yaw_with_neighboring_farm.py @@ -51,7 +51,7 @@ def load_floris(): turbine_weights[0:10] = 1.0 # Now reinitialize FLORIS layout - fi.reinitialize(layout_x = X, layout_y = Y) + fi.set(layout_x = X, layout_y = Y) # And visualize the floris layout fig, ax = plt.subplots() @@ -180,13 +180,13 @@ def yaw_opt_interpolant(wd, ws): # Create a FLORIS object for AEP calculations fi_AEP = fi.copy() - fi_AEP.reinitialize(wind_speeds=ws_windrose, wind_directions=wd_windrose) + fi_AEP.set(wind_speeds=ws_windrose, wind_directions=wd_windrose) # And create a separate FLORIS object for optimization fi_opt = fi.copy() wd_array = np.arange(0.0, 360.0, 3.0) ws_array = 8.0 * np.ones_like(wd_array) - fi_opt.reinitialize( + fi_opt.set( wind_directions=wd_array, wind_speeds=ws_array, ) @@ -222,7 +222,7 @@ def yaw_opt_interpolant(wd, ws): # Optimize yaw angles while ignoring neighboring farm fi_opt_subset = fi_opt.copy() - fi_opt_subset.reinitialize( + fi_opt_subset.set( layout_x = fi.layout_x[turbs_to_opt], layout_y = fi.layout_y[turbs_to_opt] ) @@ -239,15 +239,15 @@ def yaw_opt_interpolant(wd, ws): print(" ") print("===========================================================") print("Calculating annual energy production with wake steering (AEP)...") + fi_AEP.set(yaw_angles=yaw_angles_opt_nonb_AEP) aep_opt_subset_nonb = 1.0e-9 * fi_AEP.get_farm_AEP( freq=freq_windrose, turbine_weights=turbine_weights, - yaw_angles=yaw_angles_opt_nonb_AEP, ) + fi_AEP.set(yaw_angles=yaw_angles_opt_AEP) aep_opt_subset = 1.0e-9 * fi_AEP.get_farm_AEP( freq=freq_windrose, turbine_weights=turbine_weights, - yaw_angles=yaw_angles_opt_AEP, ) uplift_subset_nonb = 100.0 * (aep_opt_subset_nonb - aep_bl_subset) / aep_bl_subset uplift_subset = 100.0 * (aep_opt_subset - aep_bl_subset) / aep_bl_subset @@ -271,15 +271,18 @@ def yaw_opt_interpolant(wd, ws): yaw_angles_opt_nonb[:, turbs_to_opt] = yaw_opt_interpolant_nonb(wd, ws) fi_opt = fi_opt.copy() - fi_opt.calculate_wake(yaw_angles=np.zeros_like(yaw_angles_opt)) + fi_opt.set(yaw_angles=np.zeros_like(yaw_angles_opt)) + fi_opt.run() farm_power_bl_subset = fi_opt.get_farm_power(turbine_weights).flatten() fi_opt = fi_opt.copy() - fi_opt.calculate_wake(yaw_angles=yaw_angles_opt) + fi_opt.set(yaw_angles=yaw_angles_opt) + fi_opt.run() farm_power_opt_subset = fi_opt.get_farm_power(turbine_weights).flatten() fi_opt = fi_opt.copy() - fi_opt.calculate_wake(yaw_angles=yaw_angles_opt_nonb) + fi_opt.set(yaw_angles=yaw_angles_opt_nonb) + fi_opt.run() farm_power_opt_subset_nonb = fi_opt.get_farm_power(turbine_weights).flatten() fig, ax = plt.subplots() diff --git a/examples/16b_heterogeneity_multiple_ws_wd.py b/examples/16b_heterogeneity_multiple_ws_wd.py index 58ae507a9..9fc662314 100644 --- a/examples/16b_heterogeneity_multiple_ws_wd.py +++ b/examples/16b_heterogeneity_multiple_ws_wd.py @@ -69,6 +69,6 @@ # print() # print('~~ Now forcing an error by not matching wd and het_map') -# fi.reinitialize(wind_directions=[270, 275, 280], wind_speeds=3*[8.0]) -# fi.calculate_wake() +# fi.set(wind_directions=[270, 275, 280], wind_speeds=3*[8.0]) +# fi.run() # turbine_powers = np.round(fi.get_turbine_powers() / 1000.) diff --git a/examples/29_floating_vs_fixedbottom_farm.py b/examples/29_floating_vs_fixedbottom_farm.py index a6fc380a1..e141144aa 100644 --- a/examples/29_floating_vs_fixedbottom_farm.py +++ b/examples/29_floating_vs_fixedbottom_farm.py @@ -38,17 +38,17 @@ x = x.flatten() y = y.flatten() for fi in [fi_fixed, fi_floating]: - fi.reinitialize(layout_x=x, layout_y=y) + fi.set(layout_x=x, layout_y=y) # Compute a single wind speed and direction, power and wakes for fi in [fi_fixed, fi_floating]: - fi.reinitialize( + fi.set( layout_x=x, layout_y=y, wind_speeds=[10], wind_directions=[270] ) - fi.calculate_wake() + fi.run() powers_fixed = fi_fixed.get_turbine_powers() powers_floating = fi_floating.get_turbine_powers() @@ -118,7 +118,7 @@ freq = freq / np.sum(freq) for fi in [fi_fixed, fi_floating]: - fi.reinitialize( + fi.set( wind_directions=wd_grid.flatten(), wind_speeds= ws_grid.flatten(), ) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index aba1d0d8c..80d390363 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -48,16 +48,16 @@ # Now set up a FLORIS model and initialize it using the time series and wind rose fi = FlorisInterface("inputs/gch.yaml") -fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) +fi.set(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) fi_time_series = fi.copy() fi_wind_rose = fi.copy() -fi_time_series.reinitialize(wind_data=time_series) -fi_wind_rose.reinitialize(wind_data=wind_rose) +fi_time_series.set(wind_data=time_series) +fi_wind_rose.set(wind_data=wind_rose) -fi_time_series.calculate_wake() -fi_wind_rose.calculate_wake() +fi_time_series.run() +fi_wind_rose.run() time_series_power = fi_time_series.get_farm_power() wind_rose_power = fi_wind_rose.get_farm_power() diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index ab97120ab..7016ba4a5 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -996,9 +996,9 @@ def get_farm_AEP( wind_directions=wind_directions_subset, ) if no_wake: - self.calculate_no_wake(yaw_angles=yaw_angles_subset) + self.calculate_no_wake() else: - self.calculate_wake(yaw_angles=yaw_angles_subset) + self.run() farm_power[conditions_to_evaluate] = self.get_farm_power( turbine_weights=turbine_weights ) From 1b6e158cfd8196886e1ede753de3c3de2c528c42 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 14:56:00 -0700 Subject: [PATCH 08/33] Layout optimizers. --- examples/15_optimize_layout.py | 10 +++++----- .../16c_optimize_layout_with_heterogeneity.py | 17 ++++++++--------- .../layout_optimization_base.py | 2 +- .../layout_optimization_boundary_grid.py | 2 +- .../layout_optimization_pyoptsparse.py | 5 +++-- .../layout_optimization_scipy.py | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/15_optimize_layout.py b/examples/15_optimize_layout.py index ee477ade5..f35a08a35 100644 --- a/examples/15_optimize_layout.py +++ b/examples/15_optimize_layout.py @@ -32,7 +32,7 @@ freq = (np.abs(np.sort(np.random.randn(len(wind_directions))))) freq = freq / freq.sum() -fi.reinitialize(wind_directions=wind_directions, wind_speeds=wind_speeds) +fi.set(wind_directions=wind_directions, wind_speeds=wind_speeds) # The boundaries for the turbines, specified as vertices boundaries = [(0.0, 0.0), (0.0, 1000.0), (1000.0, 1000.0), (1000.0, 0.0), (0.0, 0.0)] @@ -41,7 +41,7 @@ D = 126.0 # rotor diameter for the NREL 5MW layout_x = [0, 0, 6 * D, 6 * D] layout_y = [0, 4 * D, 0, 4 * D] -fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) # Setup the optimization problem layout_opt = LayoutOptimizationScipy(fi, boundaries, freq=freq) @@ -51,10 +51,10 @@ # Get the resulting improvement in AEP print('... calcuating improvement in AEP') -fi.calculate_wake() +fi.run() base_aep = fi.get_farm_AEP(freq=freq) / 1e6 -fi.reinitialize(layout_x=sol[0], layout_y=sol[1]) -fi.calculate_wake() +fi.set(layout_x=sol[0], layout_y=sol[1]) +fi.run() opt_aep = fi.get_farm_AEP(freq=freq) / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep diff --git a/examples/16c_optimize_layout_with_heterogeneity.py b/examples/16c_optimize_layout_with_heterogeneity.py index 1d30bd5e6..a618aaa1d 100644 --- a/examples/16c_optimize_layout_with_heterogeneity.py +++ b/examples/16c_optimize_layout_with_heterogeneity.py @@ -63,7 +63,7 @@ 'y': y_locs, } -fi.reinitialize( +fi.set( layout_x=layout_x, layout_y=layout_y, wind_directions=wind_directions, @@ -87,10 +87,10 @@ # Get the resulting improvement in AEP print('... calcuating improvement in AEP') -fi.calculate_wake() +fi.run() base_aep = fi.get_farm_AEP(freq=freq) / 1e6 -fi.reinitialize(layout_x=sol[0], layout_y=sol[1]) -fi.calculate_wake() +fi.set(layout_x=sol[0], layout_y=sol[1]) +fi.run() opt_aep = fi.get_farm_AEP(freq=freq) / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep @@ -111,7 +111,7 @@ # Rerun the layout optimization with geometric yaw enabled print("\nReoptimizing with geometric yaw enabled.") -fi.reinitialize(layout_x=layout_x, layout_y=layout_y) +fi.set(layout_x=layout_x, layout_y=layout_y) layout_opt = LayoutOptimizationScipy( fi, boundaries, @@ -127,11 +127,10 @@ # Get the resulting improvement in AEP print('... calcuating improvement in AEP') -fi.calculate_wake() +fi.set(yaw_angles=np.zeros_like(layout_opt.yaw_angles)) base_aep = fi.get_farm_AEP(freq=freq) / 1e6 -fi.reinitialize(layout_x=sol[0], layout_y=sol[1]) -fi.calculate_wake() -opt_aep = fi.get_farm_AEP(freq=freq, yaw_angles=layout_opt.yaw_angles) / 1e6 +fi.set(layout_x=sol[0], layout_y=sol[1], yaw_angles=layout_opt.yaw_angles) +opt_aep = fi.get_farm_AEP(freq=freq) / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep # Print and plot the results diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_base.py b/floris/tools/optimization/layout_optimization/layout_optimization_base.py index 47b8f2ccb..2396d1690 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_base.py @@ -60,7 +60,7 @@ def _get_geoyaw_angles(self): # NOTE: requires that child class saves x and y locations # as self.x and self.y and updates them during optimization. if self.enable_geometric_yaw: - self.yaw_opt.fi_subset.reinitialize(layout_x=self.x, layout_y=self.y) + self.yaw_opt.fi_subset.set(layout_x=self.x, layout_y=self.y) df_opt = self.yaw_opt.optimize() self.yaw_angles = np.vstack(df_opt['yaw_angles_opt'])[:, :] else: diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py b/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py index 07386b1d4..a17b3e220 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_boundary_grid.py @@ -612,7 +612,7 @@ def reinitialize_xy(self): self.boundary_spacing, ) - self.fi.reinitialize(layout=(layout_x, layout_y)) + self.fi.set(layout=(layout_x, layout_y)) def plot_layout(self): plt.figure(figsize=(9, 6)) diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py index 75bbf9c84..1705a4041 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py @@ -92,15 +92,16 @@ def _obj_func(self, varDict): self.parse_opt_vars(varDict) # Update turbine map with turbince locations - self.fi.reinitialize(layout_x=self.x, layout_y=self.y) + self.fi.set(layout_x=self.x, layout_y=self.y) # Compute turbine yaw angles using PJ's geometric code (if enabled) yaw_angles = self._get_geoyaw_angles() + self.fi.set(yaw_angles=yaw_angles) # Compute the objective function funcs = {} funcs["obj"] = ( - -1 * self.fi.get_farm_AEP(self.freq, yaw_angles=yaw_angles) / self.initial_AEP + -1 * self.fi.get_farm_AEP(self.freq) / self.initial_AEP ) # Compute constraints, if any are defined for the optimization diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py b/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py index e960576f4..2c66f1b67 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_scipy.py @@ -99,8 +99,8 @@ def _obj_func(self, locs): self._change_coordinates(locs_unnorm) # Compute turbine yaw angles using PJ's geometric code (if enabled) yaw_angles = self._get_geoyaw_angles() - return (-1 * self.fi.get_farm_AEP(self.freq, yaw_angles=yaw_angles) / - self.initial_AEP) + self.fi.set(yaw_angles=yaw_angles) + return -1 * self.fi.get_farm_AEP(self.freq) /self.initial_AEP def _change_coordinates(self, locs): # Parse the layout coordinates @@ -112,7 +112,7 @@ def _change_coordinates(self, locs): self.y = layout_y # Update the turbine map in floris - self.fi.reinitialize(layout_x=layout_x, layout_y=layout_y) + self.fi.set(layout_x=layout_x, layout_y=layout_y) def _generate_constraints(self): tmp1 = { From 66b5dd853f8cdf5d0100f3cb3832953b06821358 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 15:05:06 -0700 Subject: [PATCH 09/33] update reinitialize calls in sample_velocity_deficit_profiles. --- examples/32_plot_velocity_deficit_profiles.py | 4 ++-- floris/tools/floris_interface.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/32_plot_velocity_deficit_profiles.py b/examples/32_plot_velocity_deficit_profiles.py index a556a666c..9f28ce40c 100644 --- a/examples/32_plot_velocity_deficit_profiles.py +++ b/examples/32_plot_velocity_deficit_profiles.py @@ -55,7 +55,7 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): homogeneous_wind_speed = 8.0 fi = FlorisInterface("inputs/gch.yaml") - fi.reinitialize(layout_x=[0.0], layout_y=[0.0]) + fi.set(layout_x=[0.0], layout_y=[0.0]) # ------------------------------ Single-turbine layout ------------------------------ # We first show how to sample and plot velocity deficit profiles on a single-turbine layout. @@ -131,7 +131,7 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # Let (x_t1, y_t1) be the location of the second turbine x_t1 = 2 * D y_t1 = -2 * D - fi.reinitialize(wind_directions=[wind_direction], layout_x=[0.0, x_t1], layout_y=[0.0, y_t1]) + fi.set(wind_directions=[wind_direction], layout_x=[0.0, x_t1], layout_y=[0.0, y_t1]) # Extract profiles at a set of downstream distances from the starting point (x_start, y_start) cross_profiles = fi.sample_velocity_deficit_profiles( diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 7016ba4a5..99197b8dd 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -1193,7 +1193,7 @@ def sample_velocity_deficit_profiles( if reference_height is None: reference_height = self.floris.flow_field.reference_wind_height - self.reinitialize( + self.set( wind_directions=[wind_direction], wind_speeds=[homogeneous_wind_speed], wind_shear=0.0, @@ -1211,7 +1211,7 @@ def sample_velocity_deficit_profiles( reference_height, ) - self.reinitialize( + self.set( wind_directions=wind_directions_copy, wind_speeds=wind_speeds_copy, wind_shear=wind_shear_copy, From 0d3c8b59bf265ad9667c9fee91d411c4d30c4a27 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 15:12:56 -0700 Subject: [PATCH 10/33] Initial working implementation; update to follow. --- examples/12_optimize_yaw_in_parallel.py | 6 +++--- floris/tools/parallel_computing_interface.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/12_optimize_yaw_in_parallel.py b/examples/12_optimize_yaw_in_parallel.py index d46c94e0c..955c32e06 100644 --- a/examples/12_optimize_yaw_in_parallel.py +++ b/examples/12_optimize_yaw_in_parallel.py @@ -23,7 +23,7 @@ def load_floris(): 5.0 * fi.floris.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), 5.0 * fi.floris.farm.rotor_diameters_sorted[0][0] * np.arange(0, N, 1), ) - fi.reinitialize(layout_x=X.flatten(), layout_y=Y.flatten()) + fi.set(layout_x=X.flatten(), layout_y=Y.flatten()) return fi @@ -59,7 +59,7 @@ def load_windrose(): wd_array = wind_directions_grid.flatten() ws_array = wind_speeds_grid.flatten() - fi_aep.reinitialize( + fi_aep.set( wind_directions=wd_array, wind_speeds=ws_array, turbulence_intensities=[0.08], # Assume 8% turbulence intensity @@ -112,7 +112,7 @@ def load_windrose(): wd_array_opt = wind_directions_grid.flatten() ws_array_opt = wind_speeds_grid.flatten() - fi_opt.reinitialize( + fi_opt.set( wind_directions=wd_array_opt, wind_speeds=ws_array_opt, turbulence_intensities=[0.08], # Assume 8% turbulence intensity diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index 407ab7d1c..5c1393fd5 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -30,7 +30,8 @@ def _load_local_floris_object( def _get_turbine_powers_serial(fi_information, yaw_angles=None): fi = _load_local_floris_object(*fi_information) - fi.calculate_wake(yaw_angles=yaw_angles) + fi.set(yaw_angles=yaw_angles) + fi.run() return (fi.get_turbine_powers(), fi.floris.flow_field) @@ -150,7 +151,7 @@ def copy(self): self_copy.fi = self.fi.copy() return self_copy - def reinitialize( + def set( self, wind_speeds=None, wind_directions=None, @@ -165,7 +166,7 @@ def reinitialize( turbine_type=None, solver_settings=None, ): - """Pass to the FlorisInterface reinitialize function. To allow users + """Pass to the FlorisInterface set function. To allow users to directly replace a FlorisInterface object with this UncertaintyInterface object, this function is required.""" @@ -178,7 +179,7 @@ def reinitialize( # Just passes arguments to the floris object fi = self.fi.copy() - fi.reinitialize( + fi.set( wind_speeds=wind_speeds, wind_directions=wind_directions, wind_shear=wind_shear, @@ -279,7 +280,7 @@ def _postprocessing(self, output): return turbine_powers - def calculate_wake(self): + def calculate_wake(self): # TODO: Remove or update this function? # raise UserWarning("'calculate_wake' not supported. Please use # 'get_turbine_powers' or 'get_farm_power' directly.") return None # Do nothing @@ -454,7 +455,7 @@ def get_farm_AEP( yaw_angles_subset = None if yaw_angles is not None: yaw_angles_subset = yaw_angles[:, conditions_to_evaluate] - self.fi.reinitialize( + self.fi.set( wind_directions=wind_direction_subset, wind_speeds=wind_speeds_subset, turbulence_intensities=turbulence_intensities_subset, @@ -467,7 +468,7 @@ def get_farm_AEP( aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) # Reset the FLORIS object to the full wind speed array - self.fi.reinitialize( + self.fi.set( wind_directions=wind_directions, wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities_subset, From 053ead0a564f2e0afd684ebc505a8b08db3c30d7 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 15:27:45 -0700 Subject: [PATCH 11/33] minimize unnecessary set() calls. --- .../layout_optimization/layout_optimization_pyoptsparse.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py index 1705a4041..555ab21cb 100644 --- a/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py +++ b/floris/tools/optimization/layout_optimization/layout_optimization_pyoptsparse.py @@ -91,12 +91,10 @@ def _obj_func(self, varDict): # Parse the variable dictionary self.parse_opt_vars(varDict) - # Update turbine map with turbince locations - self.fi.set(layout_x=self.x, layout_y=self.y) - # Compute turbine yaw angles using PJ's geometric code (if enabled) yaw_angles = self._get_geoyaw_angles() - self.fi.set(yaw_angles=yaw_angles) + # Update turbine map with turbine locations and yaw angles + self.fi.set(layout_x=self.x, layout_y=self.y, yaw_angles=yaw_angles) # Compute the objective function funcs = {} From a00bff87b13948d060508a790b6fd74e6b7e8e0a Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 16:33:47 -0700 Subject: [PATCH 12/33] tests for FlorisInterfacee updated; added reset_operation() method to FlorisInterface. --- floris/simulation/flow_field.py | 4 +- floris/simulation/turbine/operation_models.py | 1 + floris/tools/floris_interface.py | 80 +++---------- tests/floris_interface_test.py | 107 +++++++++++------- .../floris_interface_regression_test.py | 6 +- 5 files changed, 83 insertions(+), 115 deletions(-) diff --git a/floris/simulation/flow_field.py b/floris/simulation/flow_field.py index 7de465da5..364462119 100644 --- a/floris/simulation/flow_field.py +++ b/floris/simulation/flow_field.py @@ -61,7 +61,7 @@ def turbulence_intensities_validator( # Check that the array is 1-dimensional if value.ndim != 1: raise ValueError( - "wind_directions must have 1-dimension" + "turbulence_intensities must have 1-dimension" ) # Check the turbulence intensity is either length 1 or n_findex @@ -90,7 +90,7 @@ def wind_speeds_validator(self, instance: attrs.Attribute, value: NDArrayFloat) "wind_speeds must have 1-dimension" ) - """Confirm wind speeds and wind directions have the same lenght""" + """Confirm wind speeds and wind directions have the same length""" if len(self.wind_directions) != len(self.wind_speeds): raise ValueError( f"wind_directions (length = {len(self.wind_directions)}) and " diff --git a/floris/simulation/turbine/operation_models.py b/floris/simulation/turbine/operation_models.py index 63c4bc38f..2ef28bd4f 100644 --- a/floris/simulation/turbine/operation_models.py +++ b/floris/simulation/turbine/operation_models.py @@ -28,6 +28,7 @@ POWER_SETPOINT_DEFAULT = 1e12 +POWER_SETPOINT_DISABLED = 0.001 def rotor_velocity_air_density_correction( velocities: NDArrayFloat, diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 99197b8dd..a90caa748 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -10,7 +10,10 @@ from floris.logging_manager import LoggingManager from floris.simulation import Floris, State from floris.simulation.rotor_velocity import average_velocity -from floris.simulation.turbine.operation_models import POWER_SETPOINT_DEFAULT +from floris.simulation.turbine.operation_models import ( + POWER_SETPOINT_DEFAULT, + POWER_SETPOINT_DISABLED +) from floris.simulation.turbine.turbine import ( axial_induction, power, @@ -151,7 +154,10 @@ def set( ) if not (save_yaw_angles == 0).all(): self.floris.farm.yaw_angles = save_yaw_angles - if not (save_power_setpoints == POWER_SETPOINT_DEFAULT).all(): + if not ( + (save_power_setpoints == POWER_SETPOINT_DEFAULT) + | (save_power_setpoints == POWER_SETPOINT_DISABLED) + ).all(): self.floris.farm.power_setpoints = save_power_setpoints # Set the operation @@ -161,6 +167,9 @@ def set( disable_turbines=disable_turbines, ) + def reset_operation(self): + self._reinitialize() + def run(self) -> None: """ Wrapper to the :py:meth:`~.Farm.set_yaw_angles` and @@ -183,11 +192,8 @@ def run(self) -> None: # Perform the wake calculations self.floris.steady_state_atmospheric_condition() - def calculate_no_wake( + def run_no_wake( self, - yaw_angles: NDArrayFloat | list[float] | None = None, - power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, - disable_turbines: NDArrayBool | list[bool] | None = None, ) -> None: """ This function is similar to `calculate_wake()` except @@ -195,66 +201,8 @@ def calculate_no_wake( farm is modeled as if there is no wake in the flow. Yaw angles are used to reduce the power and thrust of the turbine that is yawed. - - Args: - yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. - Defaults to None. """ - if yaw_angles is None: - yaw_angles = np.zeros( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, - ) - ) - self.floris.farm.yaw_angles = yaw_angles - - if power_setpoints is None: - power_setpoints = POWER_SETPOINT_DEFAULT * np.ones( - ( - self.floris.flow_field.n_findex, - self.floris.farm.n_turbines, - ) - ) - else: - power_setpoints = np.array(power_setpoints) - - # Convert any None values to the default power setpoint - power_setpoints[ - power_setpoints == np.full(power_setpoints.shape, None) - ] = POWER_SETPOINT_DEFAULT - power_setpoints = floris_array_converter(power_setpoints) - - # Check for turbines to disable - if disable_turbines is not None: - - # Force to numpy array - # disable_turbines = np.array(disable_turbines) - - # Must have first dimension = n_findex - if disable_turbines.shape[0] != self.floris.flow_field.n_findex: - raise ValueError( - f"disable_turbines has a size of {disable_turbines.shape[0]} " - f"in the 0th dimension, must be equal to " - f"n_findex={self.floris.flow_field.n_findex}" - ) - - # Must have first dimension = n_turbines - if disable_turbines.shape[1] != self.floris.farm.n_turbines: - raise ValueError( - f"disable_turbines has a size of {disable_turbines.shape[1]} " - f"in the 1th dimension, must be equal to " - f"n_turbines={self.floris.farm.n_turbines}" - ) - - # Set power_setpoints and yaw_angles to 0 in all locations where - # disable_turbines is True - yaw_angles[disable_turbines] = 0.0 - power_setpoints[disable_turbines] = 0.001 # Not zero to avoid numerical problems - - self.floris.farm.power_setpoints = power_setpoints - # Initialize solution space self.floris.initialize_domain() @@ -417,7 +365,7 @@ def _set_operation( # Set power setpoints to small value (non zero to avoid numerical issues) and # yaw_angles to 0 in all locations where disable_turbines is True self.floris.farm.yaw_angles[disable_turbines] = 0.0 - self.floris.farm.power_setpoints[disable_turbines] = 0.001 + self.floris.farm.power_setpoints[disable_turbines] = POWER_SETPOINT_DISABLED def get_plane_of_points( self, @@ -774,7 +722,7 @@ def get_turbine_powers(self) -> NDArrayFloat: if self.floris.state is not State.USED: raise RuntimeError( "Can't run function `FlorisInterface.get_turbine_powers` without " - "first running `FlorisInterface.calculate_wake`." + "first running `FlorisInterface.run`." ) # Check for negative velocities, which could indicate bad model # parameters or turbines very closely spaced. diff --git a/tests/floris_interface_test.py b/tests/floris_interface_test.py index 694322c7f..a1ce64fa4 100644 --- a/tests/floris_interface_test.py +++ b/tests/floris_interface_test.py @@ -16,51 +16,58 @@ def test_read_yaml(): fi = FlorisInterface(configuration=YAML_INPUT) assert isinstance(fi, FlorisInterface) -def test_calculate_wake(): +def test_run(): """ In FLORIS v3.2, running calculate_wake twice incorrectly set the yaw angles when the first time has non-zero yaw settings but the second run had all-zero yaw settings. The test below asserts - that the yaw angles are correctly set in subsequent calls to calculate_wake. + that the yaw angles are correctly set in subsequent calls to run. """ fi = FlorisInterface(configuration=YAML_INPUT) yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.calculate_wake(yaw_angles=yaw_angles) + fi.set(yaw_angles=yaw_angles) + fi.run() assert fi.floris.farm.yaw_angles == yaw_angles yaw_angles = np.zeros((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.calculate_wake(yaw_angles=yaw_angles) + fi.set(yaw_angles=yaw_angles) + fi.run() assert fi.floris.farm.yaw_angles == yaw_angles power_setpoints = 1e6*np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.calculate_wake(power_setpoints=power_setpoints) + fi.set(power_setpoints=power_setpoints) + fi.run() assert fi.floris.farm.power_setpoints == power_setpoints - fi.calculate_wake(power_setpoints=None) + fi.reset_operation() + fi.run() assert fi.floris.farm.power_setpoints == ( POWER_SETPOINT_DEFAULT * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) ) - fi.reinitialize(layout_x=[0, 0], layout_y=[0, 1000]) + fi.set(layout_x=[0, 0], layout_y=[0, 1000]) power_setpoints = np.array([[1e6, None]]) - fi.calculate_wake(power_setpoints=power_setpoints) + fi.set(power_setpoints=power_setpoints) + fi.run() assert np.allclose( fi.floris.farm.power_setpoints, np.array([[power_setpoints[0, 0], POWER_SETPOINT_DEFAULT]]) ) -def test_calculate_no_wake(): +def test_run_no_wake(): """ In FLORIS v3.2, running calculate_no_wake twice incorrectly set the yaw angles when the first time has non-zero yaw settings but the second run had all-zero yaw settings. The test below - asserts that the yaw angles are correctly set in subsequent calls to calculate_no_wake. + asserts that the yaw angles are correctly set in subsequent calls to run_no_wake. """ fi = FlorisInterface(configuration=YAML_INPUT) yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.calculate_no_wake(yaw_angles=yaw_angles) + fi.set(yaw_angles=yaw_angles) + fi.run() assert fi.floris.farm.yaw_angles == yaw_angles yaw_angles = np.zeros((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.calculate_no_wake(yaw_angles=yaw_angles) + fi.set(yaw_angles=yaw_angles) + fi.run() assert fi.floris.farm.yaw_angles == yaw_angles @@ -78,14 +85,14 @@ def test_get_turbine_powers(): layout_y = np.array([0, 1000]) n_turbines = len(layout_x) - fi.reinitialize( + fi.set( wind_speeds=wind_speeds, wind_directions=wind_directions, layout_x=layout_x, layout_y=layout_y, ) - fi.calculate_wake() + fi.run() turbine_powers = fi.get_turbine_powers() @@ -105,14 +112,14 @@ def test_get_farm_power(): layout_y = np.array([0, 1000]) # n_turbines = len(layout_x) - fi.reinitialize( + fi.set( wind_speeds=wind_speeds, wind_directions=wind_directions, layout_x=layout_x, layout_y=layout_y, ) - fi.calculate_wake() + fi.run() turbine_powers = fi.get_turbine_powers() farm_powers = fi.get_farm_power() @@ -155,10 +162,10 @@ def test_disable_turbines(): ) as t: turbine_type = yaml.safe_load(t) turbine_type["power_thrust_model"] = "mixed" - fi.reinitialize(turbine_type=[turbine_type]) + fi.set(turbine_type=[turbine_type]) # Init to n-findex = 2, n_turbines = 3 - fi.reinitialize( + fi.set( wind_speeds=np.array([8.,8.,]), wind_directions=np.array([270.,270.]), layout_x = [0,1000,2000], @@ -167,33 +174,39 @@ def test_disable_turbines(): # Confirm that passing in a disable value with wrong n_findex raises error with pytest.raises(ValueError): - fi.calculate_wake(disable_turbines=np.zeros((10, 3), dtype=bool)) + fi.set(disable_turbines=np.zeros((10, 3), dtype=bool)) + fi.run() # Confirm that passing in a disable value with wrong n_turbines raises error with pytest.raises(ValueError): - fi.calculate_wake(disable_turbines=np.zeros((2, 10), dtype=bool)) + fi.set(disable_turbines=np.zeros((2, 10), dtype=bool)) + fi.run() # Confirm that if all turbines are disabled, power is near 0 for all turbines - fi.calculate_wake(disable_turbines=np.ones((2, 3), dtype=bool)) + fi.set(disable_turbines=np.ones((2, 3), dtype=bool)) + fi.run() turbines_powers = fi.get_turbine_powers() np.testing.assert_allclose(turbines_powers,0,atol=0.1) - # Confirm the same for calculate_no_wake - fi.calculate_no_wake(disable_turbines=np.ones((2, 3), dtype=bool)) + # Confirm the same for run_no_wake + fi.run_no_wake() turbines_powers = fi.get_turbine_powers() np.testing.assert_allclose(turbines_powers,0,atol=0.1) # Confirm that if all disabled values set to false, equivalent to running normally - fi.calculate_wake() + fi.reset_operation() + fi.run() turbines_powers_normal = fi.get_turbine_powers() - fi.calculate_wake(disable_turbines=np.zeros((2, 3), dtype=bool)) + fi.set(disable_turbines=np.zeros((2, 3), dtype=bool)) + fi.run() turbines_powers_false_disable = fi.get_turbine_powers() np.testing.assert_allclose(turbines_powers_normal,turbines_powers_false_disable,atol=0.1) - # Confirm the same for calculate_no_wake - fi.calculate_no_wake() + # Confirm the same for run_no_wake + fi.run_no_wake() turbines_powers_normal = fi.get_turbine_powers() - fi.calculate_no_wake(disable_turbines=np.zeros((2, 3), dtype=bool)) + fi.set(disable_turbines=np.zeros((2, 3), dtype=bool)) + fi.run_no_wake() turbines_powers_false_disable = fi.get_turbine_powers() np.testing.assert_allclose(turbines_powers_normal,turbines_powers_false_disable,atol=0.1) @@ -201,19 +214,25 @@ def test_disable_turbines(): # In terms of impact on third turbine disable_turbines = np.zeros((2, 3), dtype=bool) disable_turbines[:,1] = [True, True] - fi.calculate_wake(disable_turbines=disable_turbines) + fi.set(disable_turbines=disable_turbines) + fi.run() power_with_middle_disabled = fi.get_turbine_powers() - fi.reinitialize(layout_x = [0,2000],layout_y = [0, 0]) - fi.calculate_wake() + fi.set(layout_x = [0,2000],layout_y = [0, 0], disable_turbines=np.zeros((2, 2), dtype=bool)) + fi.run() power_with_middle_removed = fi.get_turbine_powers() np.testing.assert_almost_equal(power_with_middle_disabled[0,2], power_with_middle_removed[0,1]) np.testing.assert_almost_equal(power_with_middle_disabled[1,2], power_with_middle_removed[1,1]) # Check that yaw angles are correctly set when turbines are disabled - fi.reinitialize(layout_x = [0,1000,2000],layout_y = [0,0,0]) - fi.calculate_wake(disable_turbines=disable_turbines, yaw_angles=np.ones((2, 3))) + fi.set( + layout_x=[0,1000,2000], + layout_y=[0,0,0], + disable_turbines=disable_turbines, + yaw_angles=np.ones((2, 3)) + ) + fi.run() assert (fi.floris.farm.yaw_angles == np.array([[1.0, 0.0, 1.0], [1.0, 0.0, 1.0]])).all() def test_get_farm_aep(): @@ -227,14 +246,14 @@ def test_get_farm_aep(): layout_y = np.array([0, 1000]) # n_turbines = len(layout_x) - fi.reinitialize( + fi.set( wind_speeds=wind_speeds, wind_directions=wind_directions, layout_x=layout_x, layout_y=layout_y, ) - fi.calculate_wake() + fi.run() farm_powers = fi.get_farm_power() @@ -261,14 +280,14 @@ def test_get_farm_aep_with_conditions(): layout_y = np.array([0, 1000]) # n_turbines = len(layout_x) - fi.reinitialize( + fi.set( wind_speeds=wind_speeds, wind_directions=wind_directions, layout_x=layout_x, layout_y=layout_y, ) - fi.calculate_wake() + fi.run() farm_powers = fi.get_farm_power() @@ -298,7 +317,7 @@ def test_reinitailize_ti(): # Set wind directions and wind speeds and turbulence intensitities # with n_findex = 3 - fi.reinitialize( + fi.set( wind_speeds=[8.0, 8.0, 8.0], wind_directions=[240.0, 250.0, 260.0], turbulence_intensities=[0.1, 0.1, 0.1], @@ -308,7 +327,7 @@ def test_reinitailize_ti(): # turbulence intensity since this is allowed when the turbulence # intensities are uniform # raises n_findex to 4 - fi.reinitialize( + fi.set( wind_speeds=[8.0, 8.0, 8.0, 8.0], wind_directions=[ 240.0, @@ -322,16 +341,16 @@ def test_reinitailize_ti(): np.testing.assert_allclose(fi.floris.flow_field.turbulence_intensities, [0.1, 0.1, 0.1, 0.1]) # Now should be able to change turbulence intensity to changing, so long as length 4 - fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1, 0.11]) + fi.set(turbulence_intensities=[0.08, 0.09, 0.1, 0.11]) # However the wrong length should raise an error with pytest.raises(ValueError): - fi.reinitialize(turbulence_intensities=[0.08, 0.09, 0.1]) + fi.set(turbulence_intensities=[0.08, 0.09, 0.1]) # Also, now that TI is not a single unique value, it can not be left default when changing # shape of wind speeds and directions with pytest.raises(ValueError): - fi.reinitialize( + fi.set( wind_speeds=[8.0, 8.0, 8.0, 8.0, 8.0], wind_directions=[ 240.0, @@ -343,8 +362,8 @@ def test_reinitailize_ti(): ) # Test that applying a 1D array of length 1 is allowed for ti - fi.reinitialize(turbulence_intensities=[0.12]) + fi.set(turbulence_intensities=[0.12]) # Test that applying a float however raises an error with pytest.raises(TypeError): - fi.reinitialize(turbulence_intensities=0.12) + fi.set(turbulence_intensities=0.12) diff --git a/tests/reg_tests/floris_interface_regression_test.py b/tests/reg_tests/floris_interface_regression_test.py index c5833ed8c..078c516d6 100644 --- a/tests/reg_tests/floris_interface_regression_test.py +++ b/tests/reg_tests/floris_interface_regression_test.py @@ -51,9 +51,9 @@ ) -def test_calculate_no_wake(sample_inputs_fixture): +def test_run_no_wake(sample_inputs_fixture): """ - The calculate_no_wake function calculates the power production of a wind farm + The run_no_wake function calculates the power production of a wind farm assuming no wake losses. It does this by initializing and finalizing the floris simulation while skipping the wake calculation. The power for all wind turbines should be the same for a uniform wind condition. The chosen wake model @@ -65,7 +65,7 @@ def test_calculate_no_wake(sample_inputs_fixture): sample_inputs_fixture.floris["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL fi = FlorisInterface(sample_inputs_fixture.floris) - fi.calculate_no_wake() + fi.run_no_wake() n_turbines = fi.floris.farm.n_turbines n_findex = fi.floris.flow_field.n_findex From 7f7b3bad86314a4c1a3b6a1891383f521a1931c1 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 16:54:18 -0700 Subject: [PATCH 13/33] Simple docstrings added. --- floris/tools/floris_interface.py | 98 ++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index a90caa748..e26217c5b 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -133,6 +133,39 @@ def set( power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, disable_turbines: NDArrayBool | list[bool] | None = None, ): + """ + Set the wind conditions and operation setpoints for the wind farm. + + Args: + wind_speeds (NDArrayFloat | list[float] | None, optional): Wind speeds at each findex. + Defaults to None. + wind_directions (NDArrayFloat | list[float] | None, optional): Wind directions at each + findex. Defaults to None. + wind_shear (float | None, optional): Wind shear exponent. Defaults to None. + wind_veer (float | None, optional): Wind veer. Defaults to None. + reference_wind_height (float | None, optional): Reference wind height. Defaults to None. + turbulence_intensities (NDArrayFloat | list[float] | None, optional): Turbulence + intensities at each findex. Defaults to None. + air_density (float | None, optional): Air density. Defaults to None. + layout_x (NDArrayFloat | list[float] | None, optional): X-coordinates of the turbines. + Defaults to None. + layout_y (NDArrayFloat | list[float] | None, optional): Y-coordinates of the turbines. + Defaults to None. + turbine_type (list | None, optional): Turbine type. Defaults to None. + turbine_library_path (str | Path | None, optional): Path to the turbine library. + Defaults to None. + solver_settings (dict | None, optional): Solver settings. Defaults to None. + heterogenous_inflow_config (None, optional): Heterogenous inflow configuration. Defaults + to None. + wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. + yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. + Defaults to None. + power_setpoints (NDArrayFloat | list[float] | list[float, None] | None, optional): + Turbine power setpoints. + disable_turbines (NDArrayBool | list[bool] | None, optional): NDArray with dimensions + n_findex x n_turbines. True values indicate the turbine is disabled at that findex + and the power setpoint at that position is set to 0. Defaults to None. + """ # Reinitialize the floris object after saving the setpoints save_yaw_angles = self.floris.farm.yaw_angles save_power_setpoints = self.floris.farm.power_setpoints @@ -168,22 +201,18 @@ def set( ) def reset_operation(self): + """ + Reinstantiate the floris interface and set all operation setpoints to their default values. + + Args: (None) + """ self._reinitialize() def run(self) -> None: """ - Wrapper to the :py:meth:`~.Farm.set_yaw_angles` and - :py:meth:`~.FlowField.calculate_wake` methods. + Run the FLORIS solve to compute the velocity field and wake effects. - Args: - yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. - Defaults to None. - power_setpoints (NDArrayFloat | list[float] | None, optional): Turbine power setpoints. - May be specified with some float values and some None values; power maximization - will be assumed for any None value. Defaults to None. - disable_turbines (NDArrayBool | list[bool] | None, optional): NDArray with dimensions - n_findex x n_turbines. True values indicate the turbine is disabled at that findex - and the power setpoint at that position is set to 0. Defaults to None + Args: (None) """ # Initialize solution space @@ -196,11 +225,11 @@ def run_no_wake( self, ) -> None: """ - This function is similar to `calculate_wake()` except - that it does not apply a wake model. That is, the wind - farm is modeled as if there is no wake in the flow. - Yaw angles are used to reduce the power and thrust of - the turbine that is yawed. + This function is similar to `run()` except that it does not apply a wake model. That is, + the wind farm is modeled as if there is no wake in the flow. Yaw angles are used to reduce + the power and thrust of the turbine that is yawed. + + Args: (None) """ # Initialize solution space @@ -228,6 +257,32 @@ def _reinitialize( heterogenous_inflow_config=None, wind_data: type[WindDataBase] | None = None, ): + """ + Reinstantiate the floris object with updated conditions set by arguments. + + Args: + wind_speeds (NDArrayFloat | list[float] | None, optional): Wind speeds at each findex. + Defaults to None. + wind_directions (NDArrayFloat | list[float] | None, optional): Wind directions at each + findex. Defaults to None. + wind_shear (float | None, optional): Wind shear exponent. Defaults to None. + wind_veer (float | None, optional): Wind veer. Defaults to None. + reference_wind_height (float | None, optional): Reference wind height. Defaults to None. + turbulence_intensities (NDArrayFloat | list[float] | None, optional): Turbulence + intensities at each findex. Defaults to None. + air_density (float | None, optional): Air density. Defaults to None. + layout_x (NDArrayFloat | list[float] | None, optional): X-coordinates of the turbines. + Defaults to None. + layout_y (NDArrayFloat | list[float] | None, optional): Y-coordinates of the turbines. + Defaults to None. + turbine_type (list | None, optional): Turbine type. Defaults to None. + turbine_library_path (str | Path | None, optional): Path to the turbine library. + Defaults to None. + solver_settings (dict | None, optional): Solver settings. Defaults to None. + heterogenous_inflow_config (None, optional): Heterogenous inflow configuration. Defaults + to None. + wind_data (type[WindDataBase] | None, optional): Wind data. Defaults to None. + """ # Export the floris object recursively as a dictionary floris_dict = self.floris.as_dict() flow_field_dict = floris_dict["flow_field"] @@ -325,6 +380,17 @@ def _set_operation( power_setpoints: NDArrayFloat | list[float] | list[float, None] | None = None, disable_turbines: NDArrayBool | list[bool] | None = None, ): + """ + Apply operating setpoints to the floris object. + + Args: + yaw_angles (NDArrayFloat | list[float] | None, optional): Turbine yaw angles. Defaults + to None. + power_setpoints (NDArrayFloat | list[float] | list[float, None] | None, optional): + Turbine power setpoints. Defaults to None. + disable_turbines (NDArrayBool | list[bool] | None, optional): Boolean array on whether + to disable turbines. Defaults to None. + """ # Add operating conditions to the floris object if yaw_angles is not None: self.floris.farm.yaw_angles = yaw_angles From 0f393e6cce46ba6c424b9f28684c4d06837d394f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 21 Feb 2024 16:55:45 -0700 Subject: [PATCH 14/33] isort. --- floris/tools/floris_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index e26217c5b..3eccc3fc1 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -12,7 +12,7 @@ from floris.simulation.rotor_velocity import average_velocity from floris.simulation.turbine.operation_models import ( POWER_SETPOINT_DEFAULT, - POWER_SETPOINT_DISABLED + POWER_SETPOINT_DISABLED, ) from floris.simulation.turbine.turbine import ( axial_induction, From 32de26c3945558668b196f2aa83427dfbfb4e023 Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Thu, 22 Feb 2024 16:32:25 -0700 Subject: [PATCH 15/33] adding WindTIRose class, which includes wd, ws, and ti as wind rose dimensions --- examples/34_wind_data.py | 14 ++ floris/tools/wind_data.py | 509 +++++++++++++++++++++++++++++++++++++- 2 files changed, 515 insertions(+), 8 deletions(-) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index aba1d0d8c..27bbab86f 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -46,26 +46,40 @@ fig, ax = plt.subplots(subplot_kw={"polar": True}) wind_rose.plot_wind_rose(ax=ax) +# Now build a wind rose with turbulence intensity +wind_ti_rose = time_series.to_wind_ti_rose() + +# Plot the wind rose +fig, ax = plt.subplots(subplot_kw={"polar": True}) +wind_ti_rose.plot_wind_rose(ax=ax) +wind_ti_rose.plot_wind_rose(ax=ax,wind_rose_var="ti") + # Now set up a FLORIS model and initialize it using the time series and wind rose fi = FlorisInterface("inputs/gch.yaml") fi.reinitialize(layout_x=[0, 500.0], layout_y=[0.0, 0.0]) fi_time_series = fi.copy() fi_wind_rose = fi.copy() +fi_wind_ti_rose = fi.copy() fi_time_series.reinitialize(wind_data=time_series) fi_wind_rose.reinitialize(wind_data=wind_rose) +fi_wind_ti_rose.reinitialize(wind_data=wind_ti_rose) fi_time_series.calculate_wake() fi_wind_rose.calculate_wake() +fi_wind_ti_rose.calculate_wake() time_series_power = fi_time_series.get_farm_power() wind_rose_power = fi_wind_rose.get_farm_power() +wind_ti_rose_power = fi_wind_ti_rose.get_farm_power() time_series_aep = fi_time_series.get_farm_AEP_with_wind_data(time_series) wind_rose_aep = fi_wind_rose.get_farm_AEP_with_wind_data(wind_rose) +wind_ti_rose_aep = fi_wind_ti_rose.get_farm_AEP_with_wind_data(wind_ti_rose) print(f"AEP from TimeSeries {time_series_aep / 1e9:.2f} GWh") print(f"AEP from WindRose {wind_rose_aep / 1e9:.2f} GWh") +print(f"AEP from WindTIRose {wind_ti_rose_aep / 1e9:.2f} GWh") plt.show() diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 3d22e8854..6d6d5db7f 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -57,9 +57,9 @@ def unpack_freq(self): class WindRose(WindDataBase): """ - In FLORIS v4, the WindRose class is used to drive FLORIS and optimization - operations in which the inflow is characterized by the frequency of - binned wind speed, wind direction and turbulence intensity values + The WindRose class is used to drive FLORIS and optimization operations in + which the inflow is characterized by the frequency of binned wind speed and + wind direction values. Args: wind_directions: NumPy array of wind directions (NDArrayFloat). @@ -389,17 +389,344 @@ def plot_ti_over_ws( ax.grid(True) +class WindTIRose(WindDataBase): + """ + WindTIRose is similar to the WindRose class, but contains turbulence + intensity as an additional wind rose dimension instead of being defined + as a function of wind direction and wind speed. The class is used to drive + FLORIS and optimization operations in which the inflow is characterized by + the frequency of binned wind speed, wind direction, and turbulence intensity + values. + + Args: + wind_directions: NumPy array of wind directions (NDArrayFloat). + wind_speeds: NumPy array of wind speeds (NDArrayFloat). + turbulence_intensities: NumPy array of turbulence intensities (NDArrayFloat). + freq_table: Frequency table for binned wind direction, wind speed, and + turbulence intensity values (NDArrayFloat, optional). Must have + dimension (n_wind_directions, n_wind_speeds, n_turbulence_intensities). + Defaults to None in which case uniform frequency of all bins is + assumed. + value_table: Value table for binned wind direction, wind + speed, and turbulence intensity values (NDArrayFloat, optional). + Must have dimension (n_wind_directions, n_wind_speeds, + n_turbulence_intensities). Defaults to None in which case uniform + values are assumed. Value can be used to weight power in each bin + to compute the total value of the energy produced. + compute_zero_freq_occurrence: Flag indicating whether to compute zero + frequency occurrences (bool, optional). Defaults to False. + + """ + + def __init__( + self, + wind_directions: NDArrayFloat, + wind_speeds: NDArrayFloat, + turbulence_intensities: NDArrayFloat, + freq_table: NDArrayFloat | None = None, + value_table: NDArrayFloat | None = None, + compute_zero_freq_occurrence: bool = False, + ): + if not isinstance(wind_directions, np.ndarray): + raise TypeError("wind_directions must be a NumPy array") + + if not isinstance(wind_speeds, np.ndarray): + raise TypeError("wind_speeds must be a NumPy array") + + if not isinstance(turbulence_intensities, np.ndarray): + raise TypeError("turbulence_intensities must be a NumPy array") + + # Save the wind speeds and directions + self.wind_directions = wind_directions + self.wind_speeds = wind_speeds + self.turbulence_intensities = turbulence_intensities + + # If freq_table is not None, confirm it has correct dimension, + # otherwise initialize to uniform probability + if freq_table is not None: + if not freq_table.shape[0] == len(wind_directions): + raise ValueError("freq_table first dimension must equal len(wind_directions)") + if not freq_table.shape[1] == len(wind_speeds): + raise ValueError("freq_table second dimension must equal len(wind_speeds)") + if not freq_table.shape[2] == len(turbulence_intensities): + raise ValueError("freq_table third dimension must equal len(turbulence_intensities)") + self.freq_table = freq_table + else: + self.freq_table = np.ones((len(wind_directions), len(wind_speeds), len(turbulence_intensities))) + + # Normalize freq table + self.freq_table = self.freq_table / np.sum(self.freq_table) + + # If value_table is not None, confirm it has correct dimension, + # otherwise initialize to all ones + if value_table is not None: + if not value_table.shape[0] == len(wind_directions): + raise ValueError("value_table first dimension must equal len(wind_directions)") + if not value_table.shape[1] == len(wind_speeds): + raise ValueError("value_table second dimension must equal len(wind_speeds)") + if not value_table.shape[2] == len(turbulence_intensities): + raise ValueError("value_table third dimension must equal len(turbulence_intensities)") + self.value_table = value_table + + # Save whether zero occurrence cases should be computed + self.compute_zero_freq_occurrence = compute_zero_freq_occurrence + + # Build the gridded and flatten versions + self._build_gridded_and_flattened_version() + + def _build_gridded_and_flattened_version(self): + """ + Given the wind direction, wind speed, and turbulence intensity array, + build the gridded versions covering all combinations, and then flatten + versions which put all combinations into 1D array + """ + # Gridded wind speed and direction + self.wd_grid, self.ws_grid, self.ti_grid = np.meshgrid( + self.wind_directions, self.wind_speeds, self.turbulence_intensities, indexing="ij" + ) + + # Flat wind direction, wind speed, and turbulence intensity + self.wd_flat = self.wd_grid.flatten() + self.ws_flat = self.ws_grid.flatten() + self.ti_flat = self.ti_grid.flatten() + + # Flat frequency table + self.freq_table_flat = self.freq_table.flatten() + + # value table + if self.value_table is not None: + self.value_table_flat = self.value_table.flatten() + else: + self.value_table_flat = None + + # Set mask to non-zero frequency cases depending on compute_zero_freq_occurrence + if self.compute_zero_freq_occurrence: + # If computing zero freq occurrences, then this is all True + self.non_zero_freq_mask = [True for i in range(len(self.freq_table_flat))] + else: + self.non_zero_freq_mask = self.freq_table_flat > 0.0 + + # N_findex should only be the calculated cases + self.n_findex = np.sum(self.non_zero_freq_mask) + + def unpack(self): + """ + Unpack the flattened versions of the matrices and return the values + accounting for the non_zero_freq_mask + """ + + # The unpacked versions start as the flat version of each + wind_directions_unpack = self.wd_flat.copy() + wind_speeds_unpack = self.ws_flat.copy() + turbulence_intensities_unpack = self.ti_flat.copy() + freq_table_unpack = self.freq_table_flat.copy() + + # Now mask thes values according to self.non_zero_freq_mask + wind_directions_unpack = wind_directions_unpack[self.non_zero_freq_mask] + wind_speeds_unpack = wind_speeds_unpack[self.non_zero_freq_mask] + turbulence_intensities_unpack = turbulence_intensities_unpack[self.non_zero_freq_mask] + freq_table_unpack = freq_table_unpack[self.non_zero_freq_mask] + + # Now get unpacked value table + if self.value_table_flat is not None: + value_table_unpack = self.value_table_flat[self.non_zero_freq_mask].copy() + else: + value_table_unpack = None + + return ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + turbulence_intensities_unpack, + value_table_unpack, + ) + + def resample_wind_rose(self, wd_step=None, ws_step=None, ti_step=None): + """ + Resamples the wind rose by by wd_step, ws_step, and/or ti_step + + Args: + wd_step: Step size for wind direction resampling (float, optional). + ws_step: Step size for wind speed resampling (float, optional). + ti_step: Step size for turbulence intensity resampling (float, optional). + + Returns: + WindRose: Resampled wind rose based on the provided or default step sizes. + + Notes: + - Returns a resampled version of the wind rose using new `ws_step`, + `wd_step`, and `ti_step`. + - Uses the bin weights feature in TimeSeries to resample the wind rose. + - If `ws_step`, `wd_step`, or `ti_step` are not specified, it uses + the current values. + """ + if ws_step is None: + if len(self.wind_speeds) >= 2: + ws_step = self.wind_speeds[1] - self.wind_speeds[0] + else: # wind rose will have only a single wind speed, and we assume a ws_step of 1 + ws_step = 1.0 + if wd_step is None: + if len(self.wind_directions) >= 2: + wd_step = self.wind_directions[1] - self.wind_directions[0] + else: # wind rose will have only a single wind direction, and we assume a wd_step of 1 + wd_step = 1.0 + if ti_step is None: + if len(self.turbulence_intensities) >= 2: + ti_step = self.turbulence_intensities[1] - self.turbulence_intensities[0] + else: # wind rose will have only a single turbulence intensity, and we assume a ti_step of 1 + ti_step = 1.0 + + # Pass the flat versions of each quantity to build a TimeSeries model + time_series = TimeSeries( + self.wd_flat, self.ws_flat, self.ti_flat, self.value_table_flat + ) + + # Now build a new wind rose using the new steps + return time_series.to_wind_ti_rose( + wd_step=wd_step, ws_step=ws_step, ti_step=ti_step, bin_weights=self.freq_table_flat + ) + + def plot_wind_rose( + self, + ax=None, + wind_rose_var="ws", + color_map="viridis_r", + wd_step=15.0, + wind_rose_var_step=None, + legend_kwargs={}, + ): + """ + This method creates a wind rose plot showing the frequency of occurrence + of either the specified wind direction and wind speed bins or wind + direction and turbulence intensity bins. If no axis is provided, a new + one is created. + + **Note**: Based on code provided by Patrick Murphy from the University + of Colorado Boulder. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + wind_rose_var (str, optional): The variable to display in the wind + rose plot in addition to wind direction. If + wind_rose_var = "ws", wind speed frequencies will be plotted. + If wind_rose_var = "ti", turbulence intensity frequencies will + be plotted. Defaults to "ws". + color_map (str, optional): Colormap to use. Defaults to 'viridis_r'. + wd_step (float, optional): Step size for wind direction. Defaults + to 15 degrees. + wind_rose_var_step (float, optional): Step size for other wind rose + variable. Defaults to None. If unspecified, a value of 5 m/s + will beused if wind_rose_var = "ws", and a value of 4% will be + used if wind_rose_var = "ti". + legend_kwargs (dict, optional): Keyword arguments to be passed to + ax.legend(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + if wind_rose_var not in {"ws", "ti"}: + raise ValueError("wind_rose_var must be either \"ws\" or \"ti\" for wind speed or turbulence intensity, respectively.") + + # Get a resampled wind_rose + if wind_rose_var == "ws": + if wind_rose_var_step is None: + wind_rose_var_step = 5.0 + wind_rose_resample = self.resample_wind_rose(wd_step, ws_step=wind_rose_var_step) + var_bins = wind_rose_resample.wind_speeds + freq_table = wind_rose_resample.freq_table.sum(2) # sum along TI dimension + else: # wind_rose_var == "ti" + if wind_rose_var_step is None: + wind_rose_var_step = 0.04 + wind_rose_resample = self.resample_wind_rose(wd_step, ti_step=wind_rose_var_step) + var_bins = wind_rose_resample.turbulence_intensities + freq_table = wind_rose_resample.freq_table.sum(1) # sum along wind speed dimension + + wd_bins = wind_rose_resample.wind_directions + + # Set up figure + if ax is None: + _, ax = plt.subplots(subplot_kw={"polar": True}) + + # Get a color array + color_array = cm.get_cmap(color_map, len(var_bins)) + + for wd_idx, wd in enumerate(wd_bins): + rects = [] + freq_table_sub = freq_table[wd_idx, :].flatten() + for var_idx, ws in reversed(list(enumerate(var_bins))): + plot_val = freq_table_sub[:var_idx].sum() + rects.append( + ax.bar( + np.radians(wd), + plot_val, + width=0.9 * np.radians(wd_step), + color=color_array(var_idx), + edgecolor="k", + ) + ) + + # Configure the plot + ax.legend(reversed(rects), var_bins, **legend_kwargs) + ax.set_theta_direction(-1) + ax.set_theta_offset(np.pi / 2.0) + ax.set_theta_zero_location("N") + ax.set_xticks(np.arange(0, 2 * np.pi, np.pi / 4)) + ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]) + + return ax + + + def plot_ti_over_ws( + self, + ax=None, + marker=".", + ls="-", + color="k", + ): + """ + Plot the mean turbulence intensity against wind speed. + + Args: + ax (:py:class:`matplotlib.pyplot.axes`, optional): The figure axes + on which the wind rose is plotted. Defaults to None. + plot_kwargs (dict, optional): Keyword arguments to be passed to + ax.plot(). + + Returns: + :py:class:`matplotlib.pyplot.axes`: A figure axes object containing + the plotted wind rose. + """ + + # TODO: Plot std. devs. of TI in addition to mean values + + # Set up figure + if ax is None: + _, ax = plt.subplots() + + # get mean TI for each wind speed by averaging along wind direction and + # TI dimensions + mean_ti_values = (self.ti_grid*self.freq_table).sum((0,2))/self.freq_table.sum((0,2)) + + ax.plot(self.wind_speeds, mean_ti_values*100, marker=marker, ls=ls, color=color) + ax.set_xlabel("Wind Speed (m/s)") + ax.set_ylabel("Mean Turbulence Intensity (%)") + ax.grid(True) + + class TimeSeries(WindDataBase): """ - In FLORIS v4, the TimeSeries class is used to drive FLORIS and optimization - operations in which the inflow is by a sequence of wind direction, wind speed - and turbulence intensity values + The TimeSeries class is used to drive FLORIS and optimization operations in + which the inflow is by a sequence of wind direction, wind speed and + turbulence intensity values Args: wind_directions: NumPy array of wind directions (NDArrayFloat). wind_speeds: NumPy array of wind speeds (NDArrayFloat). - turbulence_intensities: NumPy array of wind speeds (NDArrayFloat, optional). - Defaults to None + turbulence_intensities: NumPy array of turbulence intensities + (NDArrayFloat, optional). Defaults to None values: NumPy array of electricity values (NDArrayFloat, optional). Defaults to None @@ -645,3 +972,169 @@ def to_wind_rose( # Return a WindRose return WindRose(wd_centers, ws_centers, freq_table, ti_table, value_table) + + def to_wind_ti_rose( + self, + wd_step=2.0, + ws_step=1.0, + ti_step=0.02, + wd_edges=None, + ws_edges=None, + ti_edges=None, + bin_weights=None + ): + """ + Converts the TimeSeries data to a WindRose. + + Args: + wd_step (float, optional): Step size for wind direction (default is 2.0). + ws_step (float, optional): Step size for wind speed (default is 1.0). + ti_step (float, optional): Step size for turbulence intensity (default is 0.02). + wd_edges (NDArrayFloat, optional): Custom wind direction edges. Defaults to None. + ws_edges (NDArrayFloat, optional): Custom wind speed edges. Defaults to None. + ti_edges (NDArrayFloat, optional): Custom turbulence intensity + edges. Defaults to None. + bin_weights (NDArrayFloat, optional): Bin weights for resampling. Note these + are primarily used by the resample resample_wind_rose function. + Defaults to None. + + Returns: + WindRose: A WindTIRose object based on the TimeSeries data. + + Notes: + - If `wd_edges` is defined, it uses it to produce the wind direction bin edges. + - If `wd_edges` is not defined, it determines `wd_edges` from the step and data. + - If `ws_edges` is defined, it uses it for wind speed edges. + - If `ws_edges` is not defined, it determines `ws_edges` from the step and data. + - If `ti_edges` is defined, it uses it for turbulence intensity edges. + - If `ti_edges` is not defined, it determines `ti_edges` from the step and data. + """ + + # If turbulence_intensities is None, a WindTIRose object cannot be created. + if self.turbulence_intensities is None: + raise ValueError( + "turbulence_intensities must be defined to export to a WindTIRose object." + ) + + # If wd_edges is defined, then use it to produce the bin centers + if wd_edges is not None: + wd_step = wd_edges[1] - wd_edges[0] + + # use wd_step to produce a wrapped version of wind_directions + wind_directions_wrapped = self._wrap_wind_directions_near_360( + self.wind_directions, wd_step + ) + + # Else, determine wd_edges from the step and data + else: + wd_edges = np.arange(0.0 - wd_step / 2.0, 360.0, wd_step) + + # use wd_step to produce a wrapped version of wind_directions + wind_directions_wrapped = self._wrap_wind_directions_near_360( + self.wind_directions, wd_step + ) + + # Only keep the range with values in it + wd_edges = wd_edges[wd_edges + wd_step > wind_directions_wrapped.min()] + wd_edges = wd_edges[wd_edges - wd_step <= wind_directions_wrapped.max()] + + # Define the centers from the edges + wd_centers = wd_edges[:-1] + wd_step / 2.0 + + # Repeat for wind speeds + if ws_edges is not None: + ws_step = ws_edges[1] - ws_edges[0] + + else: + ws_edges = np.arange(0.0 - ws_step / 2.0, 50.0, ws_step) + + # Only keep the range with values in it + ws_edges = ws_edges[ws_edges + ws_step > self.wind_speeds.min()] + ws_edges = ws_edges[ws_edges - ws_step <= self.wind_speeds.max()] + + # Define the centers from the edges + ws_centers = ws_edges[:-1] + ws_step / 2.0 + + # Repeat for turbulence intensities + if ti_edges is not None: + ti_step = ti_edges[1] - ti_edges[0] + + else: + ti_edges = np.arange(0.0 - ti_step / 2.0, 1.0, ti_step) + + # Only keep the range with values in it + ti_edges = ti_edges[ti_edges + ti_step > self.turbulence_intensities.min()] + ti_edges = ti_edges[ti_edges - ti_step <= self.turbulence_intensities.max()] + + # Define the centers from the edges + ti_centers = ti_edges[:-1] + ti_step / 2.0 + + # Now use pandas to get the tables need for wind rose + df = pd.DataFrame( + { + "wd": wind_directions_wrapped, + "ws": self.wind_speeds, + "ti": self.turbulence_intensities, + "freq_val": np.ones(len(wind_directions_wrapped)), + } + ) + + # If bin_weights are passed in, apply these to the frequency + # this is mostly used when resampling the wind rose + if bin_weights is not None: + df = df.assign(freq_val=df["freq_val"] * bin_weights) + + # If values is not none, add to dataframe + if self.values is not None: + df = df.assign(values=self.values) + + # Bin wind speed, wind direction, and turbulence intensity and then group things up + df = ( + df.assign( + wd_bin=pd.cut( + df.wd, bins=wd_edges, labels=wd_centers, right=False, include_lowest=True + ) + ) + .assign( + ws_bin=pd.cut( + df.ws, bins=ws_edges, labels=ws_centers, right=False, include_lowest=True + ) + ) + .assign( + ti_bin=pd.cut( + df.ti, bins=ti_edges, labels=ti_centers, right=False, include_lowest=True + ) + ) + .drop(["wd", "ws", "ti"], axis=1) + ) + + # Convert wd_bin, ws_bin, and ti_bin to categoricals to ensure all + # combinations are considered and then group + wd_cat = CategoricalDtype(categories=wd_centers, ordered=True) + ws_cat = CategoricalDtype(categories=ws_centers, ordered=True) + ti_cat = CategoricalDtype(categories=ti_centers, ordered=True) + + df = ( + df.assign(wd_bin=df["wd_bin"].astype(wd_cat)) + .assign(ws_bin=df["ws_bin"].astype(ws_cat)) + .assign(ti_bin=df["ti_bin"].astype(ti_cat)) + .groupby(["wd_bin", "ws_bin", "ti_bin"], observed=False) + .agg(["sum", "mean"]) + ) + # Flatten and combine levels using an underscore + df.columns = ["_".join(col) for col in df.columns] + + # Collect the frequency table and reshape + freq_table = df["freq_val_sum"].values.copy() + freq_table = freq_table / freq_table.sum() + freq_table = freq_table.reshape((len(wd_centers), len(ws_centers), len(ti_centers))) + + # If values is not none, compute the table + if self.values is not None: + value_table = df["values_mean"].values.copy() + value_table = value_table.reshape((len(wd_centers), len(ws_centers), len(ti_centers))) + else: + value_table = None + + # Return a WindTIRose + return WindTIRose(wd_centers, ws_centers, ti_centers, freq_table, value_table) From 45f2b62484534b9dc6ea1686c13cd2856e8a011d Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 22 Feb 2024 20:31:42 -0700 Subject: [PATCH 16/33] Fix a missed calculate_no_wake() call. --- floris/tools/floris_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 3eccc3fc1..79ae6ab8a 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -1010,7 +1010,7 @@ def get_farm_AEP( wind_directions=wind_directions_subset, ) if no_wake: - self.calculate_no_wake() + self.run_no_wake() else: self.run() farm_power[conditions_to_evaluate] = self.get_farm_power( From d204ee17d370c5eac4078a63034776561fe57a10 Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Fri, 23 Feb 2024 11:09:50 -0700 Subject: [PATCH 17/33] adding tests for WindTIRose --- floris/tools/__init__.py | 1 + tests/wind_data_test.py | 178 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/floris/tools/__init__.py b/floris/tools/__init__.py index 677c569c0..7c0f99ee8 100644 --- a/floris/tools/__init__.py +++ b/floris/tools/__init__.py @@ -35,6 +35,7 @@ from .wind_data import ( TimeSeries, WindRose, + WindTIRose, ) diff --git a/tests/wind_data_test.py b/tests/wind_data_test.py index c071abd54..3a64e8e91 100644 --- a/tests/wind_data_test.py +++ b/tests/wind_data_test.py @@ -1,10 +1,10 @@ - import numpy as np import pytest from floris.tools import ( TimeSeries, WindRose, + WindTIRose, ) from floris.tools.wind_data import WindDataBase @@ -247,3 +247,179 @@ def test_time_series_to_wind_rose_with_ti(): # The 6 m/s bin should be empty freq_table = wind_rose.freq_table np.testing.assert_almost_equal(freq_table[0, 1], 0) + + +def test_wind_ti_rose_init(): + """ + The wind directions, wind speeds, and turbulence intensities can have any + length, but the frequency array must have shape (n wind directions, + n wind speeds, n turbulence intensities) + """ + wind_directions = np.array([270, 280, 290, 300]) + wind_speeds = np.array([6, 7, 8]) + turbulence_intensities = np.array([0.05, 0.1]) + + # This should be ok + _ = WindTIRose(wind_directions, wind_speeds, turbulence_intensities) + + # This should be ok since the frequency array shape matches the wind directions + # and wind speeds + _ = WindTIRose(wind_directions, wind_speeds, turbulence_intensities, np.ones((4, 3, 2))) + + # This should raise an error since the frequency array shape does not + # match the wind directions and wind speeds + with pytest.raises(ValueError): + WindTIRose(wind_directions, wind_speeds, turbulence_intensities, np.ones((3, 3, 3))) + + +def test_wind_ti_rose_grid(): + wind_directions = np.array([270, 280, 290, 300]) + wind_speeds = np.array([6, 7, 8]) + turbulence_intensities = np.array([0.05, 0.1]) + + wind_rose = WindTIRose(wind_directions, wind_speeds, turbulence_intensities) + + # Wind direction grid has the same dimensions as the frequency table + assert wind_rose.wd_grid.shape == wind_rose.freq_table.shape + + # Flattening process occurs wd first + # This is each wind direction for each wind speed: + np.testing.assert_allclose(wind_rose.wd_flat, 6 * [270] + 6 * [280] + 6 * [290] + 6 * [300]) + + +def test_wind_ti_rose_unpack(): + wind_directions = np.array([270, 280, 290, 300]) + wind_speeds = np.array([6, 7, 8]) + turbulence_intensities = np.array([0.05, 0.1]) + freq_table = np.array( + [ + [[1.0, 0.0], [1.0, 0.0], [0.0, 0.0]], + [[1.0, 0.0], [1.0, 0.0], [0.0, 0.0]], + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + ] + ) + + # First test using default assumption only non-zero frequency cases computed + wind_rose = WindTIRose(wind_directions, wind_speeds, turbulence_intensities, freq_table) + + ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + turbulence_intensities_unpack, + value_table_unpack, + ) = wind_rose.unpack() + + # Given the above frequency table with zeros for a few elements, + # we expect only combinations of wind directions of 270 and 280 deg, + # wind speeds of 6 and 7 m/s, and a TI of 5% + np.testing.assert_allclose(wind_directions_unpack, [270, 270, 280, 280]) + np.testing.assert_allclose(wind_speeds_unpack, [6, 7, 6, 7]) + np.testing.assert_allclose(turbulence_intensities_unpack, [0.05, 0.05, 0.05, 0.05]) + np.testing.assert_allclose(freq_table_unpack, [0.25, 0.25, 0.25, 0.25]) + + # In this case n_findex is the length of the wind combinations that are + # non-zero frequency + assert wind_rose.n_findex == 4 + + # Now test computing 0-freq cases too + wind_rose = WindTIRose( + wind_directions, + wind_speeds, + turbulence_intensities, + freq_table, + compute_zero_freq_occurrence=True, + ) + + ( + wind_directions_unpack, + wind_speeds_unpack, + freq_table_unpack, + turbulence_intensities_unpack, + value_table_unpack, + ) = wind_rose.unpack() + + # Expect now to compute all combinations + np.testing.assert_allclose( + wind_directions_unpack, 6 * [270] + 6 * [280] + 6 * [290] + 6 * [300] + ) + + # In this case n_findex is the total number of wind combinations + assert wind_rose.n_findex == 24 + + +def test_wind_ti_rose_unpack_for_reinitialize(): + wind_directions = np.array([270, 280, 290, 300]) + wind_speeds = np.array([6, 7, 8]) + turbulence_intensities = np.array([0.05, 0.1]) + freq_table = np.array( + [ + [[1.0, 0.0], [1.0, 0.0], [0.0, 0.0]], + [[1.0, 0.0], [1.0, 0.0], [0.0, 0.0]], + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], + ] + ) + + # First test using default assumption only non-zero frequency cases computed + wind_rose = WindTIRose(wind_directions, wind_speeds, turbulence_intensities, freq_table) + + ( + wind_directions_unpack, + wind_speeds_unpack, + turbulence_intensities_unpack, + ) = wind_rose.unpack_for_reinitialize() + + # Given the above frequency table with zeros for a few elements, + # we expect only combinations of wind directions of 270 and 280 deg, + # wind speeds of 6 and 7 m/s, and a TI of 5% + np.testing.assert_allclose(wind_directions_unpack, [270, 270, 280, 280]) + np.testing.assert_allclose(wind_speeds_unpack, [6, 7, 6, 7]) + np.testing.assert_allclose(turbulence_intensities_unpack, [0.05, 0.05, 0.05, 0.05]) + + +def test_wind_ti_rose_resample(): + wind_directions = np.array([0, 2, 4, 6, 8, 10]) + wind_speeds = np.array([7, 8]) + turbulence_intensities = np.array([0.02, 0.04, 0.06, 0.08, 0.1]) + freq_table = np.ones((6, 2, 5)) + + wind_rose = WindTIRose(wind_directions, wind_speeds, turbulence_intensities, freq_table) + + # Test that resampling with a new step size returns the same + wind_rose_resample = wind_rose.resample_wind_rose() + + np.testing.assert_allclose(wind_rose.wind_directions, wind_rose_resample.wind_directions) + np.testing.assert_allclose(wind_rose.wind_speeds, wind_rose_resample.wind_speeds) + np.testing.assert_allclose( + wind_rose.turbulence_intensities, wind_rose_resample.turbulence_intensities + ) + np.testing.assert_allclose(wind_rose.freq_table_flat, wind_rose_resample.freq_table_flat) + + # Now test resampling the turbulence intensities to 4% bins + wind_rose_resample = wind_rose.resample_wind_rose(ti_step=0.04) + np.testing.assert_allclose(wind_rose_resample.turbulence_intensities, [0.04, 0.08, 0.12]) + np.testing.assert_allclose( + wind_rose_resample.freq_table_flat, (1 / 60) * np.array(12 * [2, 2, 1]) + ) + + +def test_time_series_to_wind_ti_rose(): + wind_directions = np.array([259.8, 260.2, 260.3, 260.1]) + wind_speeds = np.array([5.0, 5.0, 5.1, 7.2]) + turbulence_intensities = np.array([0.05, 0.1, 0.15, 0.2]) + time_series = TimeSeries( + wind_directions, + wind_speeds, + turbulence_intensities=turbulence_intensities, + ) + wind_rose = time_series.to_wind_ti_rose(wd_step=2.0, ws_step=1.0, ti_step=0.1) + + # The binning should result in turbulence intensity bins of 0.1 and 0.2 + tis_windrose = wind_rose.turbulence_intensities + np.testing.assert_almost_equal(tis_windrose, [0.1, 0.2]) + + # The 6 m/s bin should be empty + freq_table = wind_rose.freq_table + np.testing.assert_almost_equal(freq_table[0, 1, :], [0, 0]) From ecb5c2a70b9c9cfa100e441170ecd9305402601b Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Fri, 23 Feb 2024 11:14:57 -0700 Subject: [PATCH 18/33] formatting wind data --- examples/34_wind_data.py | 3 +-- floris/tools/wind_data.py | 48 +++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index 27bbab86f..cce4902d7 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -1,4 +1,3 @@ - import matplotlib.pyplot as plt import numpy as np @@ -52,7 +51,7 @@ # Plot the wind rose fig, ax = plt.subplots(subplot_kw={"polar": True}) wind_ti_rose.plot_wind_rose(ax=ax) -wind_ti_rose.plot_wind_rose(ax=ax,wind_rose_var="ti") +wind_ti_rose.plot_wind_rose(ax=ax, wind_rose_var="ti") # Now set up a FLORIS model and initialize it using the time series and wind rose fi = FlorisInterface("inputs/gch.yaml") diff --git a/floris/tools/wind_data.py b/floris/tools/wind_data.py index 6d6d5db7f..09e4e0c93 100644 --- a/floris/tools/wind_data.py +++ b/floris/tools/wind_data.py @@ -1,4 +1,3 @@ - from __future__ import annotations from abc import abstractmethod @@ -383,7 +382,7 @@ def plot_ti_over_ws( if ax is None: _, ax = plt.subplots() - ax.plot(self.ws_flat, self.ti_table_flat*100, marker=marker, ls=ls, color=color) + ax.plot(self.ws_flat, self.ti_table_flat * 100, marker=marker, ls=ls, color=color) ax.set_xlabel("Wind Speed (m/s)") ax.set_ylabel("Turbulence Intensity (%)") ax.grid(True) @@ -432,7 +431,7 @@ def __init__( if not isinstance(wind_speeds, np.ndarray): raise TypeError("wind_speeds must be a NumPy array") - + if not isinstance(turbulence_intensities, np.ndarray): raise TypeError("turbulence_intensities must be a NumPy array") @@ -449,10 +448,14 @@ def __init__( if not freq_table.shape[1] == len(wind_speeds): raise ValueError("freq_table second dimension must equal len(wind_speeds)") if not freq_table.shape[2] == len(turbulence_intensities): - raise ValueError("freq_table third dimension must equal len(turbulence_intensities)") + raise ValueError( + "freq_table third dimension must equal len(turbulence_intensities)" + ) self.freq_table = freq_table else: - self.freq_table = np.ones((len(wind_directions), len(wind_speeds), len(turbulence_intensities))) + self.freq_table = np.ones( + (len(wind_directions), len(wind_speeds), len(turbulence_intensities)) + ) # Normalize freq table self.freq_table = self.freq_table / np.sum(self.freq_table) @@ -465,7 +468,9 @@ def __init__( if not value_table.shape[1] == len(wind_speeds): raise ValueError("value_table second dimension must equal len(wind_speeds)") if not value_table.shape[2] == len(turbulence_intensities): - raise ValueError("value_table third dimension must equal len(turbulence_intensities)") + raise ValueError( + "value_table third dimension must equal len(turbulence_intensities)" + ) self.value_table = value_table # Save whether zero occurrence cases should be computed @@ -573,13 +578,11 @@ def resample_wind_rose(self, wd_step=None, ws_step=None, ti_step=None): if ti_step is None: if len(self.turbulence_intensities) >= 2: ti_step = self.turbulence_intensities[1] - self.turbulence_intensities[0] - else: # wind rose will have only a single turbulence intensity, and we assume a ti_step of 1 + else: # wind rose will have only a single TI, and we assume a ti_step of 1 ti_step = 1.0 # Pass the flat versions of each quantity to build a TimeSeries model - time_series = TimeSeries( - self.wd_flat, self.ws_flat, self.ti_flat, self.value_table_flat - ) + time_series = TimeSeries(self.wd_flat, self.ws_flat, self.ti_flat, self.value_table_flat) # Now build a new wind rose using the new steps return time_series.to_wind_ti_rose( @@ -628,7 +631,9 @@ def plot_wind_rose( """ if wind_rose_var not in {"ws", "ti"}: - raise ValueError("wind_rose_var must be either \"ws\" or \"ti\" for wind speed or turbulence intensity, respectively.") + raise ValueError( + 'wind_rose_var must be either "ws" or "ti" for wind speed or turbulence intensity.' + ) # Get a resampled wind_rose if wind_rose_var == "ws": @@ -636,13 +641,13 @@ def plot_wind_rose( wind_rose_var_step = 5.0 wind_rose_resample = self.resample_wind_rose(wd_step, ws_step=wind_rose_var_step) var_bins = wind_rose_resample.wind_speeds - freq_table = wind_rose_resample.freq_table.sum(2) # sum along TI dimension - else: # wind_rose_var == "ti" + freq_table = wind_rose_resample.freq_table.sum(2) # sum along TI dimension + else: # wind_rose_var == "ti" if wind_rose_var_step is None: wind_rose_var_step = 0.04 wind_rose_resample = self.resample_wind_rose(wd_step, ti_step=wind_rose_var_step) var_bins = wind_rose_resample.turbulence_intensities - freq_table = wind_rose_resample.freq_table.sum(1) # sum along wind speed dimension + freq_table = wind_rose_resample.freq_table.sum(1) # sum along wind speed dimension wd_bins = wind_rose_resample.wind_directions @@ -678,7 +683,6 @@ def plot_wind_rose( return ax - def plot_ti_over_ws( self, ax=None, @@ -708,9 +712,9 @@ def plot_ti_over_ws( # get mean TI for each wind speed by averaging along wind direction and # TI dimensions - mean_ti_values = (self.ti_grid*self.freq_table).sum((0,2))/self.freq_table.sum((0,2)) + mean_ti_values = (self.ti_grid * self.freq_table).sum((0, 2)) / self.freq_table.sum((0, 2)) - ax.plot(self.wind_speeds, mean_ti_values*100, marker=marker, ls=ls, color=color) + ax.plot(self.wind_speeds, mean_ti_values * 100, marker=marker, ls=ls, color=color) ax.set_xlabel("Wind Speed (m/s)") ax.set_ylabel("Mean Turbulence Intensity (%)") ax.grid(True) @@ -972,7 +976,7 @@ def to_wind_rose( # Return a WindRose return WindRose(wd_centers, ws_centers, freq_table, ti_table, value_table) - + def to_wind_ti_rose( self, wd_step=2.0, @@ -981,7 +985,7 @@ def to_wind_ti_rose( wd_edges=None, ws_edges=None, ti_edges=None, - bin_weights=None + bin_weights=None, ): """ Converts the TimeSeries data to a WindRose. @@ -1010,11 +1014,11 @@ def to_wind_ti_rose( - If `ti_edges` is not defined, it determines `ti_edges` from the step and data. """ - # If turbulence_intensities is None, a WindTIRose object cannot be created. + # If turbulence_intensities is None, a WindTIRose object cannot be created. if self.turbulence_intensities is None: raise ValueError( - "turbulence_intensities must be defined to export to a WindTIRose object." - ) + "turbulence_intensities must be defined to export to a WindTIRose object." + ) # If wd_edges is defined, then use it to produce the bin centers if wd_edges is not None: From 5bfb61f656ee0622315cb2b5c5dd76a27b93f0d8 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 11:49:55 -0700 Subject: [PATCH 19/33] Test set / run sequences --- tests/floris_interface_integration_test.py | 118 ++++++++++++++++----- 1 file changed, 90 insertions(+), 28 deletions(-) diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index a1ce64fa4..234cc821b 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -16,12 +16,16 @@ def test_read_yaml(): fi = FlorisInterface(configuration=YAML_INPUT) assert isinstance(fi, FlorisInterface) -def test_run(): +def test_set_run(): """ - In FLORIS v3.2, running calculate_wake twice incorrectly set the yaw angles when the first time - has non-zero yaw settings but the second run had all-zero yaw settings. The test below asserts - that the yaw angles are correctly set in subsequent calls to run. + These tests are designed to test the set / run sequence to ensure that inputs are + set when they should be, not set when they shouldn't be, and that the run sequence + retains or resets information as intended. """ + + # In FLORIS v3.2, running calculate_wake twice incorrectly set the yaw angles when the first time + # has non-zero yaw settings but the second run had all-zero yaw settings. The test below asserts + # that the yaw angles are correctly set in subsequent calls to run. fi = FlorisInterface(configuration=YAML_INPUT) yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) fi.set(yaw_angles=yaw_angles) @@ -33,43 +37,106 @@ def test_run(): fi.run() assert fi.floris.farm.yaw_angles == yaw_angles - power_setpoints = 1e6*np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - fi.set(power_setpoints=power_setpoints) + # Verify making changes to the layout, wind speed, and wind direction both before and after + # running the calculation + fi.reset_operation() + fi.set(layout_x=[0, 0], layout_y=[0, 1000], wind_speeds=[8, 8], wind_directions=[270, 270]) + assert np.array_equal(fi.floris.farm.layout_x, np.array([0, 0])) + assert np.array_equal(fi.floris.farm.layout_y, np.array([0, 1000])) + assert np.array_equal(fi.floris.flow_field.wind_speeds, np.array([8, 8])) + assert np.array_equal(fi.floris.flow_field.wind_directions, np.array([270, 270])) + + # Double check that nothing has changed after running the calculation fi.run() - assert fi.floris.farm.power_setpoints == power_setpoints + assert np.array_equal(fi.floris.farm.layout_x, np.array([0, 0])) + assert np.array_equal(fi.floris.farm.layout_y, np.array([0, 1000])) + assert np.array_equal(fi.floris.flow_field.wind_speeds, np.array([8, 8])) + assert np.array_equal(fi.floris.flow_field.wind_directions, np.array([270, 270])) + + # Verify that changing wind shear doesn't change the other settings above + fi.set(wind_shear=0.1) + assert fi.floris.flow_field.wind_shear == 0.1 + assert np.array_equal(fi.floris.farm.layout_x, np.array([0, 0])) + assert np.array_equal(fi.floris.farm.layout_y, np.array([0, 1000])) + assert np.array_equal(fi.floris.flow_field.wind_speeds, np.array([8, 8])) + assert np.array_equal(fi.floris.flow_field.wind_directions, np.array([270, 270])) + + # Verify that operation set-points are retained after changing other settings + yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + fi.set(yaw_angles=yaw_angles) + assert np.array_equal(fi.floris.farm.yaw_angles, yaw_angles) + fi.set() + assert np.array_equal(fi.floris.farm.yaw_angles, yaw_angles) + fi.set(wind_speeds=[10, 10]) + assert np.array_equal(fi.floris.farm.yaw_angles, yaw_angles) + power_setpoints = 1e6 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + fi.set(power_setpoints=power_setpoints) + assert np.array_equal(fi.floris.farm.yaw_angles, yaw_angles) + assert np.array_equal(fi.floris.farm.power_setpoints, power_setpoints) + # Test that setting power setpoints through the .set() function actually sets the + # power setpoints in the floris object fi.reset_operation() + power_setpoints = 1e6 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + fi.set(power_setpoints=power_setpoints) fi.run() - assert fi.floris.farm.power_setpoints == ( - POWER_SETPOINT_DEFAULT * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) - ) + assert np.array_equal(fi.floris.farm.power_setpoints, power_setpoints) - fi.set(layout_x=[0, 0], layout_y=[0, 1000]) + # Similar to above, any "None" set-points should be set to the default value power_setpoints = np.array([[1e6, None]]) - fi.set(power_setpoints=power_setpoints) + fi.set(layout_x=[0, 0], layout_y=[0, 1000], power_setpoints=power_setpoints) fi.run() - assert np.allclose( + assert np.array_equal( fi.floris.farm.power_setpoints, np.array([[power_setpoints[0, 0], POWER_SETPOINT_DEFAULT]]) ) +def test_reset_operation(): + # Calling the reset function should reset the power setpoints to the default values + fi = FlorisInterface(configuration=YAML_INPUT) + yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + power_setpoints = 1e6 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + fi.set(power_setpoints=power_setpoints, yaw_angles=yaw_angles) + fi.run() + fi.reset_operation() + assert fi.floris.farm.yaw_angles == np.zeros( + (fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines) + ) + assert fi.floris.farm.power_setpoints == ( + POWER_SETPOINT_DEFAULT * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + ) + + # Double check that running the calculate also doesn't change the operating set points + fi.run() + assert fi.floris.farm.yaw_angles == np.zeros( + (fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines) + ) + assert fi.floris.farm.power_setpoints == ( + POWER_SETPOINT_DEFAULT * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) + ) + def test_run_no_wake(): - """ - In FLORIS v3.2, running calculate_no_wake twice incorrectly set the yaw angles when the first - time has non-zero yaw settings but the second run had all-zero yaw settings. The test below - asserts that the yaw angles are correctly set in subsequent calls to run_no_wake. - """ + # In FLORIS v3.2, running calculate_no_wake twice incorrectly set the yaw angles when the first + # time has non-zero yaw settings but the second run had all-zero yaw settings. The test below + # asserts that the yaw angles are correctly set in subsequent calls to run_no_wake. fi = FlorisInterface(configuration=YAML_INPUT) yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) fi.set(yaw_angles=yaw_angles) - fi.run() + fi.run_no_wake() assert fi.floris.farm.yaw_angles == yaw_angles yaw_angles = np.zeros((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) fi.set(yaw_angles=yaw_angles) - fi.run() + fi.run_no_wake() assert fi.floris.farm.yaw_angles == yaw_angles + # With no wake and three turbines in a line, the power for all turbines with zero yaw + # should be the same + fi.reset_operation() + fi.set(layout_x=[0, 200, 4000], layout_y=[0, 0, 0]) + fi.run_no_wake() + power_no_wake = fi.get_turbine_powers() + assert len(np.unique(power_no_wake)) == 1 def test_get_turbine_powers(): # Get turbine powers should return n_findex x n_turbine powers @@ -100,7 +167,6 @@ def test_get_turbine_powers(): assert turbine_powers.shape[1] == n_turbines assert turbine_powers[0, 0] == turbine_powers[1, 0] - def test_get_farm_power(): fi = FlorisInterface(configuration=YAML_INPUT) @@ -268,7 +334,6 @@ def test_get_farm_aep(): # In this case farm_aep should match farm powers np.testing.assert_allclose(farm_aep, aep) - def test_get_farm_aep_with_conditions(): fi = FlorisInterface(configuration=YAML_INPUT) @@ -311,12 +376,10 @@ def test_get_farm_aep_with_conditions(): #Confirm n_findex reset after the operation assert n_findex == fi.floris.flow_field.n_findex - -def test_reinitailize_ti(): +def test_set_ti(): fi = FlorisInterface(configuration=YAML_INPUT) - # Set wind directions and wind speeds and turbulence intensitities - # with n_findex = 3 + # Set wind directions, wind speeds and turbulence intensities with n_findex = 3 fi.set( wind_speeds=[8.0, 8.0, 8.0], wind_directions=[240.0, 250.0, 260.0], @@ -324,8 +387,7 @@ def test_reinitailize_ti(): ) # Now confirm can change wind speeds and directions shape without changing - # turbulence intensity since this is allowed when the turbulence - # intensities are uniform + # turbulence intensity since this is allowed when the turbulence intensities are uniform # raises n_findex to 4 fi.set( wind_speeds=[8.0, 8.0, 8.0, 8.0], From 6c07e7a6e4acbb3d2b533a7f5f71af5eb3230f7e Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 11:56:59 -0700 Subject: [PATCH 20/33] =?UTF-8?q?Update=20other=20test=20api=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parallel_computing_interface_integration_test.py | 2 +- tests/reg_tests/yaw_optimization_regression_test.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/parallel_computing_interface_integration_test.py b/tests/parallel_computing_interface_integration_test.py index f55fe631c..6b31297d5 100644 --- a/tests/parallel_computing_interface_integration_test.py +++ b/tests/parallel_computing_interface_integration_test.py @@ -27,7 +27,7 @@ def test_parallel_turbine_powers(sample_inputs_fixture): fi_serial = FlorisInterface(sample_inputs_fixture.floris) fi_parallel_input = copy.deepcopy(fi_serial) - fi_serial.calculate_wake() + fi_serial.run() serial_turbine_powers = fi_serial.get_turbine_powers() diff --git a/tests/reg_tests/yaw_optimization_regression_test.py b/tests/reg_tests/yaw_optimization_regression_test.py index c9e79ff23..049aee508 100644 --- a/tests/reg_tests/yaw_optimization_regression_test.py +++ b/tests/reg_tests/yaw_optimization_regression_test.py @@ -84,7 +84,7 @@ def test_serial_refine(sample_inputs_fixture): wd_array = np.arange(0.0, 360.0, 90.0) ws_array = 8.0 * np.ones_like(wd_array) D = 126.0 # Rotor diameter for the NREL 5 MW - fi.reinitialize( + fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, @@ -114,20 +114,21 @@ def test_geometric_yaw(sample_inputs_fixture): wd_array = np.arange(0.0, 360.0, 90.0) ws_array = 8.0 * np.ones_like(wd_array) D = 126.0 # Rotor diameter for the NREL 5 MW - fi.reinitialize( + fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, wind_speeds=ws_array, ) - fi.calculate_wake() + fi.run() baseline_farm_power = fi.get_farm_power().squeeze() yaw_opt = YawOptimizationGeometric(fi) df_opt = yaw_opt.optimize() yaw_angles_opt_geo = np.vstack(yaw_opt.yaw_angles_opt) - fi.calculate_wake(yaw_angles=yaw_angles_opt_geo) + fi.set(yaw_angles=yaw_angles_opt_geo) + fi.run() geo_farm_power = fi.get_farm_power().squeeze() df_opt['farm_power_baseline'] = baseline_farm_power @@ -161,7 +162,7 @@ def test_scipy_yaw_opt(sample_inputs_fixture): wd_array = np.arange(0.0, 360.0, 90.0) ws_array = 8.0 * np.ones_like(wd_array) D = 126.0 # Rotor diameter for the NREL 5 MW - fi.reinitialize( + fi.set( layout_x=[0.0, 5 * D, 10 * D], layout_y=[0.0, 0.0, 0.0], wind_directions=wd_array, From e5ef2d8255acb192339c268ca62d5d3864819bc1 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 12:15:14 -0700 Subject: [PATCH 21/33] Fix line length --- tests/floris_interface_integration_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index 234cc821b..9f63f2834 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -23,9 +23,9 @@ def test_set_run(): retains or resets information as intended. """ - # In FLORIS v3.2, running calculate_wake twice incorrectly set the yaw angles when the first time - # has non-zero yaw settings but the second run had all-zero yaw settings. The test below asserts - # that the yaw angles are correctly set in subsequent calls to run. + # In FLORIS v3.2, running calculate_wake twice incorrectly set the yaw angles when the + # first time has non-zero yaw settings but the second run had all-zero yaw settings. + # The test below asserts that the yaw angles are correctly set in subsequent calls to run. fi = FlorisInterface(configuration=YAML_INPUT) yaw_angles = 20 * np.ones((fi.floris.flow_field.n_findex, fi.floris.farm.n_turbines)) fi.set(yaw_angles=yaw_angles) From bb4623a310c6976d6e08cb35be1e69e4c7d0e2ea Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 23 Feb 2024 13:26:53 -0700 Subject: [PATCH 22/33] Small edits to comments --- examples/01_opening_floris_computing_power.py | 4 ++-- examples/18_check_turbine.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/01_opening_floris_computing_power.py b/examples/01_opening_floris_computing_power.py index f39611116..52935a956 100644 --- a/examples/01_opening_floris_computing_power.py +++ b/examples/01_opening_floris_computing_power.py @@ -25,7 +25,7 @@ print("\n========================= Single Wind Direction and Wind Speed =========================") # Get the turbine powers assuming 1 wind direction and speed -# Set the yaw angles to 0 with 1 wind direction and speed, 2 turbines +# Set the yaw angles to 0 with 1 wind direction and speed fi.set(wind_directions=[270.0], wind_speeds=[8.0], yaw_angles=np.zeros([1, 2])) fi.run() @@ -43,7 +43,7 @@ wind_speeds = np.array([8.0, 9.0, 10.0]) wind_directions = np.array([270.0, 270.0, 270.0]) -# 3 wind directions/ speeds, 2 turbines +# 3 wind directions/ speeds fi.set(wind_speeds=wind_speeds, wind_directions=wind_directions, yaw_angles=np.zeros([3, 2])) fi.run() turbine_powers = fi.get_turbine_powers() / 1000.0 diff --git a/examples/18_check_turbine.py b/examples/18_check_turbine.py index 419e73439..a19a99306 100644 --- a/examples/18_check_turbine.py +++ b/examples/18_check_turbine.py @@ -19,10 +19,10 @@ # Grab the gch model fi = FlorisInterface("inputs/gch.yaml") -# Make one turbine sim +# Make one turbine simulation fi.set(layout_x=[0], layout_y=[0]) -# Apply wind speeds +# Apply wind directions and wind speeds fi.set(wind_speeds=ws_array, wind_directions=wd_array) # Get a list of available turbine models provided through FLORIS, and remove From afb9b90f7fb1c49a4f433797cc0f0bcfedbb4a50 Mon Sep 17 00:00:00 2001 From: Eric Simley Date: Fri, 23 Feb 2024 14:35:46 -0700 Subject: [PATCH 23/33] fixing wind rose example plots --- examples/34_wind_data.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index cce4902d7..44a40a99d 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -43,15 +43,20 @@ # Plot the wind rose fig, ax = plt.subplots(subplot_kw={"polar": True}) -wind_rose.plot_wind_rose(ax=ax) +wind_rose.plot_wind_rose(ax=ax,legend_kwargs={"title": "WS"}) +fig.suptitle("WindRose Plot") # Now build a wind rose with turbulence intensity wind_ti_rose = time_series.to_wind_ti_rose() -# Plot the wind rose -fig, ax = plt.subplots(subplot_kw={"polar": True}) -wind_ti_rose.plot_wind_rose(ax=ax) -wind_ti_rose.plot_wind_rose(ax=ax, wind_rose_var="ti") +# Plot the wind rose with TI +fig, axs = plt.subplots(2, 1, figsize=(6,8), subplot_kw={"polar": True}) +wind_ti_rose.plot_wind_rose(ax=axs[0], wind_rose_var="ws",legend_kwargs={"title": "WS"}) +axs[0].set_title("Wind Direction and Wind Speed Frequencies") +wind_ti_rose.plot_wind_rose(ax=axs[1], wind_rose_var="ti",legend_kwargs={"title": "TI"}) +axs[1].set_title("Wind Direction and Turbulence Intensity Frequencies") +fig.suptitle("WindTIRose Plots") +plt.tight_layout() # Now set up a FLORIS model and initialize it using the time series and wind rose fi = FlorisInterface("inputs/gch.yaml") From fd586ee6f248afadd37e16bfa21ff1e54975a14f Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 13:57:27 -0700 Subject: [PATCH 24/33] Remove unused input args --- floris/tools/floris_interface.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 79ae6ab8a..ea6e2d036 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -119,9 +119,7 @@ def set( wind_veer: float | None = None, reference_wind_height: float | None = None, turbulence_intensities: list[float] | NDArrayFloat | None = None, - # turbulence_kinetic_energy=None, air_density: float | None = None, - # wake: WakeModelManager = None, layout_x: list[float] | NDArrayFloat | None = None, layout_y: list[float] | NDArrayFloat | None = None, turbine_type: list | None = None, @@ -246,9 +244,7 @@ def _reinitialize( wind_veer: float | None = None, reference_wind_height: float | None = None, turbulence_intensities: list[float] | NDArrayFloat | None = None, - # turbulence_kinetic_energy=None, air_density: float | None = None, - # wake: WakeModelManager = None, layout_x: list[float] | NDArrayFloat | None = None, layout_y: list[float] | NDArrayFloat | None = None, turbine_type: list | None = None, @@ -359,12 +355,6 @@ def _reinitialize( if turbine_library_path is not None: farm_dict["turbine_library_path"] = turbine_library_path - ## Wake - # if wake is not None: - # self.floris.wake = wake - # if turbulence_kinetic_energy is not None: - # pass # TODO: not needed until GCH - if solver_settings is not None: floris_dict["solver"] = solver_settings From 432c87166b5d0863faa1eea61c2bdeb66c3c0aaf Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 16:44:17 -0700 Subject: [PATCH 25/33] Refactor and clean up --- floris/tools/floris_interface.py | 80 ++++++++++------------ tests/floris_interface_integration_test.py | 15 ++-- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index ea6e2d036..e8a5fda04 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -164,9 +164,9 @@ def set( n_findex x n_turbines. True values indicate the turbine is disabled at that findex and the power setpoint at that position is set to 0. Defaults to None. """ - # Reinitialize the floris object after saving the setpoints - save_yaw_angles = self.floris.farm.yaw_angles - save_power_setpoints = self.floris.farm.power_setpoints + # Initialize a new Floris object after saving the setpoints + _yaw_angles = self.floris.farm.yaw_angles + _power_setpoints = self.floris.farm.power_setpoints self._reinitialize( wind_speeds=wind_speeds, wind_directions=wind_directions, @@ -183,13 +183,16 @@ def set( heterogenous_inflow_config=heterogenous_inflow_config, wind_data=wind_data, ) - if not (save_yaw_angles == 0).all(): - self.floris.farm.yaw_angles = save_yaw_angles + + # If the yaw angles or power setpoints are not the default, set them back to the + # previous setting + if not (_yaw_angles == 0).all(): + self.floris.farm.yaw_angles = _yaw_angles if not ( - (save_power_setpoints == POWER_SETPOINT_DEFAULT) - | (save_power_setpoints == POWER_SETPOINT_DISABLED) + (_power_setpoints == POWER_SETPOINT_DEFAULT) + | (_power_setpoints == POWER_SETPOINT_DISABLED) ).all(): - self.floris.farm.power_setpoints = save_power_setpoints + self.floris.farm.power_setpoints = _power_setpoints # Set the operation self._set_operation( @@ -200,42 +203,10 @@ def set( def reset_operation(self): """ - Reinstantiate the floris interface and set all operation setpoints to their default values. - - Args: (None) + Instantiate a new Floris object to set all operation setpoints to their default values. """ self._reinitialize() - def run(self) -> None: - """ - Run the FLORIS solve to compute the velocity field and wake effects. - - Args: (None) - """ - - # Initialize solution space - self.floris.initialize_domain() - - # Perform the wake calculations - self.floris.steady_state_atmospheric_condition() - - def run_no_wake( - self, - ) -> None: - """ - This function is similar to `run()` except that it does not apply a wake model. That is, - the wind farm is modeled as if there is no wake in the flow. Yaw angles are used to reduce - the power and thrust of the turbine that is yawed. - - Args: (None) - """ - - # Initialize solution space - self.floris.initialize_domain() - - # Finalize values to user-supplied order - self.floris.finalize() - def _reinitialize( self, wind_speeds: list[float] | NDArrayFloat | None = None, @@ -254,7 +225,8 @@ def _reinitialize( wind_data: type[WindDataBase] | None = None, ): """ - Reinstantiate the floris object with updated conditions set by arguments. + Instantiate a new Floris object with updated conditions set by arguments. Any parameters + in Floris that aren't changed by arguments to this function retain their values. Args: wind_speeds (NDArrayFloat | list[float] | None, optional): Wind speeds at each findex. @@ -423,6 +395,30 @@ def _set_operation( self.floris.farm.yaw_angles[disable_turbines] = 0.0 self.floris.farm.power_setpoints[disable_turbines] = POWER_SETPOINT_DISABLED + def run(self) -> None: + """ + Run the FLORIS solve to compute the velocity field and wake effects. + """ + + # Initialize solution space + self.floris.initialize_domain() + + # Perform the wake calculations + self.floris.steady_state_atmospheric_condition() + + def run_no_wake(self) -> None: + """ + This function is similar to `run()` except that it does not apply a wake model. That is, + the wind farm is modeled as if there is no wake in the flow. Operation settings may + reduce the power and thrust of the turbine to where they're applied. + """ + + # Initialize solution space + self.floris.initialize_domain() + + # Finalize values to user-supplied order + self.floris.finalize() + def get_plane_of_points( self, normal_vector="z", diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index 9f63f2834..2bdedfd4d 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -238,12 +238,12 @@ def test_disable_turbines(): layout_y=[0,0,0] ) - # Confirm that passing in a disable value with wrong n_findex raises error + # Confirm that using a disable value with wrong n_findex raises error with pytest.raises(ValueError): fi.set(disable_turbines=np.zeros((10, 3), dtype=bool)) fi.run() - # Confirm that passing in a disable value with wrong n_turbines raises error + # Confirm that using a disable value with wrong n_turbines raises error with pytest.raises(ValueError): fi.set(disable_turbines=np.zeros((2, 10), dtype=bool)) fi.run() @@ -252,12 +252,12 @@ def test_disable_turbines(): fi.set(disable_turbines=np.ones((2, 3), dtype=bool)) fi.run() turbines_powers = fi.get_turbine_powers() - np.testing.assert_allclose(turbines_powers,0,atol=0.1) + np.testing.assert_allclose(turbines_powers, 0, atol=0.1) # Confirm the same for run_no_wake fi.run_no_wake() turbines_powers = fi.get_turbine_powers() - np.testing.assert_allclose(turbines_powers,0,atol=0.1) + np.testing.assert_allclose(turbines_powers, 0, atol=0.1) # Confirm that if all disabled values set to false, equivalent to running normally fi.reset_operation() @@ -284,7 +284,8 @@ def test_disable_turbines(): fi.run() power_with_middle_disabled = fi.get_turbine_powers() - fi.set(layout_x = [0,2000],layout_y = [0, 0], disable_turbines=np.zeros((2, 2), dtype=bool)) + disable_turbines = np.zeros((2, 3), dtype=bool) + fi.set(layout_x=[0,2000], layout_y=[0, 0], disable_turbines=disable_turbines) fi.run() power_with_middle_removed = fi.get_turbine_powers() @@ -293,8 +294,8 @@ def test_disable_turbines(): # Check that yaw angles are correctly set when turbines are disabled fi.set( - layout_x=[0,1000,2000], - layout_y=[0,0,0], + layout_x=[0, 1000, 2000], + layout_y=[0, 0, 0], disable_turbines=disable_turbines, yaw_angles=np.ones((2, 3)) ) From 3feb4cd1da33953a3e3430704f30138b9cd55323 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 17:13:05 -0700 Subject: [PATCH 26/33] Bug fix in tests --- tests/floris_interface_integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index 2bdedfd4d..21a46605c 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -284,7 +284,7 @@ def test_disable_turbines(): fi.run() power_with_middle_disabled = fi.get_turbine_powers() - disable_turbines = np.zeros((2, 3), dtype=bool) + disable_turbines = np.zeros((2, 2), dtype=bool) fi.set(layout_x=[0,2000], layout_y=[0, 0], disable_turbines=disable_turbines) fi.run() power_with_middle_removed = fi.get_turbine_powers() From 058930b053f2065469872bacb9f5ba69c2266b28 Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Fri, 23 Feb 2024 17:16:55 -0700 Subject: [PATCH 27/33] Error if calculate_wake or reinitialize are used --- floris/tools/floris_interface.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index e8a5fda04..535520415 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -1256,3 +1256,17 @@ def get_turbine_layout(self, z=False): return xcoords, ycoords, zcoords else: return xcoords, ycoords + + ### v3 functions that are removed - raise an error if used + + def calculate_wake(self): + raise NotImplementedError( + "The calculate_wake method has been removed. Please use the run method. " + "See https://nrel.github.io/floris/upgrade_guides/v3_to_v4.html for more information." + ) + + def reinitialize(self): + raise NotImplementedError( + "The reinitialize method has been removed. Please use the set method. " + "See https://nrel.github.io/floris/upgrade_guides/v3_to_v4.html for more information." + ) From e083ced1eb98d227c7de31849537f0afead3f74f Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 26 Feb 2024 09:06:09 -0700 Subject: [PATCH 28/33] Fix the bug in test_disable_turbines --- tests/floris_interface_integration_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index 21a46605c..f22f4f3ad 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -284,10 +284,14 @@ def test_disable_turbines(): fi.run() power_with_middle_disabled = fi.get_turbine_powers() - disable_turbines = np.zeros((2, 2), dtype=bool) - fi.set(layout_x=[0,2000], layout_y=[0, 0], disable_turbines=disable_turbines) - fi.run() - power_with_middle_removed = fi.get_turbine_powers() + # Set up case with middle turbine removed + disable_turbines_remove_middle = np.zeros((2, 2), dtype=bool) + fi_remove_middle = fi.copy() + fi_remove_middle.set(layout_x=[0,2000], + layout_y=[0, 0], + disable_turbines=disable_turbines_remove_middle) + fi_remove_middle.run() + power_with_middle_removed = fi_remove_middle.get_turbine_powers() np.testing.assert_almost_equal(power_with_middle_disabled[0,2], power_with_middle_removed[0,1]) np.testing.assert_almost_equal(power_with_middle_disabled[1,2], power_with_middle_removed[1,1]) From b979729552b03236b6ec082c21a4d297daef24a2 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 26 Feb 2024 09:08:59 -0700 Subject: [PATCH 29/33] Fix whitespace --- floris/tools/floris_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 535520415..7b95a80bf 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -1264,7 +1264,7 @@ def calculate_wake(self): "The calculate_wake method has been removed. Please use the run method. " "See https://nrel.github.io/floris/upgrade_guides/v3_to_v4.html for more information." ) - + def reinitialize(self): raise NotImplementedError( "The reinitialize method has been removed. Please use the set method. " From 6f3c4d07b620fdbfa9a316173f7b810d28ed9c5d Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 26 Feb 2024 09:14:51 -0700 Subject: [PATCH 30/33] Fix typo and docstrings --- floris/tools/floris_interface.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/floris/tools/floris_interface.py b/floris/tools/floris_interface.py index 7b95a80bf..97d8cae4b 100644 --- a/floris/tools/floris_interface.py +++ b/floris/tools/floris_interface.py @@ -510,7 +510,7 @@ def calculate_horizontal_plane( wd=None, ws=None, yaw_angles=None, - power_septoints=None, + power_setpoints=None, disable_turbines=None, ): """ @@ -528,6 +528,14 @@ def calculate_horizontal_plane( Defaults to None. y_bounds (tuple, optional): Limits of output array (in m). Defaults to None. + wd (float, optional): Wind direction. Defaults to None. + ws (float, optional): Wind speed. Defaults to None. + yaw_angles (NDArrayFloat, optional): Turbine yaw angles. Defaults + to None. + power_setpoints (NDArrayFloat, optional): + Turbine power setpoints. Defaults to None. + disable_turbines (NDArrayBool, optional): Boolean array on whether + to disable turbines. Defaults to None. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values @@ -555,7 +563,7 @@ def calculate_horizontal_plane( wind_speeds=ws, solver_settings=solver_settings, yaw_angles=yaw_angles, - power_setpoints=power_septoints, + power_setpoints=power_setpoints, disable_turbines=disable_turbines, ) @@ -696,6 +704,18 @@ def calculate_y_plane( Defaults to None. y_bounds (tuple, optional): Limits of output array (in m). Defaults to None. + z_bounds (tuple, optional): Limits of output array (in m). + Defaults to None. + wd (float, optional): Wind direction. Defaults to None. + ws (float, optional): Wind speed. Defaults to None. + yaw_angles (NDArrayFloat, optional): Turbine yaw angles. Defaults + to None. + power_setpoints (NDArrayFloat, optional): + Turbine power setpoints. Defaults to None. + disable_turbines (NDArrayBool, optional): Boolean array on whether + to disable turbines. Defaults to None. + + Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values From 0358f5271d26515ef4af860af92e7bd9b5808cc1 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 26 Feb 2024 09:16:24 -0700 Subject: [PATCH 31/33] Update docstring --- floris/tools/visualization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/floris/tools/visualization.py b/floris/tools/visualization.py index 535a8f865..eb54650ae 100644 --- a/floris/tools/visualization.py +++ b/floris/tools/visualization.py @@ -617,6 +617,8 @@ def calculate_horizontal_plane_with_turbines( wd (float, optional): Wind direction setting. Defaults to None. ws (float, optional): Wind speed setting. Defaults to None. yaw_angles (np.ndarray, optional): Yaw angles settings. Defaults to None. + power_setpoints (np.ndarray, optional): Power setpoints settings. Defaults to None. + disable_turbines (np.ndarray, optional): Disable turbines settings. Defaults to None. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values of x, y, u, v, w From 18cfa2ad481cef9a12642eba20267660836971de Mon Sep 17 00:00:00 2001 From: Rafael M Mudafort Date: Mon, 26 Feb 2024 13:00:35 -0600 Subject: [PATCH 32/33] Fix formatting --- tests/floris_interface_integration_test.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/floris_interface_integration_test.py b/tests/floris_interface_integration_test.py index f22f4f3ad..93243950f 100644 --- a/tests/floris_interface_integration_test.py +++ b/tests/floris_interface_integration_test.py @@ -284,12 +284,9 @@ def test_disable_turbines(): fi.run() power_with_middle_disabled = fi.get_turbine_powers() - # Set up case with middle turbine removed - disable_turbines_remove_middle = np.zeros((2, 2), dtype=bool) + # Two turbine case to compare against above fi_remove_middle = fi.copy() - fi_remove_middle.set(layout_x=[0,2000], - layout_y=[0, 0], - disable_turbines=disable_turbines_remove_middle) + fi_remove_middle.set(layout_x=[0,2000], layout_y=[0, 0]) fi_remove_middle.run() power_with_middle_removed = fi_remove_middle.get_turbine_powers() From f530c600d85cb7ccbda8c38c4b4f335faed37210 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 26 Feb 2024 14:13:05 -0700 Subject: [PATCH 33/33] Raise error if run() called on ParallelComputingInterface. --- floris/tools/parallel_computing_interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/floris/tools/parallel_computing_interface.py b/floris/tools/parallel_computing_interface.py index 5c1393fd5..7260b0305 100644 --- a/floris/tools/parallel_computing_interface.py +++ b/floris/tools/parallel_computing_interface.py @@ -280,10 +280,11 @@ def _postprocessing(self, output): return turbine_powers - def calculate_wake(self): # TODO: Remove or update this function? - # raise UserWarning("'calculate_wake' not supported. Please use - # 'get_turbine_powers' or 'get_farm_power' directly.") - return None # Do nothing + def run(self): # TODO: Remove or update this function? + raise UserWarning( + "'run' not supported on ParallelComputingInterface. Please use " + "'get_turbine_powers' or 'get_farm_power' directly." + ) def get_turbine_powers(self, yaw_angles=None): # Retrieve multiargs: preprocessing