Skip to content

Commit

Permalink
Expand functionalities of UncertaintyInterface and fix no_wake compat…
Browse files Browse the repository at this point in the history
…ibility (#428)
  • Loading branch information
Bart Doekemeijer authored May 26, 2022
1 parent fa9f63e commit 0697c37
Showing 1 changed file with 133 additions and 4 deletions.
137 changes: 133 additions & 4 deletions floris/tools/uncertainty_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ def __init__(
fix_yaw_in_relative_frame=fix_yaw_in_relative_frame,
)

# Add a _no_wake switch to keep track of calculate_wake/calculate_no_wake
self._no_wake = False

# Private methods

def _generate_pdfs_from_dict(self):
Expand Down Expand Up @@ -364,8 +367,24 @@ def calculate_wake(self, yaw_angles=None):
yaw_angles: NDArrayFloat | list[float] | None = None,
"""
self._reassign_yaw_angles(yaw_angles)
self._no_wake = False

def calculate_no_wake(self, yaw_angles=None):
"""Replaces the 'calculate_no_wake' function in the FlorisInterface
object. Fundamentally, this function only overwrites the nominal
yaw angles in the FlorisInterface object. The actual wake calculations
are performed once 'get_turbine_powers' or 'get_farm_powers' is
called. However, to allow users to directly replace a FlorisInterface
object with this UncertaintyInterface object, this function is
required.
def get_turbine_powers(self, no_wake=False):
Args:
yaw_angles: NDArrayFloat | list[float] | None = None,
"""
self._reassign_yaw_angles(yaw_angles)
self._no_wake = True

def get_turbine_powers(self):
"""Calculates the probability-weighted power production of each
turbine in the wind farm.
Expand Down Expand Up @@ -419,7 +438,7 @@ def get_turbine_powers(self, no_wake=False):

# Evaluate floris for minimal probablistic set
self.fi.reinitialize(wind_directions=wd_array_probablistic_min)
if no_wake:
if self._no_wake:
self.fi.calculate_no_wake(yaw_angles=yaw_angles_probablistic_min)
else:
self.fi.calculate_wake(yaw_angles=yaw_angles_probablistic_min)
Expand All @@ -443,7 +462,7 @@ def get_turbine_powers(self, no_wake=False):
# Now apply probability distribution weighing to get turbine powers
return np.sum(wd_weighing * power_probablistic, axis=0)

def get_farm_power(self, no_wake=False):
def get_farm_power(self):
"""Calculates the probability-weighted power production of the
collective of all turbines in the farm, for each wind direction
and wind speed specified.
Expand All @@ -458,9 +477,119 @@ def get_farm_power(self, no_wake=False):
NDArrayFloat: Expectation of power production of the wind farm.
This array has the shape (num_wind_directions, num_wind_speeds).
"""
turbine_powers = self.get_turbine_powers(no_wake=no_wake)
turbine_powers = self.get_turbine_powers()
return np.sum(turbine_powers, axis=2)

def get_farm_AEP(
self,
freq,
cut_in_wind_speed=0.001,
cut_out_wind_speed=None,
yaw_angles=None,
no_wake=False,
) -> 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_wind_directions,
n_wind_speeds) 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.
yaw_angles (NDArrayFloat | list[float] | None, optional):
The relative turbine yaw angles in degrees. If None is
specified, will assume that the turbine yaw angles are all
zero degrees for all conditions. Defaults to None.
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 not (
(np.shape(freq)[0] == self.floris.flow_field.n_wind_directions)
& (np.shape(freq)[1] == self.floris.flow_field.n_wind_speeds)
& (len(np.shape(freq)) == 2)
):
raise UserWarning(
"'freq' should be a two-dimensional array with dimensions"
+ " (n_wind_directions, n_wind_speeds)."
)

# 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.fi.floris.flow_field.wind_speeds, copy=True)
farm_power = np.zeros(
(self.fi.floris.flow_field.n_wind_directions, len(wind_speeds))
)

# Determine which wind speeds we must evaluate in floris
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]
yaw_angles_subset = None
if yaw_angles is not None:
yaw_angles_subset = yaw_angles[:, conditions_to_evaluate]
self.reinitialize(wind_speeds=wind_speeds_subset)
if no_wake:
self.calculate_no_wake(yaw_angles=yaw_angles_subset)
else:
self.calculate_wake(yaw_angles=yaw_angles_subset)
farm_power[:, conditions_to_evaluate] = self.get_farm_power()

# 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.reinitialize(wind_speeds=wind_speeds)

return aep

def assign_hub_height_to_ref_height(self):
return self.fi.assign_hub_height_to_ref_height()

def get_turbine_layout(self, z=False):
return self.fi.get_turbine_layout(z=z)

def get_turbine_Cts(self):
return self.fi.get_turbine_Cts()

def get_turbine_ais(self):
return self.fi.get_turbine_ais()

def get_turbine_average_velocities(self):
return self.fi.get_turbine_average_velocities()

# Define getter functions that just pass information from FlorisInterface
@property
def floris(self):
Expand Down

0 comments on commit 0697c37

Please sign in to comment.