{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"id": "ac224ce9-bd4f-4f5c-88b7-f0e9e49ee498",
"metadata": {},
"source": [
"# Turbine Operation Models\n",
"\n",
"Separate from the turbine models, which define the physical characterstics of the turbines, FLORIS\n",
"allows users to specify how the turbine behaves in terms of producing power and thurst. We refer to \n",
"different models for turbine behavior as \"operation models\". A key feature of operation models is\n",
"the ability for users to specify control setpoints at which the operation model will be evaluated. \n",
"For instance, some operation models allow users to specify `yaw_angles`, which alter the power \n",
"being produced by the turbine along with it's thrust force on flow.\n",
"\n",
"Operation models are specified by the `operation_model` key on the turbine yaml file, or by using\n",
"the `set_operation_model()` method on `FlorisModel`. Each operation model available in FLORIS is\n",
"described and demonstrated below. The simplest operation model is the `\"simple\"` operation model,\n",
"which takes no control setpoints and simply evaluates the power and thrust coefficient curves for \n",
"the turbine at the current wind condition. The default operation model is the `\"cosine-loss\"`\n",
"operation model, which models the loss in power of a turbine under yaw misalignment using a cosine\n",
"term with an exponent.\n",
"\n",
"We first provide a quick demonstration of how to switch between different operation models. Then, \n",
"each operation model available in FLORIS is described, along with its relevant control setpoints.\n",
"We also describe the different parameters that must be specified in the turbine \n",
"`\"power_thrust_table\"` dictionary in order to use that operation model."
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "71788b47-6641-4080-bb3f-eb799d969e0b",
"metadata": {},
"source": [
"## Selecting the operation model\n",
"\n",
"There are two options for selecting the operation model:\n",
"1. Manually changing the `\"operation_model\"` field of the turbine input yaml \n",
"(see [Turbine Input File Reference](input_reference_turbine))\n",
"\n",
"2. Using `set_operation_model()` on an instantiated `FlorisModel` object.\n",
"\n",
"The following code demonstrates the use of the second option."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2275840e-48a3-41d2-ace9-fad05da0dc02",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from floris import FlorisModel\n",
"from floris import layout_visualization as layoutviz\n",
"\n",
"fmodel = FlorisModel(\"../examples/inputs/gch.yaml\")\n",
"\n",
"# Look at layout\n",
"ax = layoutviz.plot_turbine_rotors(fmodel)\n",
"layoutviz.plot_turbine_labels(fmodel, ax=ax)\n",
"ax.set_xlabel(\"x [m]\")\n",
"ax.set_ylabel(\"y [m]\")\n",
"\n",
"# Set simple operation model\n",
"fmodel.set_operation_model(\"simple\")\n",
"\n",
"# Evalaute the model and extract the power output\n",
"fmodel.run()\n",
"print(\"simple operation model powers [kW]: \", fmodel.get_turbine_powers() / 1000)\n",
"\n",
"# Set the yaw angles (which the \"simple\" operation model does not use\n",
"# and change the operation model to \"cosine-loss\"\n",
"fmodel.set(yaw_angles=[[20., 0., 0.]])\n",
"fmodel.set_operation_model(\"cosine-loss\")\n",
"ax = layoutviz.plot_turbine_rotors(fmodel)\n",
"layoutviz.plot_turbine_labels(fmodel, ax=ax)\n",
"ax.set_xlabel(\"x [m]\")\n",
"ax.set_ylabel(\"y [m]\")\n",
"\n",
"# Evaluate again\n",
"fmodel.run()\n",
"powers_cosine_loss = fmodel.get_turbine_powers()\n",
"print(\"cosine-loss operation model powers [kW]: \", fmodel.get_turbine_powers() / 1000)\n"
]
},
{
"cell_type": "markdown",
"id": "5d22f376",
"metadata": {},
"source": [
"## Operation model library"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "f2576e8a-47ee-48b5-8707-aca0dc76929c",
"metadata": {},
"source": [
"### Simple model\n",
"User-level name: `\"simple\"`\n",
"\n",
"Underlying class: `SimpleTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"\n",
"The `\"simple\"` operation model describes the \"normal\" function of a wind turbine, as described by\n",
"its power curve and thrust coefficient. It does not respond to any control setpoints, and is most \n",
"often used as a baseline or for users wanting to evaluate wind farms in nominal operation."
]
},
{
"cell_type": "markdown",
"id": "ced1e091",
"metadata": {},
"source": [
"### Cosine loss model\n",
"User-level name: `\"cosine-loss\"`\n",
"\n",
"Underlying class: `CosineLossTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"- `cosine_loss_exponent_yaw` (scalar)\n",
"- `cosine_loss_exponent_tilt` (scalar)\n",
"\n",
"The `\"cosine-loss\"` operation model describes the decrease in power and thrust produced by a \n",
"wind turbine as it yaws (or tilts) away from the incoming wind. The thrust is reduced by a factor of \n",
"$\\cos \\gamma$, where $\\gamma$ is the yaw misalignment angle, while the power is reduced by a factor \n",
"of $(\\cos\\gamma)^{p_P}$, where $p_P$ is the cosine loss exponent, specified by `cosine_loss_exponent_yaw`\n",
"(or `cosine_loss_exponent_tilt` for tilt angles). The power and thrust produced by the turbine\n",
"thus vary as a function of the turbine's yaw angle, set using the `yaw_angles` argument to \n",
"`FlorisModel.set()`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b9a5f00a-0ead-4759-b911-3a1161e55791",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"from floris import TimeSeries\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# Set up the FlorisModel\n",
"fmodel.set_operation_model(\"cosine-loss\")\n",
"fmodel.set(layout_x=[0.0], layout_y=[0.0])\n",
"fmodel.set(\n",
" wind_data=TimeSeries(\n",
" wind_speeds=np.ones(100) * 8.0,\n",
" wind_directions=np.ones(100) * 270.0,\n",
" turbulence_intensities=0.06\n",
" )\n",
")\n",
"fmodel.reset_operation()\n",
"\n",
"# Sweep the yaw angles\n",
"yaw_angles = np.linspace(-25, 25, 100)\n",
"fmodel.set(yaw_angles=yaw_angles.reshape(-1,1))\n",
"fmodel.run()\n",
"\n",
"powers = fmodel.get_turbine_powers()/1000\n",
"\n",
"fig, ax = plt.subplots()\n",
"ax.plot(yaw_angles, powers)\n",
"ax.grid()\n",
"ax.set_xlabel(\"Yaw angle [deg]\")\n",
"ax.set_ylabel(\"Power [kW]\")"
]
},
{
"cell_type": "markdown",
"id": "019abca6",
"metadata": {},
"source": [
"### Simple derating model\n",
"User-level name: `\"simple-derating\"`\n",
"\n",
"Underlying class: `SimpleDeratingTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"\n",
"The `\"simple-derating\"` operation model enables users to derate turbines by setting a new power \n",
"rating. It does not require any extra parameters on the `power_thrust_table`, but adescribes the \n",
"decrease in power and thrust produced by providing the `power_setpoints` argument to\n",
"`FlorisModel.set()`. The default power rating for the turbine can be acheived by setting the\n",
"appropriate entries of `power_setpoints` to `None`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "722be425-9231-451a-bd84-7824db6a5098",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Set up the FlorisModel\n",
"fmodel.set_operation_model(\"simple-derating\")\n",
"fmodel.reset_operation()\n",
"wind_speeds = np.linspace(0, 30, 100)\n",
"fmodel.set(\n",
" wind_data=TimeSeries(\n",
" wind_speeds=wind_speeds,\n",
" wind_directions=np.ones(100) * 270.0,\n",
" turbulence_intensities=0.06\n",
" )\n",
")\n",
"\n",
"fig, ax = plt.subplots()\n",
"for power_setpoint in [5.0, 4.0, 3.0, 2.0]:\n",
" fmodel.set(power_setpoints=np.array([[power_setpoint*1e6]]*100))\n",
" fmodel.run()\n",
" powers = fmodel.get_turbine_powers()/1000\n",
" ax.plot(wind_speeds, powers[:,0], label=f\"Power setpoint (MW): {power_setpoint}\")\n",
"\n",
"ax.grid()\n",
"ax.legend()\n",
"ax.set_xlabel(\"Wind speed [m/s]\")\n",
"ax.set_ylabel(\"Power [kW]\")"
]
},
{
"cell_type": "markdown",
"id": "4caca5fa",
"metadata": {},
"source": [
"### Mixed operation model\n",
"User-level name: `\"mixed\"`\n",
"\n",
"Underlying class: `MixedOperationTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"- `cosine_loss_exponent_yaw` (scalar)\n",
"- `cosine_loss_exponent_tilt` (scalar)\n",
"\n",
"The `\"mixed\"` operation model allows users to specify _either_ `yaw_angles` (evaluated using the \n",
"`\"cosine-loss\"` operation model) _or_ `power_setpoints` (evaluated using the `\"simple-derating\"`\n",
"operation model). That is, for each turbine, and at each `findex`, a non-zero yaw angle or a \n",
"non-`None` power setpoint may be specified. However, specifying both a non-zero yaw angle and a \n",
"finite power setpoint for the same turbine and at the same `findex` will produce an error."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5e3cda81",
"metadata": {},
"outputs": [],
"source": [
"fmodel.set_operation_model(\"mixed\")\n",
"fmodel.set(layout_x=[0.0, 0.0], layout_y=[0.0, 500.0])\n",
"fmodel.reset_operation()\n",
"fmodel.set(\n",
" wind_data=TimeSeries(\n",
" wind_speeds=np.array([10.0]),\n",
" wind_directions=np.array([270.0]),\n",
" turbulence_intensities=0.06\n",
" )\n",
")\n",
"fmodel.set(\n",
" yaw_angles=np.array([[20.0, 0.0]]),\n",
" power_setpoints=np.array([[None, 2e6]])\n",
")\n",
"fmodel.run()\n",
"print(\"Powers [kW]: \", fmodel.get_turbine_powers()/1000)"
]
},
{
"cell_type": "markdown",
"id": "c036feda",
"metadata": {},
"source": [
"### AWC model\n",
"\n",
"User-level name: `\"awc\"`\n",
"\n",
"Underlying class: `AWCTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"- `helix_a` (scalar)\n",
"- `helix_power_b` (scalar)\n",
"- `helix_power_c` (scalar)\n",
"- `helix_thrust_b` (scalar)\n",
"- `helix_thrust_c` (scalar)\n",
"\n",
"The `\"awc\"` operation model allows for users to define _active wake control_ strategies. These strategies \n",
"use pitch control to actively enhance wake mixing and subsequently decrease wake velocity deficits. As a \n",
"result, downstream turbines can increase their power production, with limited power loss for the controlled \n",
"upstream turbine. The `AWCTurbine` class models this power loss at the turbine applying AWC. For each \n",
"turbine, the user can define an AWC strategy to implement through the `awc_modes` array. Note that currently, \n",
"only `\"baseline\"`, i.e., no AWC, and `\"helix\"`, i.e., the \n",
"[counterclockwise helix method](https://doi.org/10.1002/we.2513) have been implemented. \n",
"\n",
"The user then defines the exact AWC implementation through setting the variable `awc_amplitudes` for \n",
"each turbine. This variable defines the mean-to-peak amplitude of the sinusoidal AWC pitch excitation,\n",
"i.e., for a turbine that under `awc_modes = \"baseline\"` has a constant pitch angle of 0 degrees, setting \n",
"`awc_amplitude = 2` results in a pitch signal varying from -2 to 2 degrees over the desired Strouhal\n",
"frequency. This Strouhal frequency is not used as an input here, since it has minimal influence on turbine \n",
"power production. Note that setting `awc_amplitudes = 0` effectively disables AWC and is therefore the same \n",
"as running a turbine at `awc_modes = \"baseline\"`.\n",
"\n",
"Each example turbine input file `floris/turbine_library/*.yaml` has its own `helix_*` parameter data. These \n",
"parameters are determined by fitting data from `OpenFAST` simulations in region II to the following equation:\n",
"\n",
"$$\n",
" P_\\text{AWC} = P_\\text{baseline} \\cdot (1 - (b + c \\cdot P_\\text{baseline} ) \\cdot A_\\text{AWC}^a)\n",
"$$\n",
"\n",
"where $a$ is `\"helix_a\"`, $b$ is `\"helix_power_b\"`, $c$ is `\"helix_power_c\"`, and $A_\\text{AWC}$ is `awc_amplitudes`. \n",
"The thrust coefficient follows the same equation, but with the respective thrust parameters. When AWC is \n",
"turned on while $P_\\text{baseline} > P_\\text{rated}$, a warning is given as the model is not yet tuned for region III.\n",
"\n",
"The figure below shows the fit between the turbine power and thrust in OpenFAST helix AWC simulations (x) \n",
"and FLORIS simulations (--) at different region II wind speeds for the NREL 5MW reference turbine.\n",
"\n",
"\n",
"![](./powerthrust_helix.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "40e9bcda",
"metadata": {},
"outputs": [],
"source": [
"fmodel = FlorisModel(\"../examples/inputs/emgauss_helix.yaml\")\n",
"fmodel.set_operation_model(\"awc\")\n",
"fmodel.set(layout_x=[0.0, 0.0], layout_y=[0.0, 500.0])\n",
"fmodel.reset_operation()\n",
"fmodel.set(\n",
" wind_speeds=np.array([8.0]),\n",
" wind_directions=np.array([270.0]),\n",
" turbulence_intensities=np.array([0.06])\n",
")\n",
"fmodel.set(\n",
" awc_modes=np.array([[\"helix\", \"baseline\"]]),\n",
" awc_amplitudes=np.array([[2.5, 0]])\n",
")\n",
"fmodel.run()\n",
"print(\"Powers [kW]: \", fmodel.get_turbine_powers()/1000)"
]
},
{
"cell_type": "markdown",
"id": "25f9c86c",
"metadata": {},
"source": [
"### Peak shaving model\n",
"\n",
"User-level name: `\"peak-shaving\"`\n",
"\n",
"Underlying class: `PeakShavingTurbine`\n",
"\n",
"Required data on `power_thrust_table`:\n",
"- `ref_air_density` (scalar)\n",
"- `ref_tilt` (scalar)\n",
"- `wind_speed` (list)\n",
"- `power` (list)\n",
"- `thrust_coefficient` (list)\n",
"- `peak_shaving_fraction` (scalar)\n",
"- `peak_shaving_TI_threshold` (scalar)\n",
"\n",
"The `\"peak-shaving\"` operation model allows users to implement peak shaving, where the thrust\n",
"of the wind turbine is reduced from the nominal curve near rated to reduce unwanted structural\n",
"loading. Peak shaving here is implemented here by reducing the thrust by a fixed fraction from\n",
"the peak thrust on the nominal thrust curve, as specified by `peak_shaving_fraction`.This only\n",
"affects wind speeds near the peak in the thrust\n",
"curve (usually near rated wind speed), as thrust values away from the peak will be below the\n",
"fraction regardless. Further, peak shaving is only applied if the turbulence intensity experienced\n",
"by the turbine meets the `peak_shaving_TI_threshold`. To apply peak shaving in all wind conditions,\n",
"`peak_shaving_TI_threshold` may be set to zero.\n",
"\n",
"When the turbine is peak shaving to reduce thrust, the power output is updated accordingly. Letting\n",
"$C_{T}$ represent the thrust coefficient when peak shaving (at given wind speed), and $C_{T}'$\n",
"represent the thrust coefficient that the turbine would be operating at under nominal control, then\n",
"the power $P$ due to peak shaving (compared to the power $P'$ available under nominal control) is \n",
"computed (based on actuator disk theory) as\n",
"\n",
"$$ P = \\frac{C_T (1 - a)}{C_T' (1 - a')} P'$$\n",
"\n",
"where $a$ (respectively, $a'$) is the axial induction factor corresponding to $C_T$\n",
"(respectively, $C_T'$), computed using the usual relationship from actuator disk theory,\n",
"i.e. the lesser solution to $C_T=4a(1-a)$.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1eff05f3",
"metadata": {},
"outputs": [],
"source": [
"# Set up the FlorisModel\n",
"fmodel = FlorisModel(\"../examples/inputs/gch.yaml\")\n",
"fmodel.set(\n",
" layout_x=[0.0], layout_y=[0.0],\n",
" wind_data=TimeSeries(\n",
" wind_speeds=wind_speeds,\n",
" wind_directions=np.ones(100) * 270.0,\n",
" turbulence_intensities=0.2 # Higher than threshold value of 0.1\n",
" )\n",
")\n",
"fmodel.reset_operation()\n",
"fmodel.set_operation_model(\"simple\")\n",
"fmodel.run()\n",
"powers_base = fmodel.get_turbine_powers()/1000\n",
"thrust_coefficients_base = fmodel.get_turbine_thrust_coefficients()\n",
"fmodel.set_operation_model(\"peak-shaving\")\n",
"fmodel.run()\n",
"powers_peak_shaving = fmodel.get_turbine_powers()/1000\n",
"thrust_coefficients_peak_shaving = fmodel.get_turbine_thrust_coefficients()\n",
"\n",
"fig, ax = plt.subplots(2,1,sharex=True)\n",
"ax[0].plot(wind_speeds, thrust_coefficients_base, label=\"Without peak shaving\", color=\"black\")\n",
"ax[0].plot(wind_speeds, thrust_coefficients_peak_shaving, label=\"With peak shaving\", color=\"C0\")\n",
"ax[1].plot(wind_speeds, powers_base, label=\"Without peak shaving\", color=\"black\")\n",
"ax[1].plot(wind_speeds, powers_peak_shaving, label=\"With peak shaving\", color=\"C0\")\n",
"\n",
"ax[1].grid()\n",
"ax[0].grid()\n",
"ax[0].legend()\n",
"ax[0].set_ylabel(\"Thrust coefficient [-]\")\n",
"ax[1].set_xlabel(\"Wind speed [m/s]\")\n",
"ax[1].set_ylabel(\"Power [kW]\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92912bf7",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}