Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to fix yaw to nominal wind direction #850

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 98 additions & 53 deletions floris/uncertain_floris_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
NDArrayBool,
NDArrayFloat,
)
from floris.utilities import wrap_360
from floris.utilities import wrap_180
from floris.wind_data import WindDataBase


Expand All @@ -20,7 +20,14 @@ class UncertainFlorisModel(LoggingManager):
An interface for handling uncertainty in wind farm simulations.

This class contains a FlorisModel object and adds functionality to handle
uncertainty in wind direction.
uncertainty in wind direction. It is designed to be used similarly to FlorisModel.
In the model, the turbine powers are computed for a set of expanded wind conditions,
given by wd_sample_points, and then the powers are computed as a gaussian blend
of these expanded conditions.

To reduce computational costs, the wind directions, wind speeds, turbulence intensities,
yaw angles, and power setpoints are rounded to specified resolutions. Only unique
conditions from within the expanded set of conditions are run.

Args:
configuration (:py:obj:`dict`): The Floris configuration dictionary or YAML file.
Expand All @@ -29,20 +36,27 @@ class UncertainFlorisModel(LoggingManager):
- **farm**: See `floris.simulation.farm.Farm` for more details.
- **turbine**: See `floris.simulation.turbine.Turbine` for more details.
- **wake**: See `floris.simulation.wake.WakeManager` for more details.
- **logging**: See `floris.core.Core` for more details.
wd_resolution (float, optional): The resolution of wind direction, in degrees.
Defaults to 1.0.
- **logging**: See `floris.simulation.core.Core` for more details.
wd_resolution (float, optional): The resolution of wind direction for generating
gaussian blends, in degrees. Defaults to 1.0.
ws_resolution (float, optional): The resolution of wind speed, in m/s. Defaults to 1.0.
ti_resolution (float, optional): The resolution of turbulence intensity. Defaults to 0.01.
yaw_resolution (float, optional): The resolution of yaw angle, in degrees. Defaults to 1.0.
ti_resolution (float, optional): The resolution of turbulence intensity.
Defaults to 0.01.
yaw_resolution (float, optional): The resolution of yaw angle, in degrees.
Defaults to 1.0.
power_setpoint_resolution (int, optional): The resolution of power setpoints, in kW.
Defaults to 100.
wd_std (float, optional): The standard deviation of wind direction. Defaults to 3.0.
wd_sample_points (list[float], optional): The sample points for wind direction.
If not provided, defaults to [-2 * wd_std, -1 * wd_std, 0, wd_std, 2 * wd_std].
fix_yaw_to_nominal_direction (bool, optional): Fix the yaw angle to the nominal
direction? When False, the yaw misalignment is the same across the sampled wind
directions. When True, the turbine orientation is fixed to the nominal wind
direction such that the yaw misalignment changes depending on the sampled wind
direction. Defaults to False.
verbose (bool, optional): Verbosity flag for printing messages. Defaults to False.

"""

def __init__(
self,
configuration: dict | str | Path,
Expand All @@ -53,40 +67,17 @@ def __init__(
power_setpoint_resolution=100, # kW
wd_std=3.0,
wd_sample_points=None,
fix_yaw_to_nominal_direction=False,
verbose=False,
):
"""
Instantiate the UncertainFlorisModel.

Args:
configuration (:py:obj:`dict`): The Floris configuration dictionary or YAML file.
The configuration should have the following inputs specified.
- **flow_field**: See `floris.simulation.flow_field.FlowField` for more details.
- **farm**: See `floris.simulation.farm.Farm` for more details.
- **turbine**: See `floris.simulation.turbine.Turbine` for more details.
- **wake**: See `floris.simulation.wake.WakeManager` for more details.
- **logging**: See `floris.simulation.core.Core` for more details.
wd_resolution (float, optional): The resolution of wind direction for generating
gaussian blends, in degrees. Defaults to 1.0.
ws_resolution (float, optional): The resolution of wind speed, in m/s. Defaults to 1.0.
ti_resolution (float, optional): The resolution of turbulence intensity.
efaults to 0.01.
yaw_resolution (float, optional): The resolution of yaw angle, in degrees.
Defaults to 1.0.
power_setpoint_resolution (int, optional): The resolution of power setpoints, in kW.
Defaults to 100.
wd_std (float, optional): The standard deviation of wind direction. Defaults to 3.0.
wd_sample_points (list[float], optional): The sample points for wind direction.
If not provided, defaults to [-2 * wd_std, -1 * wd_std, 0, wd_std, 2 * wd_std].
verbose (bool, optional): Verbosity flag for printing messages. Defaults to False.
"""
# Save these inputs
self.wd_resolution = wd_resolution
self.ws_resolution = ws_resolution
self.ti_resolution = ti_resolution
self.yaw_resolution = yaw_resolution
self.power_setpoint_resolution = power_setpoint_resolution
self.wd_std = wd_std
self.fix_yaw_to_nominal_direction = fix_yaw_to_nominal_direction
self.verbose = verbose

# If wd_sample_points, default to 1 and 2 std
Expand All @@ -108,6 +99,20 @@ 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,
Expand All @@ -116,15 +121,13 @@ def set(
"""
Set the wind farm conditions in the UncertainFlorisModel.

See FlorisInterace.set() for details of the contents of kwargs.
See FlorisModel.set() for details of the contents of kwargs.

Args:
**kwargs: The wind farm conditions to set.
"""
# Call the nominal set function
self.fmodel_unexpanded.set(
**kwargs
)
self.fmodel_unexpanded.set(**kwargs)

self._set_uncertain()

Expand Down Expand Up @@ -171,7 +174,10 @@ def _set_uncertain(

# Get the expanded inputs
self._expanded_wind_directions = self._expand_wind_directions(
self.rounded_inputs, self.wd_sample_points
self.rounded_inputs,
self.wd_sample_points,
self.fix_yaw_to_nominal_direction,
self.fmodel_unexpanded.core.farm.n_turbines,
)
self.n_expanded = self._expanded_wind_directions.shape[0]

Expand All @@ -196,7 +202,9 @@ def _set_uncertain(
wind_speeds=self.unique_inputs[:, 1],
turbulence_intensities=self.unique_inputs[:, 2],
yaw_angles=self.unique_inputs[:, 3 : 3 + self.fmodel_unexpanded.core.farm.n_turbines],
power_setpoints=self.unique_inputs[:, 3 + self.fmodel_unexpanded.core.farm.n_turbines:]
power_setpoints=self.unique_inputs[
:, 3 + self.fmodel_unexpanded.core.farm.n_turbines :
],
)

def run(self):
Expand Down Expand Up @@ -245,7 +253,7 @@ def get_turbine_powers(self):
weights=self.weights,
n_unexpanded=self.n_unexpanded,
n_sample_points=self.n_sample_points,
n_turbines=self.fmodel_unexpanded.core.farm.n_turbines
n_turbines=self.fmodel_unexpanded.core.farm.n_turbines,
)

return result
Expand Down Expand Up @@ -525,17 +533,26 @@ def _get_rounded_inputs(
rounded_input_array[:, 2] = (
np.round(rounded_input_array[:, 2] / ti_resolution) * ti_resolution
)
rounded_input_array[:, 3] = (
np.round(rounded_input_array[:, 3] / yaw_resolution) * yaw_resolution
rounded_input_array[:, 3 : 3 + self.fmodel_unexpanded.core.farm.n_turbines] = (
np.round(
rounded_input_array[:, 3 : 3 + self.fmodel_unexpanded.core.farm.n_turbines]
/ yaw_resolution
)
* yaw_resolution
)
rounded_input_array[:, 4] = (
np.round(rounded_input_array[:, 4] / power_setpoint_resolution)
rounded_input_array[:, 3 + self.fmodel_unexpanded.core.farm.n_turbines :] = (
np.round(
rounded_input_array[:, 3 + self.fmodel_unexpanded.core.farm.n_turbines :]
/ power_setpoint_resolution
)
* power_setpoint_resolution
)

return rounded_input_array

def _expand_wind_directions(self, input_array, wd_sample_points):
def _expand_wind_directions(
self, input_array, wd_sample_points, fix_yaw_to_nominal_direction=False, n_turbines=None
):
"""
Expand wind direction data.

Expand All @@ -547,6 +564,10 @@ def _expand_wind_directions(self, input_array, wd_sample_points):
represents wind direction.
wd_sample_points (list): List of integers representing
wind direction sample points.
fix_yaw_to_nominal_direction (bool): Fix the yaw angle to the nominal
direction? Defaults to False
n_turbines (int): The number of turbines in the wind farm. Must be supplied
if fix_yaw_to_nominal_direction is True.

Returns:
numpy.ndarray: Expanded wind direction data as a 2D numpy array
Expand All @@ -572,6 +593,10 @@ def _expand_wind_directions(self, input_array, wd_sample_points):
if wd_sample_points[len(wd_sample_points) // 2] != 0:
raise ValueError("The middle element of wd_sample_points must be 0.")

# If fix_yaw_to_nominal_direction is True, n_turbines must be supplied
if fix_yaw_to_nominal_direction and n_turbines is None:
raise ValueError("The number of turbines in the wind farm must be supplied")

num_samples = len(wd_sample_points)
num_rows = input_array.shape[0]

Expand All @@ -589,6 +614,15 @@ def _expand_wind_directions(self, input_array, wd_sample_points):
output_array[start_idx:end_idx, 0] + wd_sample_points[i]
) % 360

# 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]
)

return output_array

def _get_unique_inputs(self, input_array):
Expand Down Expand Up @@ -644,7 +678,7 @@ def layout_x(self):
Returns:
np.array: Wind turbine x-coordinate.
"""
return self.core_interface.core.farm.layout_x
return self.fmodel_unexpanded.core.farm.layout_x

@property
def layout_y(self):
Expand All @@ -654,15 +688,26 @@ def layout_y(self):
Returns:
np.array: Wind turbine y-coordinate.
"""
return self.core_interface.core.farm.layout_y
return self.fmodel_unexpanded.core.farm.layout_y

@property
def core(self):
"""
Returns the core of the unexpanded model.

Returns:
Floris: The core of the unexpanded model.
"""
return self.fmodel_unexpanded.core


def map_turbine_powers_uncertain(
unique_turbine_powers,
map_to_expanded_inputs,
weights,
n_unexpanded,
n_sample_points,
n_turbines
unique_turbine_powers,
map_to_expanded_inputs,
weights,
n_unexpanded,
n_sample_points,
n_turbines,
):
"""Calculates the power at each turbine in the wind farm based on uncertainty weights.

Expand Down
35 changes: 34 additions & 1 deletion tests/uncertain_floris_model_integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,39 @@ def test_expand_wind_directions():
np.testing.assert_almost_equal(output_array[-1, 0], 14.0)


def test_expand_wind_directions_with_yaw_nom():
ufmodel = UncertainFlorisModel(configuration=YAML_INPUT)

# Assume 2 turbine
n_turbines = 2

# Assume n_findex = 2
input_array = np.array(
[[270.0, 8.0, 0.6, 0.0, 0.0, 0.0, 0.0], [270.0, 8.0, 0.6, 0.0, 2.0, 0.0, 0.0]]
)

# 3 sample points
wd_sample_points = [-3, 0, 3]

# Test correction operations
output_array = ufmodel._expand_wind_directions(input_array, wd_sample_points, True, n_turbines)

# Check the first direction
np.testing.assert_almost_equal(output_array[0, 0], 267)

# Check the first yaw
np.testing.assert_almost_equal(output_array[0, 4], -3)

# Rerun with fix_yaw_to_nominal_direction = False, and now the yaw should be 0
output_array = ufmodel._expand_wind_directions(input_array, wd_sample_points, False, n_turbines)

# Check the first direction
np.testing.assert_almost_equal(output_array[0, 0], 267)

# Check the first yaw
np.testing.assert_almost_equal(output_array[0, 4], 0)


def test_get_unique_inputs():
ufmodel = UncertainFlorisModel(configuration=YAML_INPUT)

Expand Down Expand Up @@ -131,8 +164,8 @@ def test_uncertain_floris_model():

np.testing.assert_allclose(np.sum(nom_powers * weights), unc_powers)

def test_uncertain_floris_model_setpoints():

def test_uncertain_floris_model_setpoints():
fmodel = FlorisModel(configuration=YAML_INPUT)
ufmodel = UncertainFlorisModel(configuration=YAML_INPUT, wd_sample_points=[-3, 0, 3], wd_std=3)

Expand Down
Loading