Turbine Operation Models#

Separate from the turbine models, which define the physical characterstics of the turbines, FLORIS allows users to specify how the turbine behaves in terms of producing power and thurst. We refer to different models for turbine behavior as "operation models". A key feature of operation models is the ability for users to specify control setpoints at which the operation model will be evaluated. For instance, some operation models allow users to specify yaw_angles, which alter the power being produced by the turbine along with it's thrust force on flow.

Operation models are specified by the operation_model key on the turbine yaml file, or by using the set_operation_model() method on FlorisModel. Each operation model available in FLORIS is described and demonstrated below. The simplest operation model is the "simple" operation model, which takes no control setpoints and simply evaluates the power and thrust coefficient curves for the turbine at the current wind condition. The default operation model is the "cosine-loss" operation model, which models the loss in power of a turbine under yaw misalignment using a cosine term with an exponent.

We first provide a quick demonstration of how to switch between different operation models. Then, each operation model available in FLORIS is described, along with its relevant control setpoints. We also describe the different parameters that must be specified in the turbine "power_thrust_table" dictionary in order to use that operation model.

Selecting the operation model#

There are two options for selecting the operation model:

  1. Manually changing the "operation_model" field of the turbine input yaml (see Turbine Input File Reference)

  2. Using set_operation_model() on an instantiated FlorisModel object.

The following code demonstrates the use of the second option.

from floris import FlorisModel
from floris import layout_visualization as layoutviz

fmodel = FlorisModel("../examples/inputs/gch.yaml")

# Look at layout
ax = layoutviz.plot_turbine_rotors(fmodel)
layoutviz.plot_turbine_labels(fmodel, ax=ax)
ax.set_xlabel("x [m]")
ax.set_ylabel("y [m]")

# Set simple operation model
fmodel.set_operation_model("simple")

# Evalaute the model and extract the power output
fmodel.run()
print("simple operation model powers [kW]: ", fmodel.get_turbine_powers() / 1000)

# Set the yaw angles (which the "simple" operation model does not use
# and change the operation model to "cosine-loss"
fmodel.set(yaw_angles=[[20., 0., 0.]])
fmodel.set_operation_model("cosine-loss")
ax = layoutviz.plot_turbine_rotors(fmodel)
layoutviz.plot_turbine_labels(fmodel, ax=ax)
ax.set_xlabel("x [m]")
ax.set_ylabel("y [m]")

# Evaluate again
fmodel.run()
powers_cosine_loss = fmodel.get_turbine_powers()
print("cosine-loss operation model powers [kW]: ", fmodel.get_turbine_powers() / 1000)
simple operation model powers [kW]:  [[1753.95445918  436.4427005   506.66815478]]
cosine-loss operation model powers [kW]:  [[1561.31837381  778.04338242  651.77709894]]
_images/23634896fb924d8c8c1186e04bacbfff72ae371de5fa2195eeb1a827813743cd.png _images/887f0e2b55a73b89a84d7600e3b37d669c5d7e7220c38152584af0e614e6c453.png

Operation model library#

Simple model#

User-level name: "simple"

Underlying class: SimpleTurbine

Required data on power_thrust_table:

  • ref_air_density (scalar)

  • ref_tilt (scalar)

  • wind_speed (list)

  • power (list)

  • thrust_coefficient (list)

The "simple" operation model describes the "normal" function of a wind turbine, as described by its power curve and thrust coefficient. It does not respond to any control setpoints, and is most often used as a baseline or for users wanting to evaluate wind farms in nominal operation.

Cosine loss model#

User-level name: "cosine-loss"

Underlying class: CosineLossTurbine

Required data on power_thrust_table:

  • ref_air_density (scalar)

  • ref_tilt (scalar)

  • wind_speed (list)

  • power (list)

  • thrust_coefficient (list)

  • cosine_loss_exponent_yaw (scalar)

  • cosine_loss_exponent_tilt (scalar)

The "cosine-loss" operation model describes the decrease in power and thrust produced by a wind turbine as it yaws (or tilts) away from the incoming wind. The thrust is reduced by a factor of \(\cos \gamma\), where \(\gamma\) is the yaw misalignment angle, while the power is reduced by a factor of \((\cos\gamma)^{p_P}\), where \(p_P\) is the cosine loss exponent, specified by cosine_loss_exponent_yaw (or cosine_loss_exponent_tilt for tilt angles). The power and thrust produced by the turbine thus vary as a function of the turbine's yaw angle, set using the yaw_angles argument to FlorisModel.set().

from floris import TimeSeries
import numpy as np
import matplotlib.pyplot as plt

# Set up the FlorisModel
fmodel.set_operation_model("cosine-loss")
fmodel.set(layout_x=[0.0], layout_y=[0.0])
fmodel.set(
    wind_data=TimeSeries(
        wind_speeds=np.ones(100) * 8.0,
        wind_directions=np.ones(100) * 270.0,
        turbulence_intensities=0.06
    )
)
fmodel.reset_operation()

# Sweep the yaw angles
yaw_angles = np.linspace(-25, 25, 100)
fmodel.set(yaw_angles=yaw_angles.reshape(-1,1))
fmodel.run()

powers = fmodel.get_turbine_powers()/1000

fig, ax = plt.subplots()
ax.plot(yaw_angles, powers)
ax.grid()
ax.set_xlabel("Yaw angle [deg]")
ax.set_ylabel("Power [kW]")
Text(0, 0.5, 'Power [kW]')
_images/165c42a72adb379e661c7a7944cdd8dd5c424a52d1acf17141f993999fcd380b.png

Simple derating model#

User-level name: "simple-derating"

Underlying class: SimpleDeratingTurbine

Required data on power_thrust_table:

  • ref_air_density (scalar)

  • ref_tilt (scalar)

  • wind_speed (list)

  • power (list)

  • thrust_coefficient (list)

The "simple-derating" operation model enables users to derate turbines by setting a new power rating. It does not require any extra parameters on the power_thrust_table, but adescribes the decrease in power and thrust produced by providing the power_setpoints argument to FlorisModel.set(). The default power rating for the turbine can be acheived by setting the appropriate entries of power_setpoints to None.

# Set up the FlorisModel
fmodel.set_operation_model("simple-derating")
fmodel.reset_operation()
wind_speeds = np.linspace(0, 30, 100)
fmodel.set(
    wind_data=TimeSeries(
        wind_speeds=wind_speeds,
        wind_directions=np.ones(100) * 270.0,
        turbulence_intensities=0.06
    )
)

fig, ax = plt.subplots()
for power_setpoint in [5.0, 4.0, 3.0, 2.0]:
    fmodel.set(power_setpoints=np.array([[power_setpoint*1e6]]*100))
    fmodel.run()
    powers = fmodel.get_turbine_powers()/1000
    ax.plot(wind_speeds, powers[:,0], label=f"Power setpoint (MW): {power_setpoint}")

ax.grid()
ax.legend()
ax.set_xlabel("Wind speed [m/s]")
ax.set_ylabel("Power [kW]")
/Users/msinner/floris3/floris/core/turbine/operation_models.py:367: RuntimeWarning: divide by zero encountered in divide
  power_fractions = power_setpoints / base_powers
/Users/msinner/floris3/floris/core/wake_deflection/gauss.py:323: RuntimeWarning: invalid value encountered in divide
  val = 2 * (avg_v - v_core) / (v_top + v_bottom)
/Users/msinner/floris3/floris/core/wake_deflection/gauss.py:158: RuntimeWarning: invalid value encountered in divide
  C0 = 1 - u0 / freestream_velocity
/Users/msinner/floris3/floris/core/wake_velocity/gauss.py:80: RuntimeWarning: invalid value encountered in divide
  sigma_z0 = rotor_diameter_i * 0.5 * np.sqrt(uR / (u_initial + u0))
Text(0, 0.5, 'Power [kW]')
_images/9facc726018877ff30b9530a9ba631cfdb7d3a64c4bc5eb2fef1017a0b0c5a37.png

Mixed operation model#

User-level name: "mixed"

Underlying class: MixedOperationTurbine

Required data on power_thrust_table:

  • ref_air_density (scalar)

  • ref_tilt (scalar)

  • wind_speed (list)

  • power (list)

  • thrust_coefficient (list)

  • cosine_loss_exponent_yaw (scalar)

  • cosine_loss_exponent_tilt (scalar)

The "mixed" operation model allows users to specify either yaw_angles (evaluated using the "cosine-loss" operation model) or power_setpoints (evaluated using the "simple-derating" operation model). That is, for each turbine, and at each findex, a non-zero yaw angle or a non-None power setpoint may be specified. However, specifying both a non-zero yaw angle and a finite power setpoint for the same turbine and at the same findex will produce an error.

fmodel.set_operation_model("mixed")
fmodel.set(layout_x=[0.0, 0.0], layout_y=[0.0, 500.0])
fmodel.reset_operation()
fmodel.set(
    wind_data=TimeSeries(
        wind_speeds=np.array([10.0]),
        wind_directions=np.array([270.0]),
        turbulence_intensities=0.06
    )
)
fmodel.set(
    yaw_angles=np.array([[20.0, 0.0]]),
    power_setpoints=np.array([[None, 2e6]])
)
fmodel.run()
print("Powers [kW]: ", fmodel.get_turbine_powers()/1000)
Powers [kW]:  [[3063.49046772 2000.        ]]

AWC model#

User-level name: "awc"

Underlying class: AWCTurbine

Required data on power_thrust_table:

  • ref_air_density (scalar)

  • ref_tilt (scalar)

  • wind_speed (list)

  • power (list)

  • thrust_coefficient (list)

  • helix_a (scalar)

  • helix_power_b (scalar)

  • helix_power_c (scalar)

  • helix_thrust_b (scalar)

  • helix_thrust_c (scalar)

The "awc" operation model allows for users to define active wake control strategies. These strategies use pitch control to actively enhance wake mixing and subsequently decrease wake velocity deficits. As a result, downstream turbines can increase their power production, with limited power loss for the controlled upstream turbine. The AWCTurbine class models this power loss at the turbine applying AWC. For each turbine, the user can define an AWC strategy to implement through the awc_modes array. Note that currently, only "baseline", i.e., no AWC, and "helix", i.e., the counterclockwise helix method have been implemented.

The user then defines the exact AWC implementation through setting the variable awc_amplitudes for each turbine. This variable defines the mean-to-peak amplitude of the sinusoidal AWC pitch excitation, i.e., for a turbine that under awc_modes = "baseline" has a constant pitch angle of 0 degrees, setting awc_amplitude = 2 results in a pitch signal varying from -2 to 2 degrees over the desired Strouhal frequency. This Strouhal frequency is not used as an input here, since it has minimal influence on turbine power production. Note that setting awc_amplitudes = 0 effectively disables AWC and is therefore the same as running a turbine at awc_modes = "baseline".

Each example turbine input file floris/turbine_library/*.yaml has its own helix_* parameter data. These parameters are determined by fitting data from OpenFAST simulations in region II to the following equation:

\[ P_\text{AWC} = P_\text{baseline} \cdot (1 - (b + c \cdot P_\text{baseline} ) \cdot A_\text{AWC}^a) \]

where \(a\) is "helix_a", \(b\) is "helix_power_b", \(c\) is "helix_power_c", and \(A_\text{AWC}\) is awc_amplitudes. The thrust coefficient follows the same equation, but with the respective thrust parameters. When AWC is turned on while \(P_\text{baseline} > P_\text{rated}\), a warning is given as the model is not yet tuned for region III.

The figure below shows the fit between the turbine power and thrust in OpenFAST helix AWC simulations (x) and FLORIS simulations (--) at different region II wind speeds for the NREL 5MW reference turbine.

fmodel.set_operation_model("awc")
fmodel.set(layout_x=[0.0, 0.0], layout_y=[0.0, 500.0])
fmodel.reset_operation()
fmodel.set(
    wind_data=TimeSeries(
        wind_speeds=np.array([10.0]),
        wind_directions=np.array([270.0]),
        turbulence_intensities=0.06
    )
)
fmodel.set(
    awc_modes=np.array(["helix", "baseline"]),
    awc_amplitudes=np.array([2.5, 0])
)
fmodel.run()
print("Powers [kW]: ", fmodel.get_turbine_powers()/1000)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[5], line 1
----> 1 fmodel.set_operation_model("awc")
      2 fmodel.set(layout_x=[0.0, 0.0], layout_y=[0.0, 500.0])
      3 fmodel.reset_operation()

File ~\projects\floris\floris\floris_model.py:1306, in FlorisModel.set_operation_model(self, operation_model)
   1304 turbine_type = self.core.farm.turbine_definitions[0]
   1305 turbine_type["operation_model"] = operation_model
-> 1306 self.set(turbine_type=[turbine_type])

File ~\projects\floris\floris\floris_model.py:347, in FlorisModel.set(self, wind_speeds, wind_directions, wind_shear, wind_veer, reference_wind_height, turbulence_intensities, air_density, layout_x, layout_y, turbine_type, turbine_library_path, solver_settings, heterogenous_inflow_config, wind_data, yaw_angles, power_setpoints, disable_turbines)
    345 _yaw_angles = self.core.farm.yaw_angles
    346 _power_setpoints = self.core.farm.power_setpoints
--> 347 self._reinitialize(
    348     wind_speeds=wind_speeds,
    349     wind_directions=wind_directions,
    350     wind_shear=wind_shear,
    351     wind_veer=wind_veer,
    352     reference_wind_height=reference_wind_height,
    353     turbulence_intensities=turbulence_intensities,
    354     air_density=air_density,
    355     layout_x=layout_x,
    356     layout_y=layout_y,
    357     turbine_type=turbine_type,
    358     turbine_library_path=turbine_library_path,
    359     solver_settings=solver_settings,
    360     heterogenous_inflow_config=heterogenous_inflow_config,
    361     wind_data=wind_data,
    362 )
    364 # If the yaw angles or power setpoints are not the default, set them back to the
    365 # previous setting
    366 if not (_yaw_angles == 0).all():

File ~\projects\floris\floris\floris_model.py:230, in FlorisModel._reinitialize(self, wind_speeds, wind_directions, wind_shear, wind_veer, reference_wind_height, turbulence_intensities, air_density, layout_x, layout_y, turbine_type, turbine_library_path, solver_settings, heterogenous_inflow_config, wind_data)
    227 floris_dict["farm"] = farm_dict
    229 # Create a new instance of floris and attach to self
--> 230 self.core = Core.from_dict(floris_dict)

File ~\projects\floris\floris\type_dec.py:226, in FromDictMixin.from_dict(cls, data)
    221 if undefined:
    222     raise AttributeError(
    223         f"The class definition for {cls.__name__} "
    224         f"is missing the following inputs: {undefined}"
    225     )
--> 226 return cls(**kwargs)

File <attrs generated init floris.core.core.Core>:13, in __init__(self, logging, solver, wake, farm, flow_field, name, description, floris_version)
     11 _setattr('description', __attr_converter_description(description))
     12 _setattr('floris_version', __attr_converter_floris_version(floris_version))
---> 13 self.__attrs_post_init__()

File ~\projects\floris\floris\core\core.py:75, in Core.__attrs_post_init__(self)
     69 logging_manager.configure_file_log(
     70     self.logging["file"]["enable"],
     71     self.logging["file"]["level"],
     72 )
     74 # Initialize farm quantities that depend on other objects
---> 75 self.farm.construct_turbine_map()
     76 self.farm.construct_turbine_thrust_coefficient_functions()
     77 self.farm.construct_turbine_axial_induction_functions()

File ~\projects\floris\floris\core\farm.py:262, in Farm.construct_turbine_map(self)
    261 def construct_turbine_map(self):
--> 262     self.turbine_map = [Turbine.from_dict(turb) for turb in self.turbine_definitions]

File ~\projects\floris\floris\type_dec.py:226, in FromDictMixin.from_dict(cls, data)
    221 if undefined:
    222     raise AttributeError(
    223         f"The class definition for {cls.__name__} "
    224         f"is missing the following inputs: {undefined}"
    225     )
--> 226 return cls(**kwargs)

File <attrs generated init floris.core.turbine.turbine.Turbine>:24, in __init__(self, turbine_type, rotor_diameter, hub_height, TSR, power_thrust_table, operation_model, correct_cp_ct_for_tilt, floating_tilt_table, multi_dimensional_cp_ct, power_thrust_data_file, turbine_library_path)
     22     __attr_validator_floating_tilt_table(self, __attr_floating_tilt_table, self.floating_tilt_table)
     23     __attr_validator_turbine_library_path(self, __attr_turbine_library_path, self.turbine_library_path)
---> 24 self.__attrs_post_init__()

File ~\projects\floris\floris\core\turbine\turbine.py:461, in Turbine.__attrs_post_init__(self)
    460 def __attrs_post_init__(self) -> None:
--> 461     self._initialize_power_thrust_functions()
    462     self.__post_init__()

File ~\projects\floris\floris\core\turbine\turbine.py:472, in Turbine._initialize_power_thrust_functions(self)
    471 def _initialize_power_thrust_functions(self) -> None:
--> 472     turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model]
    473     self.thrust_coefficient_function = turbine_function_model.thrust_coefficient
    474     self.axial_induction_function = turbine_function_model.axial_induction

KeyError: 'awc'