From f4c09632d1d6b13a5c15abc73f5c69fcf810e6c5 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 14 Mar 2024 11:54:04 -0600 Subject: [PATCH 01/24] Add functions to reshape turbine and farm power to wd x ws --- floris/floris_model.py | 103 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 8ca0c1a96..e724a72fc 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -818,6 +818,53 @@ def get_turbine_powers(self) -> NDArrayFloat: ) return turbine_powers + def get_turbine_powers_in_rose(self) -> NDArrayFloat: + """Calculates the power at each turbine in the wind farm and returns + the power reshaped into a matrix of wind_directions x wind_speeds + + Returns: + NDArrayFloat: Powers at each turbine (wind_directions x wind_speeds) + """ + + # Check that each unique combination self.wind_directions + # and self_wind_speeds occurs no more than once + # Assuming wind_directions and wind_speeds are your arrays + flow_field_dict = self.core.as_dict()["flow_field"] + wind_directions = np.array(flow_field_dict["wind_directions"]) + wind_speeds = np.array(flow_field_dict["wind_speeds"]) + combined_wd_ws = np.stack((wind_directions, wind_speeds), axis=-1) + + unique_rows = np.unique(combined_wd_ws, axis=0) + + if unique_rows.shape[0] < wind_directions.shape[0]: + raise ValueError( + "Wind direction and wind speed combinations must be unique in order" + "to reshape the power array into a matrix of wind_directions x wind_speeds" + "ensure that FlorisModel run with WindRose, or WindRose-like input" + ) + + # Collect the turbine powers + turbine_powers = self.get_turbine_powers() + + # Get the unique wind directions and wind speeds and note the lengths + unique_wind_directions = np.unique(wind_directions) + unique_wind_speeds = np.unique(wind_speeds) + n_wd = len(unique_wind_directions) + n_ws = len(unique_wind_speeds) + + # Declare an array of size n_wd x n_ws x n_turbines, initialized to nans + reshaped_powers = np.full((n_wd, n_ws, turbine_powers.shape[1]), np.nan) + + # Fill the matrix with the turbine powers according to the entries in + # wind_directions and wind_speeds + for findex in range(turbine_powers.shape[0]): + wd_index = np.where(unique_wind_directions == wind_directions[findex])[0][0] + ws_index = np.where(unique_wind_speeds == wind_speeds[findex])[0][0] + reshaped_powers[wd_index, ws_index, :] = turbine_powers[findex, :] + + return reshaped_powers + + def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_thrust_coefficients = thrust_coefficient( velocities=self.core.flow_field.u, @@ -905,7 +952,7 @@ def get_farm_power( # for turbine in self.core.farm.turbines: # turbine.use_turbulence_correction = use_turbulence_correction - # Confirm calculate wake has been run + # Confirm run() has been run if self.core.state is not State.USED: raise RuntimeError( "Can't run function `FlorisModel.get_turbine_powers` without " @@ -933,6 +980,60 @@ def get_farm_power( return np.sum(turbine_powers, axis=1) + def get_farm_power_in_rose( + self, + turbine_weights=None, + ) -> NDArrayFloat: + """ + Report wind plant power from instance of floris. Optionally includes + uncertainty in wind direction and yaw position when determining power. + Uncertainty is included by computing the mean wind farm power for a + distribution of wind direction and yaw position deviations from the + original wind direction and yaw angles. + + Args: + 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 + from other turbines. For wind rose case, this should be + a 1D array with length equal to the number of turbines. + + Returns: + NDArrayFloat: Sum of wind turbine powers in W reshaped into a matrix + of wind_directions x wind_speeds. + """ + + # Confirm run() has been run + if self.core.state is not State.USED: + raise RuntimeError( + "Can't run function `FlorisModel.get_turbine_powers` without " + "first running `FlorisModel.calculate_wake`." + ) + + if turbine_weights is None: + # Default to equal weighing of all turbines when turbine_weights is None + turbine_weights = np.ones( + ( + self.core.farm.n_turbines, + ) + ) + + # Confirm that the turbine weights are of the correct length + elif len(turbine_weights) != self.core.farm.n_turbines: + raise ValueError( + "The length of the turbine weights must be equal " + "to the number of turbines in the wind farm." + ) + + # Get the turbine powers + turbine_powers = self.get_turbine_powers_in_rose() + + # Turbine powers will be (n_wd, n_ws, n_turbines) + # Multiply the turbine powers by the weights (n_turbines) along + # the turbine axis + # and sum along the turbine axis to get the farm power + return np.sum(turbine_powers * turbine_weights[None, None, :], axis=-1) + def get_farm_AEP( self, freq, From 6892eeb0a87ec4aa921d8d26f3d3c7914698f040 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 14 Mar 2024 11:54:18 -0600 Subject: [PATCH 02/24] Add tests of new wind rose functions --- tests/floris_model_integration_test.py | 113 ++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 397cbef9d..543feb148 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -4,7 +4,7 @@ import pytest import yaml -from floris import FlorisModel +from floris import FlorisModel, WindRose from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT @@ -493,3 +493,114 @@ def test_calculate_planes(): fmodel.calculate_y_plane(0.0, ws=[wind_speeds[0]], wd=[wind_directions[0]]) with pytest.raises(ValueError): fmodel.calculate_cross_plane(500.0, ws=[wind_speeds[0]], wd=[wind_directions[0]]) + +def test_get_turbine_powers_in_rose(): + fmodel = FlorisModel(configuration=YAML_INPUT) + + # Test that if wind directions and speeds are not unique + # calling get_turbine_powers_in_rose raises a ValueError + wind_speeds = np.array([8.0, 8.0]) + wind_directions = np.array([270.0, 270.0]) + turbulence_intensities = np.array([0.06, 0.06]) + + fmodel.set( + wind_speeds=wind_speeds, + wind_directions=wind_directions, + turbulence_intensities=turbulence_intensities, + ) + + fmodel.run() + + with pytest.raises(ValueError): + fmodel.get_turbine_powers_in_rose() + + # Now declare a WindRose with 2 wind directions and 3 wind speeds + # uniform TI and frequency + wind_rose = WindRose( + wind_directions = np.array([270.0, 280.0]), + wind_speeds = np.array([8.0, 10.0, 12.0]), + ti_table=0.06 + ) + + # Set this wind rose and set the layout to have 4 turbines + fmodel.set(wind_data=wind_rose, layout_x=[0, 1000, 2000, 3000], layout_y=[0, 0, 0, 0]) + + # Run + fmodel.run() + + # Get the turbine powers in the wind rose + turbine_powers = fmodel.get_turbine_powers_in_rose() + + # Turbine power should have shape (n_wind_directions, n_wind_speeds, n_turbines) + assert turbine_powers.shape == (2, 3, 4) + + # Rerun the model with single wind speed and directions to confirm results + fmodel.set( + wind_directions=np.array([270.]), + wind_speeds=np.array([10.]), + turbulence_intensities=np.array([0.06]) + ) + fmodel.run() + test_power = fmodel.get_turbine_powers() + assert np.allclose(turbine_powers[0, 1, :], test_power) + + fmodel.set( + wind_directions=np.array([280.]), + wind_speeds=np.array([12.]), + turbulence_intensities=np.array([0.06]) + ) + fmodel.run() + test_power = fmodel.get_turbine_powers() + assert np.allclose(turbine_powers[-1, -1, :], test_power) + + # Test that if certain combinations in the wind rose have 0 frequency, the power in + # those locations is nan + wind_rose = WindRose( + wind_directions = np.array([270.0, 280.0]), + wind_speeds = np.array([8.0, 10.0, 12.0]), + ti_table=0.06, + freq_table=np.array([[0.25, 0.25, 0.0], [0.0, 0.0, 0.5]]) + ) + fmodel.set(wind_data=wind_rose) + fmodel.run() + turbine_powers = fmodel.get_turbine_powers_in_rose() + assert np.isnan(turbine_powers[0, 2, 0]) + +def test_get_farm_power_in_rose(): + + fmodel = FlorisModel(configuration=YAML_INPUT) + + # Now declare a WindRose with 2 wind directions and 3 wind speeds + # uniform TI and frequency + wind_rose = WindRose( + wind_directions = np.array([270.0, 280.0]), + wind_speeds = np.array([8.0, 10.0, 12.0]), + ti_table=0.06 + ) + + # Set this wind rose and set the layout to have 4 turbines + fmodel.set(wind_data=wind_rose, layout_x=[0, 1000, 2000, 3000], layout_y=[0, 0, 0, 0]) + + # Run + fmodel.run() + + # Get the turbine powers in the wind rose + turbine_powers = fmodel.get_turbine_powers_in_rose() + + # Test that the farm power is the same as the sum of the turbine powers + farm_power = fmodel.get_farm_power_in_rose() + + # Sum the turbine powers over the turbine axis + turbine_powers_sum = turbine_powers.sum(axis=2) + + assert np.allclose(farm_power, turbine_powers_sum) + + # Test that if the last turbine's weight is set to 0, the farm power is the same as the + # sum of the first 3 turbines + turbine_weights = np.array([1.0, 1.0, 1.0, 0.0]) + farm_power = fmodel.get_farm_power_in_rose(turbine_weights=turbine_weights) + + # Sum the turbine powers over the turbine axis + turbine_powers_sum = turbine_powers[:, :, :-1].sum(axis=2) + + assert np.allclose(farm_power, turbine_powers_sum) From 65950c1c4123fb03b8676e51dd22bfb96897351f Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 17:23:15 -0600 Subject: [PATCH 03/24] wind_data saved onto FlorisModel; functions partially built. --- floris/floris_model.py | 142 ++++++++++++++++++++++++++++++++--------- floris/wind_data.py | 11 +--- 2 files changed, 113 insertions(+), 40 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index e724a72fc..1c02a1b02 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -25,7 +25,7 @@ NDArrayBool, NDArrayFloat, ) -from floris.wind_data import WindDataBase +from floris.wind_data import WindDataBase, WindRose, WindTIRose class FlorisModel(LoggingManager): @@ -93,6 +93,9 @@ def __init__(self, configuration: dict | str | Path): "but have a small change on accuracy." ) raise ValueError("turbine_grid_points must be less than or equal to 3.") + + # Initialize stored wind_data object to None + self._wind_data = None def assign_hub_height_to_ref_height(self): @@ -256,30 +259,31 @@ def _reinitialize( flow_field_dict = floris_dict["flow_field"] farm_dict = floris_dict["farm"] - # Make the given changes - - # First check if wind data is not None, - # if not, get wind speeds, wind direction and - # turbulence intensity using the unpack_for_reinitialize - # method - if wind_data is not None: - if ( - (wind_directions is not None) - or (wind_speeds is not None) - or (turbulence_intensities is not None) - or (heterogenous_inflow_config is not None) - ): + # + if ( + (wind_directions is not None) + or (wind_speeds is not None) + or (turbulence_intensities is not None) + or (heterogenous_inflow_config is not None) + ): + if wind_data is not None: raise ValueError( "If wind_data is passed to reinitialize, then do not pass wind_directions, " "wind_speeds, turbulence_intensities or " "heterogenous_inflow_config as this is redundant" ) - ( - wind_directions, - wind_speeds, - turbulence_intensities, - heterogenous_inflow_config, - ) = wind_data.unpack_for_reinitialize() + elif self._wind_data is not None: + self.logger.warning("Deleting stored wind_data information.") + self._wind_data = None + if wind_data is not None: + # Unpack wind data for reinitialization and save wind_data for use in output + ( + wind_directions, + wind_speeds, + turbulence_intensities, + heterogenous_inflow_config, + ) = wind_data.unpack_for_reinitialize() + self._wind_data = wind_data ## FlowField if wind_speeds is not None: @@ -576,7 +580,7 @@ def calculate_horizontal_plane( # Reset the fmodel object back to the turbine grid configuration self.core = Core.from_dict(floris_dict) - # Run the simulation again for futher postprocessing (i.e. now we can get farm power) + # Run the simulation again for further postprocessing (i.e. now we can get farm power) self.run() return horizontal_plane @@ -650,7 +654,7 @@ def calculate_cross_plane( # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? - # It seems the biggest depenedcy is on CutPlane and the subsequent visualization tools. + # It seems the biggest dependency is on CutPlane and the subsequent visualization tools. df = self.get_plane_of_points( normal_vector="x", planar_coordinate=downstream_dist, @@ -662,7 +666,7 @@ def calculate_cross_plane( # Reset the fmodel object back to the turbine grid configuration self.core = Core.from_dict(floris_dict) - # Run the simulation again for futher postprocessing (i.e. now we can get farm power) + # Run the simulation again for further postprocessing (i.e. now we can get farm power) self.run() return cross_plane @@ -749,7 +753,7 @@ def calculate_y_plane( # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? - # It seems the biggest depenedcy is on CutPlane and the subsequent visualization tools. + # It seems the biggest dependency is on CutPlane and the subsequent visualization tools. df = self.get_plane_of_points( normal_vector="y", planar_coordinate=crossstream_dist, @@ -761,7 +765,7 @@ def calculate_y_plane( # Reset the fmodel object back to the turbine grid configuration self.core = Core.from_dict(floris_dict) - # Run the simulation again for futher postprocessing (i.e. now we can get farm power) + # Run the simulation again for further postprocessing (i.e. now we can get farm power) self.run() return y_plane @@ -912,7 +916,7 @@ def turbine_average_velocities(self) -> NDArrayFloat: def get_turbine_TIs(self) -> NDArrayFloat: return self.core.flow_field.turbulence_intensity_field - def get_farm_power( + def _get_farm_power( self, turbine_weights=None, use_turbulence_correction=False, @@ -951,12 +955,16 @@ def get_farm_power( # TODO: Uncomment out the following two lines once the above are resolved # for turbine in self.core.farm.turbines: # turbine.use_turbulence_correction = use_turbulence_correction + if use_turbulence_correction: + raise NotImplementedError( + "Turbulence correction is not yet implemented in the power calculation." + ) # Confirm run() has been run if self.core.state is not State.USED: raise RuntimeError( - "Can't run function `FlorisModel.get_turbine_powers` without " - "first running `FlorisModel.calculate_wake`." + "Can't run function `FlorisModel.get_farm_power` without " + "first running `FlorisModel.run`." ) if turbine_weights is None: @@ -979,6 +987,24 @@ def get_farm_power( turbine_powers = np.multiply(turbine_weights, turbine_powers) return np.sum(turbine_powers, axis=1) + + def get_farm_power( + self, + turbine_weights=None, + use_turbulence_correction=False, + ): + farm_power = self._get_farm_power(turbine_weights, use_turbulence_correction) + + if self._wind_data is not None: + if type(self._wind_data) is WindRose: + raise NotImplementedError("Figure out for WindRose") + # Todo : repackage power as a rose + elif type(self._wind_data) is WindTIRose: + raise NotImplementedError("Figure out for WindTIRose") + #repackage power as TI rose + # Wind Task 57 + + return farm_power def get_farm_power_in_rose( self, @@ -1006,8 +1032,7 @@ def get_farm_power_in_rose( # Confirm run() has been run if self.core.state is not State.USED: raise RuntimeError( - "Can't run function `FlorisModel.get_turbine_powers` without " - "first running `FlorisModel.calculate_wake`." + "Can't extract powers without first running `FlorisModel.run()`." ) if turbine_weights is None: @@ -1033,8 +1058,65 @@ def get_farm_power_in_rose( # the turbine axis # and sum along the turbine axis to get the farm power return np.sum(turbine_powers * turbine_weights[None, None, :], axis=-1) + + def get_expected_farm_power( + self, + freq=None, + turbine_weights=None, + ) -> float: + farm_power = self._get_farm_power(turbine_weights=turbine_weights) + + if freq is None: + if self._wind_data is None: + freq = np.array([1.0]) + else: + freq = self._wind_data.unpack_freq() + + return np.sum(np.multiply(freq, farm_power)) + def get_farm_AEP( + self, + freq=None, + turbine_weights=None, + hours_per_year=8760, + ) -> float: + """ + Estimate annual energy production (AEP) for distributions of wind speed, wind + direction, frequency of occurrence, and yaw offset. + + Args: + freq (NDArrayFloat): NumPy array with shape (n_findex) + with the frequencies of each wind direction and + wind speed combination. These frequencies should typically sum + up to 1.0 and are used to weigh the wind farm power for every + condition in calculating the wind farm's AEP. Defaults to None. + If None and a WindData object was supplied, the WindData object's + frequencies will be used. Otherwise, uniform frequencies are assumed. + 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 + from other turbines. This is useful when, for example, you are + modeling multiple wind farms in a single floris object. If you + only want to calculate the power production for one of those + farms and include the wake effects of the neighboring farms, + you can set the turbine_weights for the neighboring farms' + turbines to 0.0. The array of turbine powers from floris + is multiplied with this array in the calculation of the + objective function. If None, this is an array with all values + 1.0 and with shape equal to (n_findex, + n_turbines). Defaults to None. + + Returns: + float: + The Annual Energy Production (AEP) for the wind farm in + watt-hours. + """ + return self.get_expected_farm_power( + freq=freq, turbine_weights=turbine_weights + ) * hours_per_year + + def get_farm_AEP_( self, freq, cut_in_wind_speed=0.001, diff --git a/floris/wind_data.py b/floris/wind_data.py index ab202e670..94e1972b0 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -49,16 +49,7 @@ def unpack_for_reinitialize(self): def unpack_freq(self): """Unpack frequency weighting""" - ( - _, - _, - _, - freq_table_unpack, - _, - _, - ) = self.unpack() - - return freq_table_unpack + return self.unpack()[3] def check_heterogenous_inflow_config_by_wd(self, heterogenous_inflow_config_by_wd): """ From 3d20c0adcc1f8e972e4afbd008b5bad01bda89da Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 17:46:40 -0600 Subject: [PATCH 04/24] Update tests; rename _wind_data to wind_data since optimizers may need access. --- floris/floris_model.py | 18 ++--- .../layout_optimization_base.py | 14 ++-- tests/floris_model_integration_test.py | 66 +++++++++---------- tests/layout_optimization_integration_test.py | 27 ++++---- .../parallel_floris_model_integration_test.py | 2 + 5 files changed, 65 insertions(+), 62 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 1c02a1b02..3343cfefb 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -95,7 +95,7 @@ def __init__(self, configuration: dict | str | Path): raise ValueError("turbine_grid_points must be less than or equal to 3.") # Initialize stored wind_data object to None - self._wind_data = None + self.wind_data = None def assign_hub_height_to_ref_height(self): @@ -272,9 +272,9 @@ def _reinitialize( "wind_speeds, turbulence_intensities or " "heterogenous_inflow_config as this is redundant" ) - elif self._wind_data is not None: + elif self.wind_data is not None: self.logger.warning("Deleting stored wind_data information.") - self._wind_data = None + self.wind_data = None if wind_data is not None: # Unpack wind data for reinitialization and save wind_data for use in output ( @@ -283,7 +283,7 @@ def _reinitialize( turbulence_intensities, heterogenous_inflow_config, ) = wind_data.unpack_for_reinitialize() - self._wind_data = wind_data + self.wind_data = wind_data ## FlowField if wind_speeds is not None: @@ -995,11 +995,11 @@ def get_farm_power( ): farm_power = self._get_farm_power(turbine_weights, use_turbulence_correction) - if self._wind_data is not None: - if type(self._wind_data) is WindRose: + if self.wind_data is not None: + if type(self.wind_data) is WindRose: raise NotImplementedError("Figure out for WindRose") # Todo : repackage power as a rose - elif type(self._wind_data) is WindTIRose: + elif type(self.wind_data) is WindTIRose: raise NotImplementedError("Figure out for WindTIRose") #repackage power as TI rose # Wind Task 57 @@ -1068,10 +1068,10 @@ def get_expected_farm_power( farm_power = self._get_farm_power(turbine_weights=turbine_weights) if freq is None: - if self._wind_data is None: + if self.wind_data is None: freq = np.array([1.0]) else: - freq = self._wind_data.unpack_freq() + freq = self.wind_data.unpack_freq() return np.sum(np.multiply(freq, farm_power)) diff --git a/floris/optimization/layout_optimization/layout_optimization_base.py b/floris/optimization/layout_optimization/layout_optimization_base.py index c8e192d1a..7264a740a 100644 --- a/floris/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/optimization/layout_optimization/layout_optimization_base.py @@ -21,15 +21,13 @@ class LayoutOptimization(LoggingManager): fmodel (FlorisModel): A FlorisModel object. boundaries (iterable(float, float)): Pairs of x- and y-coordinates that represent the boundary's vertices (m). - wind_data (TimeSeries | WindRose): A TimeSeries or WindRose object - values. min_dist (float, optional): The minimum distance to be maintained between turbines during the optimization (m). If not specified, initializes to 2 rotor diameters. Defaults to None. enable_geometric_yaw (bool, optional): If True, enables geometric yaw optimization. Defaults to False. """ - def __init__(self, fmodel, boundaries, wind_data, min_dist=None, enable_geometric_yaw=False): + def __init__(self, fmodel, boundaries, min_dist=None, enable_geometric_yaw=False): self.fmodel = fmodel.copy() self.boundaries = boundaries self.enable_geometric_yaw = enable_geometric_yaw @@ -49,12 +47,13 @@ def __init__(self, fmodel, boundaries, wind_data, min_dist=None, enable_geometri self.min_dist = min_dist # Check that wind_data is a WindDataBase object - if (not isinstance(wind_data, WindDataBase)): + if (not isinstance(fmodel.wind_data, WindDataBase)): + # NOTE: it is no longer strictly necessary that fmodel use + # a WindData object, but it is still recommended. raise ValueError( "wind_data entry is not an object of WindDataBase" " (eg TimeSeries, WindRose, WindTIRose)" ) - self.wind_data = wind_data # Establish geometric yaw class if self.enable_geometric_yaw: @@ -63,8 +62,9 @@ def __init__(self, fmodel, boundaries, wind_data, min_dist=None, enable_geometri minimum_yaw_angle=-30.0, maximum_yaw_angle=30.0, ) - - self.initial_AEP = fmodel.get_farm_AEP_with_wind_data(self.wind_data) + # TODO: is this being used? + fmodel.run() + self.initial_AEP = fmodel.get_farm_AEP() def __str__(self): return "layout" diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 543feb148..3875a4c27 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -376,49 +376,49 @@ 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(): - fmodel = FlorisModel(configuration=YAML_INPUT) +# def test_get_farm_aep_with_conditions(): +# fmodel = FlorisModel(configuration=YAML_INPUT) - wind_speeds = np.array([5.0, 8.0, 8.0, 8.0, 20.0]) - wind_directions = np.array([270.0, 270.0, 270.0, 270.0, 270.0]) - turbulence_intensities = np.array([0.06, 0.06, 0.06, 0.06, 0.06]) - n_findex = len(wind_directions) +# wind_speeds = np.array([5.0, 8.0, 8.0, 8.0, 20.0]) +# wind_directions = np.array([270.0, 270.0, 270.0, 270.0, 270.0]) +# turbulence_intensities = np.array([0.06, 0.06, 0.06, 0.06, 0.06]) +# n_findex = len(wind_directions) - layout_x = np.array([0, 0]) - layout_y = np.array([0, 1000]) - # n_turbines = len(layout_x) +# layout_x = np.array([0, 0]) +# layout_y = np.array([0, 1000]) +# # n_turbines = len(layout_x) - fmodel.set( - wind_speeds=wind_speeds, - wind_directions=wind_directions, - turbulence_intensities=turbulence_intensities, - layout_x=layout_x, - layout_y=layout_y, - ) +# fmodel.set( +# wind_speeds=wind_speeds, +# wind_directions=wind_directions, +# turbulence_intensities=turbulence_intensities, +# layout_x=layout_x, +# layout_y=layout_y, +# ) - fmodel.run() +# fmodel.run() - farm_powers = fmodel.get_farm_power() +# farm_powers = fmodel.get_farm_power() - # Start with uniform frequency - freq = np.ones(n_findex) - freq = freq / np.sum(freq) +# # Start with uniform frequency +# freq = np.ones(n_findex) +# freq = freq / np.sum(freq) - # Get farm AEP with conditions on minimun and max wind speed - # which exclude the first and last findex - farm_aep = fmodel.get_farm_AEP(freq=freq, cut_in_wind_speed=6.0, cut_out_wind_speed=15.0) +# # Get farm AEP with conditions on minimun and max wind speed +# # which exclude the first and last findex +# farm_aep = fmodel.get_farm_AEP(freq=freq, cut_in_wind_speed=6.0, cut_out_wind_speed=15.0) - # In this case the aep should be computed assuming 0 power - # for the 0th and last findex - farm_powers[0] = 0 - farm_powers[-1] = 0 - aep = np.sum(np.multiply(freq, farm_powers) * 365 * 24) +# # In this case the aep should be computed assuming 0 power +# # for the 0th and last findex +# farm_powers[0] = 0 +# farm_powers[-1] = 0 +# aep = np.sum(np.multiply(freq, farm_powers) * 365 * 24) - # In this case farm_aep should match farm powers - np.testing.assert_allclose(farm_aep, aep) +# # In this case farm_aep should match farm powers +# np.testing.assert_allclose(farm_aep, aep) - #Confirm n_findex reset after the operation - assert n_findex == fmodel.core.flow_field.n_findex +# #Confirm n_findex reset after the operation +# assert n_findex == fmodel.core.flow_field.n_findex def test_set_ti(): fmodel = FlorisModel(configuration=YAML_INPUT) diff --git a/tests/layout_optimization_integration_test.py b/tests/layout_optimization_integration_test.py index dafd5e0d6..e283beebb 100644 --- a/tests/layout_optimization_integration_test.py +++ b/tests/layout_optimization_integration_test.py @@ -32,24 +32,25 @@ def test_base_class(): # (this should fail) freq = np.ones((5, 5)) freq = freq / freq.sum() - with pytest.raises(ValueError): - LayoutOptimization(fmodel, boundaries, freq, 5) - # Passing as a keyword freq to wind_data should also fail + # Check that ValueError raised if fmodel does not contain wind_data + with pytest.raises(ValueError): + LayoutOptimization(fmodel, boundaries, 5) with pytest.raises(ValueError): - LayoutOptimization(fmodel=fmodel, boundaries=boundaries, wind_data=freq, min_dist=5,) + LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5,) time_series = TimeSeries( wind_directions=fmodel.core.flow_field.wind_directions, wind_speeds=fmodel.core.flow_field.wind_speeds, turbulence_intensities=fmodel.core.flow_field.turbulence_intensities, ) - wind_rose = time_series.to_wind_rose() - - # Passing wind_data objects in the 3rd position should not fail - LayoutOptimization(fmodel, boundaries, time_series, 5) - LayoutOptimization(fmodel, boundaries, wind_rose, 5) - - # Passing wind_data objects by keyword should not fail - LayoutOptimization(fmodel=fmodel, boundaries=boundaries, wind_data=time_series, min_dist=5) - LayoutOptimization(fmodel=fmodel, boundaries=boundaries, wind_data=wind_rose, min_dist=5) + fmodel.set(wind_data=time_series) + + # Passing without keyword arguments should work, or with keyword arguments + LayoutOptimization(fmodel, boundaries, 5) + LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) + + # Check with WindRose on fmodel + fmodel.set(wind_data=time_series.to_wind_rose()) + LayoutOptimization(fmodel, boundaries, 5) + LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) diff --git a/tests/parallel_floris_model_integration_test.py b/tests/parallel_floris_model_integration_test.py index e5d603adf..550feb53e 100644 --- a/tests/parallel_floris_model_integration_test.py +++ b/tests/parallel_floris_model_integration_test.py @@ -60,6 +60,8 @@ def test_parallel_get_AEP(sample_inputs_fixture): fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel_input = copy.deepcopy(fmodel) + + fmodel.run() serial_farm_AEP = fmodel.get_farm_AEP(freq=freq) pfmodel = ParallelFlorisModel( From 648cb14adc007bcc9c2407930c8a0c709be2bc98 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 17:57:52 -0600 Subject: [PATCH 05/24] 07 example updated (waked and no_wake match previous output). --- examples/07_calc_aep_from_rose.py | 23 ++++++----------------- floris/floris_model.py | 3 ++- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/examples/07_calc_aep_from_rose.py b/examples/07_calc_aep_from_rose.py index cc2de88d4..135a4c119 100644 --- a/examples/07_calc_aep_from_rose.py +++ b/examples/07_calc_aep_from_rose.py @@ -55,26 +55,15 @@ wind_speeds=wind_speeds, turbulence_intensities=turbulence_intensities, ) +fmodel.run() # Compute the AEP using the default settings aep = fmodel.get_farm_AEP(freq=freq) -print("Farm AEP (default options): {:.3f} GWh".format(aep / 1.0e9)) - -# Compute the AEP again while specifying a cut-in and cut-out wind speed. -# The wake calculations are skipped for any wind speed below respectively -# above the cut-in and cut-out wind speed. This can speed up computation and -# prevent unexpected behavior for zero/negative and very high wind speeds. -# In this example, the results should not change between this and the default -# call to 'get_farm_AEP()'. -aep = fmodel.get_farm_AEP( - freq=freq, - cut_in_wind_speed=3.0, # Wakes are not evaluated below this wind speed - cut_out_wind_speed=25.0, # Wakes are not evaluated above this wind speed -) -print("Farm AEP (with cut_in/out specified): {:.3f} GWh".format(aep / 1.0e9)) +print("Farm AEP: {:.3f} GWh".format(aep / 1.0e9)) # Finally, we can also compute the AEP while ignoring all wake calculations. # This can be useful to quantity the annual wake losses in the farm. Such -# calculations can be facilitated by enabling the 'no_wake' handle. -aep_no_wake = fmodel.get_farm_AEP(freq, no_wake=True) -print("Farm AEP (no_wake=True): {:.3f} GWh".format(aep_no_wake / 1.0e9)) +# calculations can be facilitated by first running with run_no_wake(). +fmodel.run_no_wake() +aep_no_wake = fmodel.get_farm_AEP(freq=freq) +print("Farm AEP (no wakes): {:.3f} GWh".format(aep_no_wake / 1.0e9)) diff --git a/floris/floris_model.py b/floris/floris_model.py index 3343cfefb..d24d7b47e 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1073,7 +1073,7 @@ def get_expected_farm_power( else: freq = self.wind_data.unpack_freq() - return np.sum(np.multiply(freq, farm_power)) + return np.nansum(np.multiply(freq, farm_power)) def get_farm_AEP( self, @@ -1106,6 +1106,7 @@ def get_farm_AEP( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. + hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. Returns: float: From 6c9af1f3d34e9c990c06d0dff9e92d0275f3eece Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 19:05:07 -0600 Subject: [PATCH 06/24] Remove wind_data need from layout optimizers. --- .../layout_optimization_pyoptsparse.py | 11 +++-------- .../layout_optimization_pyoptsparse_spread.py | 3 +-- .../layout_optimization_scipy.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/floris/optimization/layout_optimization/layout_optimization_pyoptsparse.py b/floris/optimization/layout_optimization/layout_optimization_pyoptsparse.py index 9d26bc616..959b152a3 100644 --- a/floris/optimization/layout_optimization/layout_optimization_pyoptsparse.py +++ b/floris/optimization/layout_optimization/layout_optimization_pyoptsparse.py @@ -12,7 +12,6 @@ def __init__( self, fmodel, boundaries, - wind_data, min_dist=None, solver=None, optOptions=None, @@ -21,7 +20,7 @@ def __init__( hotStart=None, enable_geometric_yaw=False, ): - super().__init__(fmodel, boundaries, wind_data=wind_data, min_dist=min_dist, + super().__init__(fmodel, boundaries, min_dist=min_dist, enable_geometric_yaw=enable_geometric_yaw) self.x0 = self._norm(self.fmodel.layout_x, self.xmin, self.xmax) @@ -95,15 +94,11 @@ def _obj_func(self, varDict): yaw_angles = self._get_geoyaw_angles() # Update turbine map with turbine locations and yaw angles self.fmodel.set(layout_x=self.x, layout_y=self.y, yaw_angles=yaw_angles) + self.fmodel.run() # Compute the objective function funcs = {} - funcs["obj"] = ( - - -1 * self.fmodel.get_farm_AEP_with_wind_data(self.wind_data) - / self.initial_AEP - - ) + funcs["obj"] = -1 * self.fmodel.get_farm_AEP() / self.initial_AEP # Compute constraints, if any are defined for the optimization funcs = self.compute_cons(funcs, self.x, self.y) diff --git a/floris/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py b/floris/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py index aa8d9f54e..ac568d4de 100644 --- a/floris/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py +++ b/floris/optimization/layout_optimization/layout_optimization_pyoptsparse_spread.py @@ -12,7 +12,6 @@ def __init__( self, fmodel, boundaries, - wind_data, min_dist=None, solver=None, optOptions=None, @@ -20,7 +19,7 @@ def __init__( storeHistory='hist.hist', hotStart=None ): - super().__init__(fmodel, boundaries, wind_data=wind_data, min_dist=min_dist) + super().__init__(fmodel, boundaries, min_dist=min_dist) self._reinitialize(solver=solver, optOptions=optOptions) self.storeHistory = storeHistory diff --git a/floris/optimization/layout_optimization/layout_optimization_scipy.py b/floris/optimization/layout_optimization/layout_optimization_scipy.py index 23c866071..ff3048cae 100644 --- a/floris/optimization/layout_optimization/layout_optimization_scipy.py +++ b/floris/optimization/layout_optimization/layout_optimization_scipy.py @@ -13,7 +13,6 @@ def __init__( self, fmodel, boundaries, - wind_data, bnds=None, min_dist=None, solver='SLSQP', @@ -27,8 +26,6 @@ def __init__( fmodel (FlorisModel): A FlorisModel object. boundaries (iterable(float, float)): Pairs of x- and y-coordinates that represent the boundary's vertices (m). - wind_data (TimeSeries | WindRose): A TimeSeries or WindRose object - values. If None, equal weight is given to each pair of wind conditions bnds (iterable, optional): Bounds for the optimization variables (pairs of min/max values for each variable (m)). If none are specified, they are set to 0 and 1. Defaults to None. @@ -39,8 +36,12 @@ def __init__( optOptions (dict, optional): Dicitonary for setting the optimization options. Defaults to None. """ - super().__init__(fmodel, boundaries, min_dist=min_dist, wind_data=wind_data, - enable_geometric_yaw=enable_geometric_yaw) + super().__init__( + fmodel, + boundaries, + min_dist=min_dist, + enable_geometric_yaw=enable_geometric_yaw + ) self.boundaries_norm = [ [ @@ -98,9 +99,9 @@ def _obj_func(self, locs): # Compute turbine yaw angles using PJ's geometric code (if enabled) yaw_angles = self._get_geoyaw_angles() self.fmodel.set(yaw_angles=yaw_angles) + self.fmodel.run() - return (-1 * self.fmodel.get_farm_AEP_with_wind_data(self.wind_data) / - self.initial_AEP) + return -1 * self.fmodel.get_farm_AEP() / self.initial_AEP def _change_coordinates(self, locs): From 80d979583f86a8baefb1325e6160cc24e60ff151 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 19:19:49 -0600 Subject: [PATCH 07/24] Bugfix; copy did not bring over wind_data." --- .../layout_optimization/layout_optimization_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/floris/optimization/layout_optimization/layout_optimization_base.py b/floris/optimization/layout_optimization/layout_optimization_base.py index 7264a740a..652d1129f 100644 --- a/floris/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/optimization/layout_optimization/layout_optimization_base.py @@ -28,7 +28,8 @@ class LayoutOptimization(LoggingManager): optimization. Defaults to False. """ def __init__(self, fmodel, boundaries, min_dist=None, enable_geometric_yaw=False): - self.fmodel = fmodel.copy() + self.fmodel = fmodel.copy() # Does not copy over the wind_data object + self.fmodel.set(wind_data=fmodel.wind_data) self.boundaries = boundaries self.enable_geometric_yaw = enable_geometric_yaw @@ -47,7 +48,7 @@ def __init__(self, fmodel, boundaries, min_dist=None, enable_geometric_yaw=False self.min_dist = min_dist # Check that wind_data is a WindDataBase object - if (not isinstance(fmodel.wind_data, WindDataBase)): + if (not isinstance(self.fmodel.wind_data, WindDataBase)): # NOTE: it is no longer strictly necessary that fmodel use # a WindData object, but it is still recommended. raise ValueError( From 3486ff91a03ba65e893041576e4b8f313278d6b0 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 19:20:30 -0600 Subject: [PATCH 08/24] Update examples 13, 15 --- examples/13_optimize_yaw_with_neighboring_farm.py | 3 +++ examples/15_optimize_layout.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/13_optimize_yaw_with_neighboring_farm.py b/examples/13_optimize_yaw_with_neighboring_farm.py index 18d5e1b26..300748341 100644 --- a/examples/13_optimize_yaw_with_neighboring_farm.py +++ b/examples/13_optimize_yaw_with_neighboring_farm.py @@ -202,6 +202,7 @@ def yaw_opt_interpolant(wd, ws): print(" ") print("===========================================================") print("Calculating baseline annual energy production (AEP)...") + fmodel_aep.run() aep_bl_subset = 1.0e-9 * fmodel_aep.get_farm_AEP( freq=freq_windrose, turbine_weights=turbine_weights @@ -247,11 +248,13 @@ def yaw_opt_interpolant(wd, ws): print("===========================================================") print("Calculating annual energy production with wake steering (AEP)...") fmodel_aep.set(yaw_angles=yaw_angles_opt_nonb_AEP) + fmodel_aep.run() aep_opt_subset_nonb = 1.0e-9 * fmodel_aep.get_farm_AEP( freq=freq_windrose, turbine_weights=turbine_weights, ) fmodel_aep.set(yaw_angles=yaw_angles_opt_AEP) + fmodel_aep.run() aep_opt_subset = 1.0e-9 * fmodel_aep.get_farm_AEP( freq=freq_windrose, turbine_weights=turbine_weights, diff --git a/examples/15_optimize_layout.py b/examples/15_optimize_layout.py index 071a62b87..df0f1d460 100644 --- a/examples/15_optimize_layout.py +++ b/examples/15_optimize_layout.py @@ -54,7 +54,7 @@ fmodel.set(layout_x=layout_x, layout_y=layout_y) # Setup the optimization problem -layout_opt = LayoutOptimizationScipy(fmodel, boundaries, wind_data=wind_rose) +layout_opt = LayoutOptimizationScipy(fmodel, boundaries) # Run the optimization sol = layout_opt.optimize() @@ -62,10 +62,10 @@ # Get the resulting improvement in AEP print('... calcuating improvement in AEP') fmodel.run() -base_aep = fmodel.get_farm_AEP_with_wind_data(wind_data=wind_rose) / 1e6 +base_aep = fmodel.get_farm_AEP() / 1e6 fmodel.set(layout_x=sol[0], layout_y=sol[1]) fmodel.run() -opt_aep = fmodel.get_farm_AEP_with_wind_data(wind_data=wind_rose) / 1e6 +opt_aep = fmodel.get_farm_AEP() / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep From 9a43a3e5036b907e868c74d94c01969d00107a69 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:14:29 -0600 Subject: [PATCH 09/24] Removing unneeded methods. --- floris/floris_model.py | 336 ++++++++--------------------------------- 1 file changed, 67 insertions(+), 269 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index d24d7b47e..af290d84e 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -789,7 +789,7 @@ def check_wind_condition_for_viz(self, wd=None, ws=None, ti=None): f"Current length is {len(ti)}." ) - def get_turbine_powers(self) -> NDArrayFloat: + def _get_turbine_powers(self) -> NDArrayFloat: """Calculates the power at each turbine in the wind farm. Returns: @@ -799,8 +799,7 @@ def get_turbine_powers(self) -> NDArrayFloat: # Confirm calculate wake has been run if self.core.state is not State.USED: raise RuntimeError( - "Can't run function `FlorisModel.get_turbine_powers` without " - "first running `FlorisModel.run`." + "Can't compute turbine powers without first running `FlorisModel.run()`." ) # Check for negative velocities, which could indicate bad model # parameters or turbines very closely spaced. @@ -822,53 +821,6 @@ def get_turbine_powers(self) -> NDArrayFloat: ) return turbine_powers - def get_turbine_powers_in_rose(self) -> NDArrayFloat: - """Calculates the power at each turbine in the wind farm and returns - the power reshaped into a matrix of wind_directions x wind_speeds - - Returns: - NDArrayFloat: Powers at each turbine (wind_directions x wind_speeds) - """ - - # Check that each unique combination self.wind_directions - # and self_wind_speeds occurs no more than once - # Assuming wind_directions and wind_speeds are your arrays - flow_field_dict = self.core.as_dict()["flow_field"] - wind_directions = np.array(flow_field_dict["wind_directions"]) - wind_speeds = np.array(flow_field_dict["wind_speeds"]) - combined_wd_ws = np.stack((wind_directions, wind_speeds), axis=-1) - - unique_rows = np.unique(combined_wd_ws, axis=0) - - if unique_rows.shape[0] < wind_directions.shape[0]: - raise ValueError( - "Wind direction and wind speed combinations must be unique in order" - "to reshape the power array into a matrix of wind_directions x wind_speeds" - "ensure that FlorisModel run with WindRose, or WindRose-like input" - ) - - # Collect the turbine powers - turbine_powers = self.get_turbine_powers() - - # Get the unique wind directions and wind speeds and note the lengths - unique_wind_directions = np.unique(wind_directions) - unique_wind_speeds = np.unique(wind_speeds) - n_wd = len(unique_wind_directions) - n_ws = len(unique_wind_speeds) - - # Declare an array of size n_wd x n_ws x n_turbines, initialized to nans - reshaped_powers = np.full((n_wd, n_ws, turbine_powers.shape[1]), np.nan) - - # Fill the matrix with the turbine powers according to the entries in - # wind_directions and wind_speeds - for findex in range(turbine_powers.shape[0]): - wd_index = np.where(unique_wind_directions == wind_directions[findex])[0][0] - ws_index = np.where(unique_wind_speeds == wind_speeds[findex])[0][0] - reshaped_powers[wd_index, ws_index, :] = turbine_powers[findex, :] - - return reshaped_powers - - def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_thrust_coefficients = thrust_coefficient( velocities=self.core.flow_field.u, @@ -983,7 +935,7 @@ def _get_farm_power( ) # Calculate all turbine powers and apply weights - turbine_powers = self.get_turbine_powers() + turbine_powers = self._get_turbine_powers() turbine_powers = np.multiply(turbine_weights, turbine_powers) return np.sum(turbine_powers, axis=1) @@ -997,93 +949,66 @@ def get_farm_power( if self.wind_data is not None: if type(self.wind_data) is WindRose: - raise NotImplementedError("Figure out for WindRose") - # Todo : repackage power as a rose + farm_power_expanded = np.full(len(self.wind_data.wd_flat), np.nan) + farm_power_expanded[self.wind_data.non_zero_freq_mask] = farm_power + farm_power = farm_power_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds) + ) elif type(self.wind_data) is WindTIRose: - raise NotImplementedError("Figure out for WindTIRose") - #repackage power as TI rose - # Wind Task 57 + farm_power_expanded = np.full(len(self.wind_data.wd_flat), np.nan) + farm_power_expanded[self.wind_data.non_zero_freq_mask] = farm_power + farm_power = farm_power_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds), + len(self.wind_data.turbulence_intensities) + ) return farm_power - - def get_farm_power_in_rose( - self, - turbine_weights=None, - ) -> NDArrayFloat: + + def get_turbine_powers(self): """ - Report wind plant power from instance of floris. Optionally includes - uncertainty in wind direction and yaw position when determining power. - Uncertainty is included by computing the mean wind farm power for a - distribution of wind direction and yaw position deviations from the - original wind direction and yaw angles. - - Args: - 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 - from other turbines. For wind rose case, this should be - a 1D array with length equal to the number of turbines. + Calculates the power at each turbine in the wind farm. Returns: - NDArrayFloat: Sum of wind turbine powers in W reshaped into a matrix - of wind_directions x wind_speeds. + NDArrayFloat: Powers at each turbine. """ + turbine_powers = self._get_turbine_powers() - # Confirm run() has been run - if self.core.state is not State.USED: - raise RuntimeError( - "Can't extract powers without first running `FlorisModel.run()`." - ) - - if turbine_weights is None: - # Default to equal weighing of all turbines when turbine_weights is None - turbine_weights = np.ones( - ( - self.core.farm.n_turbines, + if self.wind_data is not None: + if type(self.wind_data) is WindRose: + turbine_powers_expanded = np.full( + (len(self.wind_data.wd_flat), self.core.farm.n_turbines), + np.nan + ) + turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds), + self.core.farm.n_turbines + ) + elif type(self.wind_data) is WindTIRose: + turbine_powers_expanded = np.full( + (len(self.wind_data.wd_flat), self.core.farm.n_turbines), + np.nan + ) + turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds), + len(self.wind_data.turbulence_intensities), + self.core.farm.n_turbines ) - ) - - # Confirm that the turbine weights are of the correct length - elif len(turbine_weights) != self.core.farm.n_turbines: - raise ValueError( - "The length of the turbine weights must be equal " - "to the number of turbines in the wind farm." - ) - - # Get the turbine powers - turbine_powers = self.get_turbine_powers_in_rose() - # Turbine powers will be (n_wd, n_ws, n_turbines) - # Multiply the turbine powers by the weights (n_turbines) along - # the turbine axis - # and sum along the turbine axis to get the farm power - return np.sum(turbine_powers * turbine_weights[None, None, :], axis=-1) + return turbine_powers def get_expected_farm_power( self, freq=None, turbine_weights=None, ) -> float: - - farm_power = self._get_farm_power(turbine_weights=turbine_weights) - - if freq is None: - if self.wind_data is None: - freq = np.array([1.0]) - else: - freq = self.wind_data.unpack_freq() - - return np.nansum(np.multiply(freq, farm_power)) - - def get_farm_AEP( - self, - freq=None, - turbine_weights=None, - hours_per_year=8760, - ) -> float: """ - Estimate annual energy production (AEP) for distributions of wind speed, wind - direction, frequency of occurrence, and yaw offset. + Compute the expected (mean) power of the wind farm. Args: freq (NDArrayFloat): NumPy array with shape (n_findex) @@ -1106,24 +1031,23 @@ def get_farm_AEP( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. - hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. - - Returns: - float: - The Annual Energy Production (AEP) for the wind farm in - watt-hours. """ - return self.get_expected_farm_power( - freq=freq, turbine_weights=turbine_weights - ) * hours_per_year - def get_farm_AEP_( + farm_power = self._get_farm_power(turbine_weights=turbine_weights) + + if freq is None: + if self.wind_data is None: + freq = np.array([1.0]) + else: + freq = self.wind_data.unpack_freq() + + return np.nansum(np.multiply(freq, farm_power)) + + def get_farm_AEP( self, - freq, - cut_in_wind_speed=0.001, - cut_out_wind_speed=None, + freq=None, turbine_weights=None, - no_wake=False, + hours_per_year=8760, ) -> float: """ Estimate annual energy production (AEP) for distributions of wind speed, wind @@ -1134,121 +1058,9 @@ def get_farm_AEP_( with the frequencies of each wind direction and wind speed combination. These frequencies should typically sum up to 1.0 and are used to weigh the wind farm power for every - condition in calculating the wind farm's AEP. - cut_in_wind_speed (float, optional): Wind speed in m/s below which - any calculations are ignored and the wind farm is known to - produce 0.0 W of power. Note that to prevent problems with the - wake models at negative / zero wind speeds, this variable must - always have a positive value. Defaults to 0.001 [m/s]. - cut_out_wind_speed (float, optional): Wind speed above which the - 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. - 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 - from other turbines. This is useful when, for example, you are - modeling multiple wind farms in a single floris object. If you - only want to calculate the power production for one of those - farms and include the wake effects of the neighboring farms, - you can set the turbine_weights for the neighboring farms' - turbines to 0.0. The array of turbine powers from floris - is multiplied with this array in the calculation of the - objective function. If None, this is an array with all values - 1.0 and with shape equal to (n_findex, - n_turbines). Defaults to None. - no_wake: (bool, optional): When *True* updates the turbine - quantities without calculating the wake or adding the wake to - the flow field. This can be useful when quantifying the loss - in AEP due to wakes. Defaults to *False*. - - - Returns: - float: - The Annual Energy Production (AEP) for the wind farm in - watt-hours. - """ - - # Verify dimensions of the variable "freq" - if np.shape(freq)[0] != self.core.flow_field.n_findex: - raise UserWarning( - "'freq' should be a one-dimensional array with dimensions (n_findex). " - f"Given shape is {np.shape(freq)}" - ) - - # Check if frequency vector sums to 1.0. If not, raise a warning - if np.abs(np.sum(freq) - 1.0) > 0.001: - self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() does not sum to 1.0." - ) - - # Copy the full wind speed array from the floris object and initialize - # the the farm_power variable as an empty array. - wind_speeds = np.array(self.core.flow_field.wind_speeds, copy=True) - wind_directions = np.array(self.core.flow_field.wind_directions, copy=True) - turbulence_intensities = np.array(self.core.flow_field.turbulence_intensities, copy=True) - farm_power = np.zeros(self.core.flow_field.n_findex) - - # Determine which wind speeds we must evaluate - conditions_to_evaluate = wind_speeds >= cut_in_wind_speed - if cut_out_wind_speed is not None: - conditions_to_evaluate = conditions_to_evaluate & (wind_speeds < cut_out_wind_speed) - - # Evaluate the conditions in floris - if np.any(conditions_to_evaluate): - wind_speeds_subset = wind_speeds[conditions_to_evaluate] - wind_directions_subset = wind_directions[conditions_to_evaluate] - turbulence_intensities_subset = turbulence_intensities[conditions_to_evaluate] - self.set( - wind_speeds=wind_speeds_subset, - wind_directions=wind_directions_subset, - turbulence_intensities=turbulence_intensities_subset, - ) - if no_wake: - self.run_no_wake() - else: - self.run() - farm_power[conditions_to_evaluate] = self.get_farm_power( - turbine_weights=turbine_weights - ) - - # Finally, calculate AEP in GWh - aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) - - # Reset the FLORIS object to the full wind speed array - self.set( - wind_speeds=wind_speeds, - wind_directions=wind_directions, - turbulence_intensities=turbulence_intensities - ) - - return aep - - def get_farm_AEP_with_wind_data( - self, - wind_data, - cut_in_wind_speed=0.001, - cut_out_wind_speed=None, - turbine_weights=None, - no_wake=False, - ) -> float: - """ - Estimate annual energy production (AEP) for distributions of wind speed, wind - direction, frequency of occurrence, and yaw offset. - - Args: - wind_data: (type(WindDataBase)): TimeSeries or WindRose object containing - the wind conditions over which to calculate the AEP. Should match the wind_data - object passed to reinitialize(). - cut_in_wind_speed (float, optional): Wind speed in m/s below which - any calculations are ignored and the wind farm is known to - produce 0.0 W of power. Note that to prevent problems with the - wake models at negative / zero wind speeds, this variable must - always have a positive value. Defaults to 0.001 [m/s]. - cut_out_wind_speed (float, optional): Wind speed above which the - 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. + condition in calculating the wind farm's AEP. Defaults to None. + If None and a WindData object was supplied, the WindData object's + frequencies will be used. Otherwise, uniform frequencies are assumed. 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 @@ -1262,31 +1074,17 @@ def get_farm_AEP_with_wind_data( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. - no_wake: (bool, optional): When *True* updates the turbine - quantities without calculating the wake or adding the wake to - the flow field. This can be useful when quantifying the loss - in AEP due to wakes. Defaults to *False*. + hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. Returns: float: The Annual Energy Production (AEP) for the wind farm in watt-hours. """ - - # Verify the wind_data object matches FLORIS' initialization - if wind_data.n_findex != self.core.flow_field.n_findex: - raise ValueError("WindData object and floris do not have same findex") - - # Get freq directly from wind_data - freq = wind_data.unpack_freq() - - return self.get_farm_AEP( - freq, - cut_in_wind_speed=cut_in_wind_speed, - cut_out_wind_speed=cut_out_wind_speed, - turbine_weights=turbine_weights, - no_wake=no_wake, - ) + return self.get_expected_farm_power( + freq=freq, + turbine_weights=turbine_weights + ) * hours_per_year def sample_flow_at_points(self, x: NDArrayFloat, y: NDArrayFloat, z: NDArrayFloat): """ From 2366eba8805ced8622e94d79fef8f3aff5fcecfc Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:16:47 -0600 Subject: [PATCH 10/24] Group hidden and outer get_turbine_powers methods. --- floris/floris_model.py | 84 +++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index af290d84e..6eed78101 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -93,7 +93,7 @@ def __init__(self, configuration: dict | str | Path): "but have a small change on accuracy." ) raise ValueError("turbine_grid_points must be less than or equal to 3.") - + # Initialize stored wind_data object to None self.wind_data = None @@ -259,7 +259,7 @@ def _reinitialize( flow_field_dict = floris_dict["flow_field"] farm_dict = floris_dict["farm"] - # + # if ( (wind_directions is not None) or (wind_speeds is not None) @@ -275,7 +275,7 @@ def _reinitialize( elif self.wind_data is not None: self.logger.warning("Deleting stored wind_data information.") self.wind_data = None - if wind_data is not None: + if wind_data is not None: # Unpack wind data for reinitialization and save wind_data for use in output ( wind_directions, @@ -821,6 +821,42 @@ def _get_turbine_powers(self) -> NDArrayFloat: ) return turbine_powers + def get_turbine_powers(self): + """ + Calculates the power at each turbine in the wind farm. + + Returns: + NDArrayFloat: Powers at each turbine. + """ + turbine_powers = self._get_turbine_powers() + + if self.wind_data is not None: + if type(self.wind_data) is WindRose: + turbine_powers_expanded = np.full( + (len(self.wind_data.wd_flat), self.core.farm.n_turbines), + np.nan + ) + turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds), + self.core.farm.n_turbines + ) + elif type(self.wind_data) is WindTIRose: + turbine_powers_expanded = np.full( + (len(self.wind_data.wd_flat), self.core.farm.n_turbines), + np.nan + ) + turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.wind_data.wind_directions), + len(self.wind_data.wind_speeds), + len(self.wind_data.turbulence_intensities), + self.core.farm.n_turbines + ) + + return turbine_powers + def get_turbine_thrust_coefficients(self) -> NDArrayFloat: turbine_thrust_coefficients = thrust_coefficient( velocities=self.core.flow_field.u, @@ -939,7 +975,7 @@ def _get_farm_power( turbine_powers = np.multiply(turbine_weights, turbine_powers) return np.sum(turbine_powers, axis=1) - + def get_farm_power( self, turbine_weights=None, @@ -965,43 +1001,7 @@ def get_farm_power( ) return farm_power - - def get_turbine_powers(self): - """ - Calculates the power at each turbine in the wind farm. - - Returns: - NDArrayFloat: Powers at each turbine. - """ - turbine_powers = self._get_turbine_powers() - if self.wind_data is not None: - if type(self.wind_data) is WindRose: - turbine_powers_expanded = np.full( - (len(self.wind_data.wd_flat), self.core.farm.n_turbines), - np.nan - ) - turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( - len(self.wind_data.wind_directions), - len(self.wind_data.wind_speeds), - self.core.farm.n_turbines - ) - elif type(self.wind_data) is WindTIRose: - turbine_powers_expanded = np.full( - (len(self.wind_data.wd_flat), self.core.farm.n_turbines), - np.nan - ) - turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( - len(self.wind_data.wind_directions), - len(self.wind_data.wind_speeds), - len(self.wind_data.turbulence_intensities), - self.core.farm.n_turbines - ) - - return turbine_powers - def get_expected_farm_power( self, freq=None, @@ -1040,9 +1040,9 @@ def get_expected_farm_power( freq = np.array([1.0]) else: freq = self.wind_data.unpack_freq() - + return np.nansum(np.multiply(freq, farm_power)) - + def get_farm_AEP( self, freq=None, From 782d8021eb5ccef4bcb9ed749c639646cdabda45 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:17:46 -0600 Subject: [PATCH 11/24] Ruff and isort. --- floris/floris_model.py | 6 +++++- .../layout_optimization/layout_optimization_base.py | 2 +- tests/layout_optimization_integration_test.py | 2 +- tests/parallel_floris_model_integration_test.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 6eed78101..607077223 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -25,7 +25,11 @@ NDArrayBool, NDArrayFloat, ) -from floris.wind_data import WindDataBase, WindRose, WindTIRose +from floris.wind_data import ( + WindDataBase, + WindRose, + WindTIRose, +) class FlorisModel(LoggingManager): diff --git a/floris/optimization/layout_optimization/layout_optimization_base.py b/floris/optimization/layout_optimization/layout_optimization_base.py index 652d1129f..02301350f 100644 --- a/floris/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/optimization/layout_optimization/layout_optimization_base.py @@ -49,7 +49,7 @@ def __init__(self, fmodel, boundaries, min_dist=None, enable_geometric_yaw=False # Check that wind_data is a WindDataBase object if (not isinstance(self.fmodel.wind_data, WindDataBase)): - # NOTE: it is no longer strictly necessary that fmodel use + # NOTE: it is no longer strictly necessary that fmodel use # a WindData object, but it is still recommended. raise ValueError( "wind_data entry is not an object of WindDataBase" diff --git a/tests/layout_optimization_integration_test.py b/tests/layout_optimization_integration_test.py index e283beebb..f9ba73927 100644 --- a/tests/layout_optimization_integration_test.py +++ b/tests/layout_optimization_integration_test.py @@ -45,7 +45,7 @@ def test_base_class(): turbulence_intensities=fmodel.core.flow_field.turbulence_intensities, ) fmodel.set(wind_data=time_series) - + # Passing without keyword arguments should work, or with keyword arguments LayoutOptimization(fmodel, boundaries, 5) LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) diff --git a/tests/parallel_floris_model_integration_test.py b/tests/parallel_floris_model_integration_test.py index 550feb53e..69a549641 100644 --- a/tests/parallel_floris_model_integration_test.py +++ b/tests/parallel_floris_model_integration_test.py @@ -60,7 +60,7 @@ def test_parallel_get_AEP(sample_inputs_fixture): fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel_input = copy.deepcopy(fmodel) - + fmodel.run() serial_farm_AEP = fmodel.get_farm_AEP(freq=freq) From 8a74427b18731fb83814bf6e3bb80c1775c69098 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:24:40 -0600 Subject: [PATCH 12/24] Add getter for wind_data. --- floris/floris_model.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 607077223..c3a34f51f 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -99,7 +99,7 @@ def __init__(self, configuration: dict | str | Path): raise ValueError("turbine_grid_points must be less than or equal to 3.") # Initialize stored wind_data object to None - self.wind_data = None + self._wind_data = None def assign_hub_height_to_ref_height(self): @@ -278,7 +278,7 @@ def _reinitialize( ) elif self.wind_data is not None: self.logger.warning("Deleting stored wind_data information.") - self.wind_data = None + self._wind_data = None if wind_data is not None: # Unpack wind data for reinitialization and save wind_data for use in output ( @@ -287,7 +287,7 @@ def _reinitialize( turbulence_intensities, heterogenous_inflow_config, ) = wind_data.unpack_for_reinitialize() - self.wind_data = wind_data + self._wind_data = wind_data ## FlowField if wind_speeds is not None: @@ -1254,6 +1254,10 @@ def layout_y(self): """ return self.core.farm.layout_y + @property + def wind_data(self): + return self._wind_data + def get_turbine_layout(self, z=False): """ Get turbine layout From 12fd7302f033810e3098b5fd344585513a833bde Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:42:00 -0600 Subject: [PATCH 13/24] Rename converters to be more explicit about output. --- examples/34_wind_data.py | 4 ++-- floris/wind_data.py | 8 ++++---- tests/layout_optimization_integration_test.py | 2 +- tests/wind_data_integration_test.py | 18 +++++++++--------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index 3a4d56fe5..78658d0fd 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -39,7 +39,7 @@ time_series = TimeSeries(wd_array, ws_array, turbulence_intensities=ti_array) # Now build the wind rose -wind_rose = time_series.to_wind_rose() +wind_rose = time_series.to_WindRose() # Plot the wind rose fig, ax = plt.subplots(subplot_kw={"polar": True}) @@ -47,7 +47,7 @@ fig.suptitle("WindRose Plot") # Now build a wind rose with turbulence intensity -wind_ti_rose = time_series.to_wind_ti_rose() +wind_ti_rose = time_series.to_WindTIRose() # Plot the wind rose with TI fig, axs = plt.subplots(2, 1, figsize=(6,8), subplot_kw={"polar": True}) diff --git a/floris/wind_data.py b/floris/wind_data.py index 94e1972b0..d14a845ac 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -389,7 +389,7 @@ def resample_wind_rose(self, wd_step=None, ws_step=None): ) # Now build a new wind rose using the new steps - return time_series.to_wind_rose( + return time_series.to_WindRose( wd_step=wd_step, ws_step=ws_step, bin_weights=self.freq_table_flat ) @@ -758,7 +758,7 @@ def resample_wind_rose(self, wd_step=None, ws_step=None, ti_step=None): ) # Now build a new wind rose using the new steps - return time_series.to_wind_ti_rose( + return time_series.to_WindTIRose( wd_step=wd_step, ws_step=ws_step, ti_step=ti_step, bin_weights=self.freq_table_flat ) @@ -1115,7 +1115,7 @@ def iref_func(wind_directions, wind_speeds): self.assign_ti_using_wd_ws_function(iref_func) - def to_wind_rose( + def to_WindRose( self, wd_step=2.0, ws_step=1.0, wd_edges=None, ws_edges=None, bin_weights=None ): """ @@ -1255,7 +1255,7 @@ def to_wind_rose( self.heterogenous_inflow_config_by_wd, ) - def to_wind_ti_rose( + def to_WindTIRose( self, wd_step=2.0, ws_step=1.0, diff --git a/tests/layout_optimization_integration_test.py b/tests/layout_optimization_integration_test.py index f9ba73927..018287e15 100644 --- a/tests/layout_optimization_integration_test.py +++ b/tests/layout_optimization_integration_test.py @@ -51,6 +51,6 @@ def test_base_class(): LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) # Check with WindRose on fmodel - fmodel.set(wind_data=time_series.to_wind_rose()) + fmodel.set(wind_data=time_series.to_WindRose()) LayoutOptimization(fmodel, boundaries, 5) LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) diff --git a/tests/wind_data_integration_test.py b/tests/wind_data_integration_test.py index ecc8281b3..9d597fcc4 100644 --- a/tests/wind_data_integration_test.py +++ b/tests/wind_data_integration_test.py @@ -218,12 +218,12 @@ def test_wrap_wind_directions_near_360(): assert np.allclose(wd_wrapped, expected_result) -def test_time_series_to_wind_rose(): +def test_time_series_to_WindRose(): # Test just 1 wind speed wind_directions = np.array([259.8, 260.2, 264.3]) wind_speeds = np.array([5.0, 5.0, 5.1]) time_series = TimeSeries(wind_directions, wind_speeds, 0.06) - wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + wind_rose = time_series.to_WindRose(wd_step=2.0, ws_step=1.0) # The wind directions should be 260, 262 and 264 because they're binned # to the nearest 2 deg increment @@ -243,7 +243,7 @@ def test_time_series_to_wind_rose(): wind_directions = np.array([259.8, 260.2, 264.3]) wind_speeds = np.array([5.0, 5.0, 6.1]) time_series = TimeSeries(wind_directions, wind_speeds, 0.06) - wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + wind_rose = time_series.to_WindRose(wd_step=2.0, ws_step=1.0) # The wind directions should be 260, 262 and 264 assert np.allclose(wind_rose.wind_directions, [260, 262, 264]) @@ -267,11 +267,11 @@ def test_time_series_to_wind_rose(): assert np.allclose(ti_table[~np.isnan(ti_table)], 0.06) -def test_time_series_to_wind_rose_wrapping(): +def test_time_series_to_WindRose_wrapping(): wind_directions = np.arange(0.0, 360.0, 0.25) wind_speeds = 8.0 * np.ones_like(wind_directions) time_series = TimeSeries(wind_directions, wind_speeds, 0.06) - wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + wind_rose = time_series.to_WindRose(wd_step=2.0, ws_step=1.0) # Expert for the first bin in this case to be 0, and the final to be 358 # and both to have equal numbers of points @@ -280,7 +280,7 @@ def test_time_series_to_wind_rose_wrapping(): np.testing.assert_almost_equal(wind_rose.freq_table[0, 0], wind_rose.freq_table[-1, 0]) -def test_time_series_to_wind_rose_with_ti(): +def test_time_series_to_WindRose_with_ti(): 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.5, 1.0, 1.5, 2.0]) @@ -289,7 +289,7 @@ def test_time_series_to_wind_rose_with_ti(): wind_speeds, turbulence_intensities=turbulence_intensities, ) - wind_rose = time_series.to_wind_rose(wd_step=2.0, ws_step=1.0) + wind_rose = time_series.to_WindRose(wd_step=2.0, ws_step=1.0) # Turbulence intensity should average to 1 in the 5 m/s bin and 2 in the 7 m/s bin ti_table = wind_rose.ti_table @@ -460,7 +460,7 @@ def test_wind_ti_rose_resample(): ) -def test_time_series_to_wind_ti_rose(): +def test_time_series_to_WindTIRose(): 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]) @@ -469,7 +469,7 @@ def test_time_series_to_wind_ti_rose(): 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) + wind_rose = time_series.to_WindTIRose(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 From e60e4b98cb8ade8e3484e65069076ffb8b278a72 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:52:41 -0600 Subject: [PATCH 14/24] Updating tests. --- floris/floris_model.py | 3 +- tests/floris_model_integration_test.py | 149 ++++++++----------------- 2 files changed, 49 insertions(+), 103 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index c3a34f51f..a01d4446f 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -1021,7 +1021,8 @@ def get_expected_farm_power( up to 1.0 and are used to weigh the wind farm power for every condition in calculating the wind farm's AEP. Defaults to None. If None and a WindData object was supplied, the WindData object's - frequencies will be used. Otherwise, uniform frequencies are assumed. + frequencies will be used. Otherwise, uniform frequencies are assumed + (i.e., a simple mean over the findices is computed). 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 diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 3875a4c27..b2aa8d8a0 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -376,49 +376,9 @@ 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(): -# fmodel = FlorisModel(configuration=YAML_INPUT) - -# wind_speeds = np.array([5.0, 8.0, 8.0, 8.0, 20.0]) -# wind_directions = np.array([270.0, 270.0, 270.0, 270.0, 270.0]) -# turbulence_intensities = np.array([0.06, 0.06, 0.06, 0.06, 0.06]) -# n_findex = len(wind_directions) - -# layout_x = np.array([0, 0]) -# layout_y = np.array([0, 1000]) -# # n_turbines = len(layout_x) - -# fmodel.set( -# wind_speeds=wind_speeds, -# wind_directions=wind_directions, -# turbulence_intensities=turbulence_intensities, -# layout_x=layout_x, -# layout_y=layout_y, -# ) - -# fmodel.run() - -# farm_powers = fmodel.get_farm_power() - -# # Start with uniform frequency -# freq = np.ones(n_findex) -# freq = freq / np.sum(freq) - -# # Get farm AEP with conditions on minimun and max wind speed -# # which exclude the first and last findex -# farm_aep = fmodel.get_farm_AEP(freq=freq, cut_in_wind_speed=6.0, cut_out_wind_speed=15.0) - -# # In this case the aep should be computed assuming 0 power -# # for the 0th and last findex -# farm_powers[0] = 0 -# farm_powers[-1] = 0 -# aep = np.sum(np.multiply(freq, farm_powers) * 365 * 24) - -# # In this case farm_aep should match farm powers -# np.testing.assert_allclose(farm_aep, aep) - -# #Confirm n_findex reset after the operation -# assert n_findex == fmodel.core.flow_field.n_findex + # Also check get_expected_farm_power + expected_farm_power = fmodel.get_expected_farm_power(freq=freq) + np.testing.assert_allclose(expected_farm_power, aep / (365 * 24)) def test_set_ti(): fmodel = FlorisModel(configuration=YAML_INPUT) @@ -494,64 +454,42 @@ def test_calculate_planes(): with pytest.raises(ValueError): fmodel.calculate_cross_plane(500.0, ws=[wind_speeds[0]], wd=[wind_directions[0]]) -def test_get_turbine_powers_in_rose(): +def test_get_turbine_powers_with_WindRose(): fmodel = FlorisModel(configuration=YAML_INPUT) - # Test that if wind directions and speeds are not unique - # calling get_turbine_powers_in_rose raises a ValueError - wind_speeds = np.array([8.0, 8.0]) - wind_directions = np.array([270.0, 270.0]) - turbulence_intensities = np.array([0.06, 0.06]) + wind_speeds = np.array([8.0, 10.0, 12.0, 8.0, 10.0, 12.0]) + wind_directions = np.array([270.0, 270.0, 270.0, 280.0, 280.0, 280.0]) + turbulence_intensities = 0.06 * np.ones_like(wind_speeds) fmodel.set( wind_speeds=wind_speeds, wind_directions=wind_directions, turbulence_intensities=turbulence_intensities, + layout_x=[0, 1000, 2000, 3000], + layout_y=[0, 0, 0, 0] ) - fmodel.run() - - with pytest.raises(ValueError): - fmodel.get_turbine_powers_in_rose() + turbine_powers_simple = fmodel.get_turbine_powers() # Now declare a WindRose with 2 wind directions and 3 wind speeds # uniform TI and frequency wind_rose = WindRose( - wind_directions = np.array([270.0, 280.0]), - wind_speeds = np.array([8.0, 10.0, 12.0]), + wind_directions=np.unique(wind_directions), + wind_speeds=np.unique(wind_speeds), ti_table=0.06 ) - # Set this wind rose and set the layout to have 4 turbines - fmodel.set(wind_data=wind_rose, layout_x=[0, 1000, 2000, 3000], layout_y=[0, 0, 0, 0]) - - # Run + # Set this wind rose, run + fmodel.set(wind_data=wind_rose) fmodel.run() # Get the turbine powers in the wind rose - turbine_powers = fmodel.get_turbine_powers_in_rose() + turbine_powers_windrose = fmodel.get_turbine_powers() # Turbine power should have shape (n_wind_directions, n_wind_speeds, n_turbines) - assert turbine_powers.shape == (2, 3, 4) - - # Rerun the model with single wind speed and directions to confirm results - fmodel.set( - wind_directions=np.array([270.]), - wind_speeds=np.array([10.]), - turbulence_intensities=np.array([0.06]) - ) - fmodel.run() - test_power = fmodel.get_turbine_powers() - assert np.allclose(turbine_powers[0, 1, :], test_power) - - fmodel.set( - wind_directions=np.array([280.]), - wind_speeds=np.array([12.]), - turbulence_intensities=np.array([0.06]) - ) - fmodel.run() - test_power = fmodel.get_turbine_powers() - assert np.allclose(turbine_powers[-1, -1, :], test_power) + assert turbine_powers_windrose.shape == (2, 3, 4) + assert np.allclose(turbine_powers_simple.reshape(2, 3, 4), turbine_powers_windrose) + assert np.allclose(turbine_powers_simple, turbine_powers_windrose.reshape(2*3, 4)) # Test that if certain combinations in the wind rose have 0 frequency, the power in # those locations is nan @@ -563,44 +501,51 @@ def test_get_turbine_powers_in_rose(): ) fmodel.set(wind_data=wind_rose) fmodel.run() - turbine_powers = fmodel.get_turbine_powers_in_rose() + turbine_powers = fmodel.get_turbine_powers() assert np.isnan(turbine_powers[0, 2, 0]) -def test_get_farm_power_in_rose(): - +def test_get_powers_with_wind_data(): fmodel = FlorisModel(configuration=YAML_INPUT) + wind_speeds = np.array([8.0, 10.0, 12.0, 8.0, 10.0, 12.0]) + wind_directions = np.array([270.0, 270.0, 270.0, 280.0, 280.0, 280.0]) + turbulence_intensities = 0.06 * np.ones_like(wind_speeds) + + fmodel.set( + wind_speeds=wind_speeds, + wind_directions=wind_directions, + turbulence_intensities=turbulence_intensities, + layout_x=[0, 1000, 2000, 3000], + layout_y=[0, 0, 0, 0] + ) + fmodel.run() + farm_power_simple = fmodel.get_farm_power() + # Now declare a WindRose with 2 wind directions and 3 wind speeds # uniform TI and frequency wind_rose = WindRose( - wind_directions = np.array([270.0, 280.0]), - wind_speeds = np.array([8.0, 10.0, 12.0]), + wind_directions=np.unique(wind_directions), + wind_speeds=np.unique(wind_speeds), ti_table=0.06 ) - # Set this wind rose and set the layout to have 4 turbines - fmodel.set(wind_data=wind_rose, layout_x=[0, 1000, 2000, 3000], layout_y=[0, 0, 0, 0]) - - # Run + # Set this wind rose, run + fmodel.set(wind_data=wind_rose) fmodel.run() - # Get the turbine powers in the wind rose - turbine_powers = fmodel.get_turbine_powers_in_rose() + farm_power_windrose = fmodel.get_farm_power() - # Test that the farm power is the same as the sum of the turbine powers - farm_power = fmodel.get_farm_power_in_rose() + # Check dimensions and that the farm power is the sum of the turbine powers + assert farm_power_windrose.shape == (2, 3) + assert np.allclose(farm_power_windrose, fmodel.get_turbine_powers().sum(axis=2)) - # Sum the turbine powers over the turbine axis - turbine_powers_sum = turbine_powers.sum(axis=2) - - assert np.allclose(farm_power, turbine_powers_sum) + # Check that simple and windrose powers are consistent + assert np.allclose(farm_power_simple.reshape(2, 3), farm_power_windrose) + assert np.allclose(farm_power_simple, farm_power_windrose.flatten()) # Test that if the last turbine's weight is set to 0, the farm power is the same as the # sum of the first 3 turbines turbine_weights = np.array([1.0, 1.0, 1.0, 0.0]) - farm_power = fmodel.get_farm_power_in_rose(turbine_weights=turbine_weights) - - # Sum the turbine powers over the turbine axis - turbine_powers_sum = turbine_powers[:, :, :-1].sum(axis=2) + farm_power_weighted = fmodel.get_farm_power(turbine_weights=turbine_weights) - assert np.allclose(farm_power, turbine_powers_sum) + assert np.allclose(farm_power_weighted, fmodel.get_turbine_powers()[:,:,:-1].sum(axis=2)) From 9e3a2b82fbffce0451ad5740dd61b1797152d60c Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 22:54:41 -0600 Subject: [PATCH 15/24] Copy up docstring from hidden version. --- floris/floris_model.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index a01d4446f..a81558651 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -934,9 +934,9 @@ def _get_farm_power( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. - use_turbulence_correction: (bool, optional): When *True* uses a + use_turbulence_correction: (bool, optional): When True uses a turbulence parameter to adjust power output calculations. - Defaults to *False*. + Defaults to False. Not currently implemented. Returns: float: Sum of wind turbine powers in W. @@ -985,6 +985,34 @@ def get_farm_power( turbine_weights=None, use_turbulence_correction=False, ): + """ + Report wind plant power from instance of floris. Optionally includes + uncertainty in wind direction and yaw position when determining power. + Uncertainty is included by computing the mean wind farm power for a + distribution of wind direction and yaw position deviations from the + original wind direction and yaw angles. + + Args: + 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 + from other turbines. This is useful when, for example, you are + modeling multiple wind farms in a single floris object. If you + only want to calculate the power production for one of those + farms and include the wake effects of the neighboring farms, + you can set the turbine_weights for the neighboring farms' + turbines to 0.0. The array of turbine powers from floris + is multiplied with this array in the calculation of the + objective function. If None, this is an array with all values + 1.0 and with shape equal to (n_findex, n_turbines). + Defaults to None. + use_turbulence_correction: (bool, optional): When True uses a + turbulence parameter to adjust power output calculations. + Defaults to False. Not currently implemented. + + Returns: + float: Sum of wind turbine powers in W. + """ farm_power = self._get_farm_power(turbine_weights, use_turbulence_correction) if self.wind_data is not None: From 38a3af1fe6bd5317b66240a2ef6aaa77cd37bcb3 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Tue, 19 Mar 2024 23:37:13 -0600 Subject: [PATCH 16/24] Fix a couple more examples. --- examples/16c_optimize_layout_with_heterogeneity.py | 13 +++++-------- examples/29_floating_vs_fixedbottom_farm.py | 1 + examples/34_wind_data.py | 6 +++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/16c_optimize_layout_with_heterogeneity.py b/examples/16c_optimize_layout_with_heterogeneity.py index 616b60e68..069511cd8 100644 --- a/examples/16c_optimize_layout_with_heterogeneity.py +++ b/examples/16c_optimize_layout_with_heterogeneity.py @@ -87,7 +87,6 @@ layout_opt = LayoutOptimizationScipy( fmodel, boundaries, - wind_data=wind_rose, min_dist=2*D, optOptions={"maxiter":maxiter} ) @@ -100,10 +99,10 @@ print('... calcuating improvement in AEP') fmodel.run() -base_aep = fmodel.get_farm_AEP_with_wind_data(wind_data=wind_rose) / 1e6 +base_aep = fmodel.get_farm_AEP() / 1e6 fmodel.set(layout_x=sol[0], layout_y=sol[1]) fmodel.run() -opt_aep = fmodel.get_farm_AEP_with_wind_data(wind_data=wind_rose) / 1e6 +opt_aep = fmodel.get_farm_AEP() / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep @@ -128,7 +127,6 @@ layout_opt = LayoutOptimizationScipy( fmodel, boundaries, - wind_data=wind_rose, min_dist=2*D, enable_geometric_yaw=True, optOptions={"maxiter":maxiter} @@ -142,12 +140,11 @@ print('... calcuating improvement in AEP') fmodel.set(yaw_angles=np.zeros_like(layout_opt.yaw_angles)) -base_aep = fmodel.get_farm_AEP_with_wind_data(wind_data=wind_rose) / 1e6 +fmodel.run() +base_aep = fmodel.get_farm_AEP() / 1e6 fmodel.set(layout_x=sol[0], layout_y=sol[1], yaw_angles=layout_opt.yaw_angles) fmodel.run() -opt_aep = fmodel.get_farm_AEP_with_wind_data( - wind_data=wind_rose -) / 1e6 +opt_aep = fmodel.get_farm_AEP() / 1e6 percent_gain = 100 * (opt_aep - base_aep) / base_aep diff --git a/examples/29_floating_vs_fixedbottom_farm.py b/examples/29_floating_vs_fixedbottom_farm.py index e04ac3f98..ef9745621 100644 --- a/examples/29_floating_vs_fixedbottom_farm.py +++ b/examples/29_floating_vs_fixedbottom_farm.py @@ -124,6 +124,7 @@ wind_speeds= ws_grid.flatten(), turbulence_intensities=0.06 * np.ones_like(wd_grid.flatten()) ) + fmodel.run() # Compute the AEP aep_fixed = fmodel_fixed.get_farm_AEP(freq=freq) diff --git a/examples/34_wind_data.py b/examples/34_wind_data.py index 78658d0fd..0d17e7924 100644 --- a/examples/34_wind_data.py +++ b/examples/34_wind_data.py @@ -78,9 +78,9 @@ wind_rose_power = fmodel_wind_rose.get_farm_power() wind_ti_rose_power = fmodel_wind_ti_rose.get_farm_power() -time_series_aep = fmodel_time_series.get_farm_AEP_with_wind_data(time_series) -wind_rose_aep = fmodel_wind_rose.get_farm_AEP_with_wind_data(wind_rose) -wind_ti_rose_aep = fmodel_wind_ti_rose.get_farm_AEP_with_wind_data(wind_ti_rose) +time_series_aep = fmodel_time_series.get_farm_AEP() +wind_rose_aep = fmodel_wind_rose.get_farm_AEP() +wind_ti_rose_aep = fmodel_wind_ti_rose.get_farm_AEP() print(f"AEP from TimeSeries {time_series_aep / 1e9:.2f} GWh") print(f"AEP from WindRose {wind_rose_aep / 1e9:.2f} GWh") From 6b1e32eac635eb3f2434e4da0c5a4fd0fdbbb3b0 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Mar 2024 12:01:12 -0600 Subject: [PATCH 17/24] Fix scaling on uniform frequency. --- floris/floris_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 588e3467d..95b2f0fd3 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -640,7 +640,7 @@ def get_expected_farm_power( if freq is None: if self.wind_data is None: - freq = np.array([1.0]) + freq = np.array([1.0/self.core.flow_field.n_findex]) else: freq = self.wind_data.unpack_freq() From cac2e52eb6c76e50c222fd0672125a312728ab56 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Wed, 20 Mar 2024 12:07:32 -0600 Subject: [PATCH 18/24] ruff and isort. --- floris/floris_model.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 95b2f0fd3..312cac32c 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -30,16 +30,17 @@ NDArrayBool, NDArrayFloat, ) -from floris.wind_data import ( - WindDataBase, - WindRose, - WindTIRose, -) from floris.utilities import ( nested_get, nested_set, print_nested_dict, ) +from floris.wind_data import ( + WindDataBase, + WindRose, + WindTIRose, +) + class FlorisModel(LoggingManager): """ @@ -106,7 +107,7 @@ def __init__(self, configuration: dict | str | Path): "but have a small change on accuracy." ) raise ValueError("turbine_grid_points must be less than or equal to 3.") - + # Initialize stored wind_data object to None self._wind_data = None @@ -440,7 +441,7 @@ def _get_turbine_powers(self) -> NDArrayFloat: multidim_condition=self.core.flow_field.multidim_conditions, ) return turbine_powers - + def get_turbine_powers(self): """ @@ -1389,7 +1390,7 @@ def turbine_average_velocities(self) -> NDArrayFloat: method=self.core.grid.average_method, cubature_weights=self.core.grid.cubature_weights, ) - + @property def wind_data(self): return self._wind_data From e56f6a11009c2759f4d1ebe07b7c1ef3798ad3b4 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Thu, 21 Mar 2024 14:09:42 -0600 Subject: [PATCH 19/24] Log warnings when freq not provided and test. --- floris/floris_model.py | 11 +++++++++ .../layout_optimization_base.py | 8 ++++--- tests/floris_model_integration_test.py | 12 +++++++++- tests/layout_optimization_integration_test.py | 24 +++++++++++++++---- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 312cac32c..56d5867cc 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -36,6 +36,7 @@ print_nested_dict, ) from floris.wind_data import ( + TimeSeries, WindDataBase, WindRose, WindTIRose, @@ -685,6 +686,16 @@ def get_farm_AEP( The Annual Energy Production (AEP) for the wind farm in watt-hours. """ + if ( + freq is None + and not isinstance(self.wind_data, WindRose) + and not isinstance(self.wind_data, WindTIRose) + ): + self.logger.warning( + "Computing AEP with uniform frequencies. Results results may not reflect annual " + "operation." + ) + return self.get_expected_farm_power( freq=freq, turbine_weights=turbine_weights diff --git a/floris/optimization/layout_optimization/layout_optimization_base.py b/floris/optimization/layout_optimization/layout_optimization_base.py index 02301350f..d52e6b1f2 100644 --- a/floris/optimization/layout_optimization/layout_optimization_base.py +++ b/floris/optimization/layout_optimization/layout_optimization_base.py @@ -51,9 +51,11 @@ def __init__(self, fmodel, boundaries, min_dist=None, enable_geometric_yaw=False if (not isinstance(self.fmodel.wind_data, WindDataBase)): # NOTE: it is no longer strictly necessary that fmodel use # a WindData object, but it is still recommended. - raise ValueError( - "wind_data entry is not an object of WindDataBase" - " (eg TimeSeries, WindRose, WindTIRose)" + self.logger.warning( + "Running layout optimization without a WindData object (e.g. TimeSeries, WindRose, " + "WindTIRose). We suggest that the user set the wind conditions on the FlorisModel " + " using the wind_data keyword argument for layout optimizations to capture " + "frequencies accurately." ) # Establish geometric yaw class diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 0fde77e5f..ae5f07558 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path import numpy as np @@ -341,7 +342,7 @@ def test_disable_turbines(): fmodel.run() assert (fmodel.core.farm.yaw_angles == np.array([[1.0, 0.0, 1.0], [1.0, 0.0, 1.0]])).all() -def test_get_farm_aep(): +def test_get_farm_aep(caplog): fmodel = FlorisModel(configuration=YAML_INPUT) wind_speeds = np.array([8.0, 8.0, 8.0]) @@ -369,6 +370,15 @@ def test_get_farm_aep(): freq = np.ones(n_findex) freq = freq / np.sum(freq) + # Check warning raised if freq not passed; no warning if freq passed + with caplog.at_level(logging.WARNING): + fmodel.get_farm_AEP() + assert caplog.text != "" # Checking not empty + caplog.clear() + with caplog.at_level(logging.WARNING): + fmodel.get_farm_AEP(freq=freq) + assert caplog.text == "" # Checking empty + farm_aep = fmodel.get_farm_AEP(freq=freq) aep = np.sum(np.multiply(freq, farm_powers) * 365 * 24) diff --git a/tests/layout_optimization_integration_test.py b/tests/layout_optimization_integration_test.py index 018287e15..0732b969c 100644 --- a/tests/layout_optimization_integration_test.py +++ b/tests/layout_optimization_integration_test.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path import numpy as np @@ -21,7 +22,7 @@ YAML_INPUT = TEST_DATA / "input_full.yaml" -def test_base_class(): +def test_base_class(caplog): # Get a test fi fmodel = FlorisModel(configuration=YAML_INPUT) @@ -33,11 +34,15 @@ def test_base_class(): freq = np.ones((5, 5)) freq = freq / freq.sum() - # Check that ValueError raised if fmodel does not contain wind_data - with pytest.raises(ValueError): + # Check that warning is raised if fmodel does not contain wind_data + with caplog.at_level(logging.WARNING): LayoutOptimization(fmodel, boundaries, 5) - with pytest.raises(ValueError): + assert caplog.text != "" # Checking not empty + + caplog.clear() + with caplog.at_level(logging.WARNING): LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5,) + assert caplog.text != "" # Checking not empty time_series = TimeSeries( wind_directions=fmodel.core.flow_field.wind_directions, @@ -46,11 +51,22 @@ def test_base_class(): ) fmodel.set(wind_data=time_series) + caplog.clear() + with caplog.at_level(logging.WARNING): + LayoutOptimization(fmodel, boundaries, 5) + assert caplog.text != "" # Not empty, because get_farm_AEP called on TimeSeries + # Passing without keyword arguments should work, or with keyword arguments LayoutOptimization(fmodel, boundaries, 5) LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) # Check with WindRose on fmodel fmodel.set(wind_data=time_series.to_WindRose()) + + caplog.clear() + with caplog.at_level(logging.WARNING): + LayoutOptimization(fmodel, boundaries, 5) + assert caplog.text == "" # Empty + LayoutOptimization(fmodel, boundaries, 5) LayoutOptimization(fmodel=fmodel, boundaries=boundaries, min_dist=5) From 7dee0b966078beaf608d793bc2dfb048f4e4c60b Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 22 Mar 2024 12:28:18 -0600 Subject: [PATCH 20/24] Update uncertain model for new paradigm --- floris/uncertain_floris_model.py | 355 +++++++++++++++++-------------- 1 file changed, 195 insertions(+), 160 deletions(-) diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 2cfc85b0b..52480130d 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -12,7 +12,12 @@ NDArrayFloat, ) from floris.utilities import wrap_180 -from floris.wind_data import WindDataBase +from floris.wind_data import ( + TimeSeries, + WindDataBase, + WindRose, + WindTIRose, +) class UncertainFlorisModel(LoggingManager): @@ -99,21 +104,6 @@ def __init__( # Instantiate the expanded FlorisModel # self.core_interface = FlorisModel(configuration) - def copy(self): - """Create an independent copy of the current UncertainFlorisModel object""" - return UncertainFlorisModel( - self.fmodel_unexpanded.core.as_dict(), - wd_resolution=self.wd_resolution, - ws_resolution=self.ws_resolution, - ti_resolution=self.ti_resolution, - yaw_resolution=self.yaw_resolution, - power_setpoint_resolution=self.power_setpoint_resolution, - wd_std=self.wd_std, - wd_sample_points=self.wd_sample_points, - fix_yaw_to_nominal_direction=self.fix_yaw_to_nominal_direction, - verbose=self.verbose, - ) - def set( self, **kwargs, @@ -207,20 +197,6 @@ def _set_uncertain( ], ) - def run(self): - """ - Run the simulation in the underlying FlorisModel object. - """ - - self.fmodel_expanded.run() - - def run_no_wake(self): - """ - Run the simulation in the underlying FlorisModel object without wakes. - """ - - self.fmodel_expanded.run_no_wake() - def reset_operation(self): """ Reset the operation of the underlying FlorisModel object. @@ -235,7 +211,21 @@ def reset_operation(self): # Calling set_uncertain again to reset the expanded FlorisModel self._set_uncertain() - def get_turbine_powers(self): + def run(self): + """ + Run the simulation in the underlying FlorisModel object. + """ + + self.fmodel_expanded.run() + + def run_no_wake(self): + """ + Run the simulation in the underlying FlorisModel object without wakes. + """ + + self.fmodel_expanded.run_no_wake() + + def _get_turbine_powers(self): """Calculates the power at each turbine in the wind farm. This method calculates the power at each turbine in the wind farm, considering @@ -248,7 +238,7 @@ def get_turbine_powers(self): # Pass to off-class function result = map_turbine_powers_uncertain( - unique_turbine_powers=self.fmodel_expanded.get_turbine_powers(), + unique_turbine_powers=self.fmodel_expanded._get_turbine_powers(), map_to_expanded_inputs=self.map_to_expanded_inputs, weights=self.weights, n_unexpanded=self.n_unexpanded, @@ -258,9 +248,58 @@ def get_turbine_powers(self): return result - def get_farm_power( + def get_turbine_powers(self): + """ + Calculate the power at each turbine in the wind farm. If WindRose or + WindTIRose is passed in, result is reshaped to match + + Returns: + NDArrayFloat: An array containing the powers at each turbine for each findex. + """ + + turbine_powers = self._get_turbine_powers() + + if self.fmodel_unexpanded.wind_data is not None: + if type(self.fmodel_unexpanded.wind_data) is WindRose: + turbine_powers_expanded = np.full( + ( + len(self.fmodel_unexpanded.wind_data.wd_flat), + self.fmodel_unexpanded.core.farm.n_turbines, + ), + np.nan, + ) + turbine_powers_expanded[ + self.fmodel_unexpanded.wind_data.non_zero_freq_mask, : + ] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.fmodel_unexpanded.wind_data.wind_directions), + len(self.fmodel_unexpanded.wind_data.wind_speeds), + self.fmodel_unexpanded.core.farm.n_turbines, + ) + elif type(self.fmodel_unexpanded.wind_data) is WindTIRose: + turbine_powers_expanded = np.full( + ( + len(self.fmodel_unexpanded.wind_data.wd_flat), + self.fmodel_unexpanded.core.farm.n_turbines, + ), + np.nan, + ) + turbine_powers_expanded[ + self.fmodel_unexpanded.wind_data.non_zero_freq_mask, : + ] = turbine_powers + turbine_powers = turbine_powers_expanded.reshape( + len(self.fmodel_unexpanded.wind_data.wind_directions), + len(self.fmodel_unexpanded.wind_data.wind_speeds), + len(self.fmodel_unexpanded.wind_data.turbulence_intensities), + self.fmodel_unexpanded.core.farm.n_turbines, + ) + + return turbine_powers + + def _get_farm_power( self, turbine_weights=None, + use_turbulence_correction=False, ): """ Report wind plant power from instance of floris with uncertainty. @@ -279,10 +318,23 @@ def get_farm_power( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. + use_turbulence_correction: (bool, optional): When True uses a + turbulence parameter to adjust power output calculations. + Defaults to False. Not currently implemented. Returns: float: Sum of wind turbine powers in W. """ + # TODO: Turbulence correction used in the power calculation, but may not be in + # the model yet + # TODO: Turbines need a switch for using turbulence correction + # TODO: Uncomment out the following two lines once the above are resolved + # for turbine in self.core.farm.turbines: + # turbine.use_turbulence_correction = use_turbulence_correction + if use_turbulence_correction: + raise NotImplementedError( + "Turbulence correction is not yet implemented in the power calculation." + ) if turbine_weights is None: # Default to equal weighing of all turbines when turbine_weights is None @@ -300,38 +352,86 @@ def get_farm_power( ) # Calculate all turbine powers and apply weights - turbine_powers = self.get_turbine_powers() + turbine_powers = self._get_turbine_powers() turbine_powers = np.multiply(turbine_weights, turbine_powers) return np.sum(turbine_powers, axis=1) - def get_farm_AEP( + def get_farm_power( self, - freq, - cut_in_wind_speed=0.001, - cut_out_wind_speed=None, turbine_weights=None, - no_wake=False, + use_turbulence_correction=False, + ): + """ + Report wind plant power from instance of floris. Optionally includes + uncertainty in wind direction and yaw position when determining power. + Uncertainty is included by computing the mean wind farm power for a + distribution of wind direction and yaw position deviations from the + original wind direction and yaw angles. + + Args: + 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 + from other turbines. This is useful when, for example, you are + modeling multiple wind farms in a single floris object. If you + only want to calculate the power production for one of those + farms and include the wake effects of the neighboring farms, + you can set the turbine_weights for the neighboring farms' + turbines to 0.0. The array of turbine powers from floris + is multiplied with this array in the calculation of the + objective function. If None, this is an array with all values + 1.0 and with shape equal to (n_findex, n_turbines). + Defaults to None. + use_turbulence_correction: (bool, optional): When True uses a + turbulence parameter to adjust power output calculations. + Defaults to False. Not currently implemented. + + Returns: + float: Sum of wind turbine powers in W. + """ + farm_power = self._get_farm_power(turbine_weights, use_turbulence_correction) + + if self.fmodel_unexpanded.wind_data is not None: + if type(self.fmodel_unexpanded.wind_data) is WindRose: + farm_power_expanded = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) + farm_power_expanded[ + self.fmodel_unexpanded.wind_data.non_zero_freq_mask + ] = farm_power + farm_power = farm_power_expanded.reshape( + len(self.fmodel_unexpanded.wind_data.wind_directions), + len(self.fmodel_unexpanded.wind_data.wind_speeds), + ) + elif type(self.fmodel_unexpanded.wind_data) is WindTIRose: + farm_power_expanded = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) + farm_power_expanded[ + self.fmodel_unexpanded.wind_data.non_zero_freq_mask + ] = farm_power + farm_power = farm_power_expanded.reshape( + len(self.fmodel_unexpanded.wind_data.wind_directions), + len(self.fmodel_unexpanded.wind_data.wind_speeds), + len(self.fmodel_unexpanded.wind_data.turbulence_intensities), + ) + + return farm_power + + def get_expected_farm_power( + self, + freq=None, + turbine_weights=None, ) -> float: """ - Estimate annual energy production (AEP) for distributions of wind speed, wind - direction, frequency of occurrence, and yaw offset. + Compute the expected (mean) power of the wind farm. Args: freq (NDArrayFloat): NumPy array with shape (n_findex) with the frequencies of each wind direction and wind speed combination. These frequencies should typically sum up to 1.0 and are used to weigh the wind farm power for every - condition in calculating the wind farm's AEP. - cut_in_wind_speed (float, optional): Wind speed in m/s below which - any calculations are ignored and the wind farm is known to - produce 0.0 W of power. Note that to prevent problems with the - wake models at negative / zero wind speeds, this variable must - always have a positive value. Defaults to 0.001 [m/s]. - cut_out_wind_speed (float, optional): Wind speed above which the - 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. + condition in calculating the wind farm's AEP. Defaults to None. + If None and a WindData object was supplied, the WindData object's + frequencies will be used. Otherwise, uniform frequencies are assumed + (i.e., a simple mean over the findices is computed). 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 @@ -345,92 +445,36 @@ def get_farm_AEP( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. - no_wake: (bool, optional): When *True* updates the turbine - quantities without calculating the wake or adding the wake to - the flow field. This can be useful when quantifying the loss - in AEP due to wakes. Defaults to *False*. - - - Returns: - float: - The Annual Energy Production (AEP) for the wind farm in - watt-hours. """ - # Verify dimensions of the variable "freq" - if np.shape(freq)[0] != self.n_unexpanded: - raise UserWarning( - "'freq' should be a one-dimensional array with dimensions (self.n_unexpanded). " - f"Given shape is {np.shape(freq)}" - ) + farm_power = self._get_farm_power(turbine_weights=turbine_weights) - # Check if frequency vector sums to 1.0. If not, raise a warning - if np.abs(np.sum(freq) - 1.0) > 0.001: - self.logger.warning( - "WARNING: The frequency array provided to get_farm_AEP() does not sum to 1.0." - ) - - # Copy the full wind speed array from the floris object and initialize - # the the farm_power variable as an empty array. - wind_directions = np.array(self.wind_directions_unexpanded, copy=True) - wind_speeds = np.array(self.wind_speeds_unexpanded, copy=True) - farm_power = np.zeros_like(wind_directions) - - # Determine which wind speeds we must evaluate - conditions_to_evaluate = wind_speeds >= cut_in_wind_speed - if cut_out_wind_speed is not None: - conditions_to_evaluate = conditions_to_evaluate & (wind_speeds < cut_out_wind_speed) - - # Evaluate the conditions in floris - if np.any(conditions_to_evaluate): - wind_speeds_subset = wind_speeds[conditions_to_evaluate] - wind_directions_subset = wind_directions[conditions_to_evaluate] - self.set( - wind_speeds=wind_speeds_subset, - wind_directions=wind_directions_subset, - ) - - if no_wake: - self.run_no_wake() + if freq is None: + if self.fmodel_unexpanded.wind_data is None: + freq = np.array([1.0 / self.core.flow_field.n_findex]) else: - self.run() - farm_power[conditions_to_evaluate] = self.get_farm_power( - turbine_weights=turbine_weights - ) + freq = self.fmodel_unexpanded.wind_data.unpack_freq() - # Finally, calculate AEP in GWh - aep = np.sum(np.multiply(freq, farm_power) * 365 * 24) + return np.nansum(np.multiply(freq, farm_power)) - # Reset the FLORIS object to the full wind speed array - self.set(wind_speeds=wind_speeds, wind_directions=wind_directions) - - return aep - - def get_farm_AEP_with_wind_data( + def get_farm_AEP( self, - wind_data, - cut_in_wind_speed=0.001, - cut_out_wind_speed=None, + freq=None, turbine_weights=None, - no_wake=False, + hours_per_year=8760, ) -> float: """ Estimate annual energy production (AEP) for distributions of wind speed, wind direction, frequency of occurrence, and yaw offset. Args: - wind_data: (type(WindDataBase)): TimeSeries or WindRose object containing - the wind conditions over which to calculate the AEP. Should match the wind_data - object passed to reinitialize(). - cut_in_wind_speed (float, optional): Wind speed in m/s below which - any calculations are ignored and the wind farm is known to - produce 0.0 W of power. Note that to prevent problems with the - wake models at negative / zero wind speeds, this variable must - always have a positive value. Defaults to 0.001 [m/s]. - cut_out_wind_speed (float, optional): Wind speed above which the - 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. + freq (NDArrayFloat): NumPy array with shape (n_findex) + with the frequencies of each wind direction and + wind speed combination. These frequencies should typically sum + up to 1.0 and are used to weigh the wind farm power for every + condition in calculating the wind farm's AEP. Defaults to None. + If None and a WindData object was supplied, the WindData object's + frequencies will be used. Otherwise, uniform frequencies are assumed. 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 @@ -444,51 +488,28 @@ def get_farm_AEP_with_wind_data( objective function. If None, this is an array with all values 1.0 and with shape equal to (n_findex, n_turbines). Defaults to None. - no_wake: (bool, optional): When *True* updates the turbine - quantities without calculating the wake or adding the wake to - the flow field. This can be useful when quantifying the loss - in AEP due to wakes. Defaults to *False*. + hours_per_year (float, optional): Number of hours in a year. Defaults to 365 * 24. Returns: float: The Annual Energy Production (AEP) for the wind farm in watt-hours. """ + if ( + freq is None + and not isinstance(self.fmodel_unexpanded.wind_data, WindRose) + and not isinstance(self.fmodel_unexpanded.wind_data, WindTIRose) + ): + self.logger.warning( + "Computing AEP with uniform frequencies. Results results may not reflect annual " + "operation." + ) - # Verify the wind_data object matches FLORIS' initialization - if wind_data.n_findex != self.n_unexpanded: - raise ValueError("WindData object findex not length n_unexpanded") - - # Get freq directly from wind_data - freq = wind_data.unpack_freq() - - return self.get_farm_AEP( - freq, - cut_in_wind_speed=cut_in_wind_speed, - cut_out_wind_speed=cut_out_wind_speed, - turbine_weights=turbine_weights, - no_wake=no_wake, + return ( + self.get_expected_farm_power(freq=freq, turbine_weights=turbine_weights) + * hours_per_year ) - # def copy(self): - # """Create an independent copy of the current UncertainFlorisModel object""" - # return UncertainFlorisModel( - # self.fmodel_unexpanded.core.as_dict(), - # wd_resolution=self.wd_resolution, - # ws_resolution=self.ws_resolution, - # ti_resolution=self.ti_resolution, - # yaw_resolution=self.yaw_resolution, - # power_setpoint_resolution=self.power_setpoint_resolution, - # wd_std=self.wd_std, - # wd_sample_points=self.wd_sample_points, - # verbose=self.verbose, - # ) - - # @property - # def core(self): - # """Return core of underlying expanded FlorisModel object""" - # return self.fmodel_expanded.core - def _get_rounded_inputs( self, input_array, @@ -617,7 +638,6 @@ def _expand_wind_directions( # If fix_yaw_to_nominal_direction is True, set the yaw angle to relative # to the nominal wind direction if fix_yaw_to_nominal_direction: - # Wrap between -180 and 180 output_array[start_idx:end_idx, 3 : 3 + n_turbines] = wrap_180( output_array[start_idx:end_idx, 3 : 3 + n_turbines] + wd_sample_points[i] @@ -670,6 +690,21 @@ def _get_weights(self, wd_std, wd_sample_points): return weights + def copy(self): + """Create an independent copy of the current UncertainFlorisModel object""" + return UncertainFlorisModel( + self.fmodel_unexpanded.core.as_dict(), + wd_resolution=self.wd_resolution, + ws_resolution=self.ws_resolution, + ti_resolution=self.ti_resolution, + yaw_resolution=self.yaw_resolution, + power_setpoint_resolution=self.power_setpoint_resolution, + wd_std=self.wd_std, + wd_sample_points=self.wd_sample_points, + fix_yaw_to_nominal_direction=self.fix_yaw_to_nominal_direction, + verbose=self.verbose, + ) + @property def layout_x(self): """ From bfa72932777398b386d18a15e770bd9ea3688567 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 22 Mar 2024 12:29:58 -0600 Subject: [PATCH 21/24] Add test of wind rose setting --- ...uncertain_floris_model_integration_test.py | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/uncertain_floris_model_integration_test.py b/tests/uncertain_floris_model_integration_test.py index c6bfb0f8e..42ac9ec8a 100644 --- a/tests/uncertain_floris_model_integration_test.py +++ b/tests/uncertain_floris_model_integration_test.py @@ -6,7 +6,7 @@ from floris import FlorisModel from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT -from floris.uncertain_floris_model import UncertainFlorisModel +from floris.uncertain_floris_model import UncertainFlorisModel, WindRose TEST_DATA = Path(__file__).resolve().parent / "data" @@ -215,3 +215,50 @@ def test_uncertain_floris_model_setpoints(): unc_powers = ufmodel.get_turbine_powers()[:, 1].flatten() np.testing.assert_allclose(np.sum(nom_powers * weights), unc_powers) + + +def test_get_powers_with_wind_data(): + ufmodel = UncertainFlorisModel(configuration=YAML_INPUT) + + wind_speeds = np.array([8.0, 10.0, 12.0, 8.0, 10.0, 12.0]) + wind_directions = np.array([270.0, 270.0, 270.0, 280.0, 280.0, 280.0]) + turbulence_intensities = 0.06 * np.ones_like(wind_speeds) + + ufmodel.set( + wind_speeds=wind_speeds, + wind_directions=wind_directions, + turbulence_intensities=turbulence_intensities, + layout_x=[0, 1000, 2000, 3000], + layout_y=[0, 0, 0, 0] + ) + ufmodel.run() + farm_power_simple = ufmodel.get_farm_power() + + # Now declare a WindRose with 2 wind directions and 3 wind speeds + # uniform TI and frequency + wind_rose = WindRose( + wind_directions=np.unique(wind_directions), + wind_speeds=np.unique(wind_speeds), + ti_table=0.06 + ) + + # Set this wind rose, run + ufmodel.set(wind_data=wind_rose) + ufmodel.run() + + farm_power_windrose = ufmodel.get_farm_power() + + # Check dimensions and that the farm power is the sum of the turbine powers + assert farm_power_windrose.shape == (2, 3) + assert np.allclose(farm_power_windrose, ufmodel.get_turbine_powers().sum(axis=2)) + + # Check that simple and windrose powers are consistent + assert np.allclose(farm_power_simple.reshape(2, 3), farm_power_windrose) + assert np.allclose(farm_power_simple, farm_power_windrose.flatten()) + + # Test that if the last turbine's weight is set to 0, the farm power is the same as the + # sum of the first 3 turbines + turbine_weights = np.array([1.0, 1.0, 1.0, 0.0]) + farm_power_weighted = ufmodel.get_farm_power(turbine_weights=turbine_weights) + + assert np.allclose(farm_power_weighted, ufmodel.get_turbine_powers()[:,:,:-1].sum(axis=2)) From 934c12334180192d743a66a620832e4d4e8a7080 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 22 Mar 2024 12:30:08 -0600 Subject: [PATCH 22/24] bufgix --- tests/parallel_floris_model_integration_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/parallel_floris_model_integration_test.py b/tests/parallel_floris_model_integration_test.py index 69a549641..4b4d5aeec 100644 --- a/tests/parallel_floris_model_integration_test.py +++ b/tests/parallel_floris_model_integration_test.py @@ -124,6 +124,7 @@ def test_parallel_uncertain_get_AEP(sample_inputs_fixture): wd_std=3 ) pfmodel_input = copy.deepcopy(ufmodel) + ufmodel.run() serial_farm_AEP = ufmodel.get_farm_AEP(freq=freq) pfmodel = ParallelFlorisModel( From cacd65ce8ee325e83b4ece8cee0e34915b839d68 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 22 Mar 2024 12:55:43 -0600 Subject: [PATCH 23/24] change _expanded suffix to _rose to avoid confusion in UncertainFlorisModel. --- floris/floris_model.py | 24 ++++++++++++------------ floris/uncertain_floris_model.py | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/floris/floris_model.py b/floris/floris_model.py index 56d5867cc..548f2e9f6 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -455,23 +455,23 @@ def get_turbine_powers(self): if self.wind_data is not None: if type(self.wind_data) is WindRose: - turbine_powers_expanded = np.full( + turbine_powers_rose = np.full( (len(self.wind_data.wd_flat), self.core.farm.n_turbines), np.nan ) - turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( + turbine_powers_rose[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), self.core.farm.n_turbines ) elif type(self.wind_data) is WindTIRose: - turbine_powers_expanded = np.full( + turbine_powers_rose = np.full( (len(self.wind_data.wd_flat), self.core.farm.n_turbines), np.nan ) - turbine_powers_expanded[self.wind_data.non_zero_freq_mask, :] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( + turbine_powers_rose[self.wind_data.non_zero_freq_mask, :] = turbine_powers + turbine_powers = turbine_powers_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), len(self.wind_data.turbulence_intensities), @@ -589,16 +589,16 @@ def get_farm_power( if self.wind_data is not None: if type(self.wind_data) is WindRose: - farm_power_expanded = np.full(len(self.wind_data.wd_flat), np.nan) - farm_power_expanded[self.wind_data.non_zero_freq_mask] = farm_power - farm_power = farm_power_expanded.reshape( + farm_power_rose = np.full(len(self.wind_data.wd_flat), np.nan) + farm_power_rose[self.wind_data.non_zero_freq_mask] = farm_power + farm_power = farm_power_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds) ) elif type(self.wind_data) is WindTIRose: - farm_power_expanded = np.full(len(self.wind_data.wd_flat), np.nan) - farm_power_expanded[self.wind_data.non_zero_freq_mask] = farm_power - farm_power = farm_power_expanded.reshape( + farm_power_rose = np.full(len(self.wind_data.wd_flat), np.nan) + farm_power_rose[self.wind_data.non_zero_freq_mask] = farm_power + farm_power = farm_power_rose.reshape( len(self.wind_data.wind_directions), len(self.wind_data.wind_speeds), len(self.wind_data.turbulence_intensities) diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 52480130d..2242f4075 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -261,33 +261,33 @@ def get_turbine_powers(self): if self.fmodel_unexpanded.wind_data is not None: if type(self.fmodel_unexpanded.wind_data) is WindRose: - turbine_powers_expanded = np.full( + turbine_powers_rose = np.full( ( len(self.fmodel_unexpanded.wind_data.wd_flat), self.fmodel_unexpanded.core.farm.n_turbines, ), np.nan, ) - turbine_powers_expanded[ + turbine_powers_rose[ self.fmodel_unexpanded.wind_data.non_zero_freq_mask, : ] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( + turbine_powers = turbine_powers_rose.reshape( len(self.fmodel_unexpanded.wind_data.wind_directions), len(self.fmodel_unexpanded.wind_data.wind_speeds), self.fmodel_unexpanded.core.farm.n_turbines, ) elif type(self.fmodel_unexpanded.wind_data) is WindTIRose: - turbine_powers_expanded = np.full( + turbine_powers_rose = np.full( ( len(self.fmodel_unexpanded.wind_data.wd_flat), self.fmodel_unexpanded.core.farm.n_turbines, ), np.nan, ) - turbine_powers_expanded[ + turbine_powers_rose[ self.fmodel_unexpanded.wind_data.non_zero_freq_mask, : ] = turbine_powers - turbine_powers = turbine_powers_expanded.reshape( + turbine_powers = turbine_powers_rose.reshape( len(self.fmodel_unexpanded.wind_data.wind_directions), len(self.fmodel_unexpanded.wind_data.wind_speeds), len(self.fmodel_unexpanded.wind_data.turbulence_intensities), @@ -394,20 +394,20 @@ def get_farm_power( if self.fmodel_unexpanded.wind_data is not None: if type(self.fmodel_unexpanded.wind_data) is WindRose: - farm_power_expanded = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) - farm_power_expanded[ + farm_power_rose = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) + farm_power_rose[ self.fmodel_unexpanded.wind_data.non_zero_freq_mask ] = farm_power - farm_power = farm_power_expanded.reshape( + farm_power = farm_power_rose.reshape( len(self.fmodel_unexpanded.wind_data.wind_directions), len(self.fmodel_unexpanded.wind_data.wind_speeds), ) elif type(self.fmodel_unexpanded.wind_data) is WindTIRose: - farm_power_expanded = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) - farm_power_expanded[ + farm_power_rose = np.full(len(self.fmodel_unexpanded.wind_data.wd_flat), np.nan) + farm_power_rose[ self.fmodel_unexpanded.wind_data.non_zero_freq_mask ] = farm_power - farm_power = farm_power_expanded.reshape( + farm_power = farm_power_rose.reshape( len(self.fmodel_unexpanded.wind_data.wind_directions), len(self.fmodel_unexpanded.wind_data.wind_speeds), len(self.fmodel_unexpanded.wind_data.turbulence_intensities), From 4c990298503e1025924dd131a0e9a103653cac47 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Fri, 22 Mar 2024 14:09:12 -0600 Subject: [PATCH 24/24] to_ methods specify class rather than suggested instantiation. --- floris/wind_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floris/wind_data.py b/floris/wind_data.py index e83b4bef4..2b8952e9f 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -610,7 +610,7 @@ def read_csv_long(file_path: str, time_series = TimeSeries(wind_directions, wind_speeds, turbulence_intensities) # Now build a new wind rose using the new steps - return time_series.to_wind_rose( + return time_series.to_WindRose( wd_step=wd_step, ws_step=ws_step, bin_weights=freq_values ) @@ -1049,7 +1049,7 @@ def read_csv_long(file_path: str, time_series = TimeSeries(wind_directions, wind_speeds, turbulence_intensities) # Now build a new wind rose using the new steps - return time_series.to_wind_ti_rose( + return time_series.to_WindTIRose( wd_step=wd_step, ws_step=ws_step, ti_step=ti_step,bin_weights=freq_values )