Skip to content
Draft
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
394 changes: 394 additions & 0 deletions docs/user_defined_operation_models.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,394 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ba9ae6ce",
"metadata": {},
"source": [
"# Turbine Operation Models\n",
"\n",
"Beginning in v4.x, FLORIS supports user-defined turbine operation models that can be passed directly into FLORIS using `fmodel.set_operation_model`. A user-defined operation model may be a dynamic or static class. If the operation model is a dynamic class, it must conform to the `attrs` package for declaring attributes. Additionally all user-defined operation models should inherit from the abstract parent class `BaseOperationModel`, available in FLORIS.\n",
"\n",
"All operation models must implement the following \"fundamental\" methods:\n",
"- `power`: computes the power output of the turbine in Watts\n",
"- `thrust_coefficient`: computes the dimensionless thrust coefficient of the turbine\n",
"- `axial_induction`: computes the dimensionless axial induction factor of the turbine\n",
"\n",
"Operation models may then implement additional methods as needed.\n",
"\n",
"The following arguments are passed to the operation model fundamental methods at runtime:\n",
"\n",
"| Argument | Data type | Description |\n",
"|----------|-----------|----------|\n",
"| `power_thrust_table` | `dict` | Dictionary of model parameters defined on the turbine input yaml |\n",
"| `velocities` | `NDArrayFloat` | Array of inflow velocities (in m/s) to each turbine grid point, dimensions `(n_findex, n_turbines, n_grid, n_grid)` |\n",
"| `turbulence_intensities` | `NDArrayFloat` | Array of inflow turbulence intensities (as decimal values) to each turbine, dimensions `(n_findex, n_turbines, 1, 1)` |\n",
"| `air_density` | `float` | Ambient air density in kg/m^3 |\n",
"| `yaw_angles` | `NDArrayFloat` | Array of turbine yaw angles (in degrees, as misalignments from the inflow wind direction), dimensions `(n_findex, n_turbines)` |\n",
"| `tilt_angles` | `NDArrayFloat` | Array of turbine absolute [CHECK] tilt angles (in degrees, positive means tilted backwards), dimensions `(n_findex, n_turbines)` |\n",
"| `power_setpoints` | `NDArrayFloat` | Array of turbine power setpoints (in Watts), dimensions `(n_findex, n_turbines)` |\n",
"| `awc_modes` | `NDArrayStr` | Array of strings specifying the AWC mode for each turbine, dimensions `(n_findex, n_turbines)` |\n",
"| `awc_amplitudes` | `NDArrayFloat` | Array of AWC amplitudes (in degrees) for each turbine, dimensions `(n_findex, n_turbines)` |\n",
"| `tilt_interp` | `interpolator` | Scipy 1D interpolator to find the (floating) tilt angle as a function of wind speed |\n",
"| `average_method` | `string` | Averaging method for combining velocities over the turbine grid points |\n",
"| `cubature_weights` | `NDArrayFloat` | Weights for cubature grid computation of rotor-effective velocity, dimensions `(1, n_grid x n_grid)`|\n",
"| `correct_cp_ct_for_tilt` | `NDArrayInt` | Flag for correcting power and thrust curves to account for platform tilt, dimensions `(n_findex, n_turbines)` |\n",
"| `**_` | -- | Catch-all for unused arguments |\n",
"\n",
"Not all of these arguments must be used or defined as arguments by the user, as long as the final argument be `**_` to allow for unused arguments.\n",
"\n",
"Each of the fundamental methods must return an array of floats (`NDArrayFloat`) with dimensions `(n_findex, n_turbines)`, representing the compute power, thrust coefficient, or axial induction factor for each turbine at each flow condition index."
]
},
{
"cell_type": "markdown",
"id": "aefe4f59",
"metadata": {},
"source": [
"### Static example\n",
"\n",
"We begin with a very simple example that will produce a constant power, thrust coefficient, and axial induction factor regardless of the inputs. We are using a static class for this example; this class does not need to be instantiated and has no attributes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d751aa3c",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"from attrs import define, field\n",
"from floris.type_dec import floris_float_type, NDArrayFloat\n",
"from floris.core.turbine.operation_models import BaseOperationModel\n",
"\n",
"@define\n",
"class ConstantValueTurbine(BaseOperationModel):\n",
" \"\"\"\n",
" A simple turbine operation model that returns constant values for power,\n",
" thrust coefficient, and axial induction factor regardless of input conditions.\n",
" \"\"\"\n",
" @staticmethod\n",
" def power(\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Constant power of 500 kW, in correct shape (n_findex, n_turbines)\n",
" return 500000.0 * np.ones(velocities.shape[0:2], dtype=floris_float_type)\n",
"\n",
" @staticmethod\n",
" def thrust_coefficient(\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Return thrust coefficient based on actuator disk theory\n",
" # Because the class is static, we can call the axial_induction method directly\n",
" a = ConstantValueTurbine.axial_induction(velocities)\n",
" return 4 * a * (1 - a)\n",
"\n",
" @staticmethod\n",
" def axial_induction(\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Constant axial induction factor of 0.3\n",
" return 0.3 * np.ones(velocities.shape[0:2], dtype=floris_float_type)"
]
},
{
"cell_type": "markdown",
"id": "29ea6830",
"metadata": {},
"source": [
"Let's now use this constant operation model in FLORIS."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b74802c2",
"metadata": {},
"outputs": [],
"source": [
"from floris import FlorisModel, TimeSeries\n",
"\n",
"fmodel = FlorisModel(\"defaults\")\n",
"time_series = TimeSeries(\n",
" wind_directions=np.array([270.0, 270.0, 280.0]),\n",
" wind_speeds=np.array([8.0, 10.0, 12.0]),\n",
" turbulence_intensities=np.array([0.06, 0.06, 0.06]),\n",
")\n",
"fmodel.set(\n",
" layout_x = [0.0, 500.0],\n",
" layout_y = [0.0, 0.0],\n",
" wind_data=time_series,\n",
")\n",
"fmodel.set_operation_model(ConstantValueTurbine)\n",
"\n",
"fmodel.run()\n",
"\n",
"print(\"Powers [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n",
"print(\"Thrust coefficients [-]:\\n\", fmodel.get_turbine_thrust_coefficients(), \"\\n\")\n",
"print(\"Axial induction factors [-]:\\n\", fmodel.get_turbine_axial_induction_factors(), \"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "aa2b73b0",
"metadata": {},
"source": [
"## Dynamic example\n",
"\n",
"Now, we will create an operation model that allows the user to set attributes at instantiation. In this example, we will create an operation model that allows the user to set constant power, thrust coefficient, and axial induction factor values at instantiation. These values will then be returned by the fundamental methods regardless of the inputs. We use the `attrs` package to define attributes of the class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a7dbaf71",
"metadata": {},
"outputs": [],
"source": [
"@define\n",
"class DynamicValueTurbine(BaseOperationModel):\n",
" \"\"\"\n",
" A simple turbine operation model that returns constant values for power,\n",
" thrust coefficient, and axial induction factor regardless of input conditions,\n",
" based on user-defined attributes.\n",
" \"\"\"\n",
" power_value = field(init=True, default=600000.0, type=floris_float_type)\n",
" axial_induction_value = field(init=True, default=0.2, type=floris_float_type)\n",
" def power(\n",
" self,\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Constant power of 500 kW, in correct shape (n_findex, n_turbines)\n",
" return self.power_value * np.ones(velocities.shape[0:2], dtype=floris_float_type)\n",
"\n",
" def thrust_coefficient(\n",
" self,\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Return thrust coefficient based on actuator disk theory\n",
" # Because the class is static, we can call the axial_induction method directly\n",
" a = self.axial_induction(velocities)\n",
" return 4 * a * (1 - a)\n",
"\n",
" def axial_induction(\n",
" self,\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" # Constant axial induction factor of 0.3\n",
" return self.axial_induction_value * np.ones(velocities.shape[0:2], dtype=floris_float_type)"
]
},
{
"cell_type": "markdown",
"id": "f9ef3c9a",
"metadata": {},
"source": [
"To use this class, we must first instantiate it. If we instantiate it without any arguments, the default values will be used. Otherwise, we can pass in our desired constant values."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "93f1e99f",
"metadata": {},
"outputs": [],
"source": [
"turbine_operation_model = DynamicValueTurbine()\n",
"fmodel.set_operation_model(turbine_operation_model)\n",
"fmodel.run()\n",
"\n",
"print(\"Powers [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n",
"print(\"Thrust coefficients [-]:\\n\", fmodel.get_turbine_thrust_coefficients(), \"\\n\")\n",
"print(\"Axial induction factors [-]:\\n\", fmodel.get_turbine_axial_induction_factors(), \"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eeb38ea7",
"metadata": {},
"outputs": [],
"source": [
"turbine_operation_model = DynamicValueTurbine(power_value=750000.0, axial_induction_value=0.25)\n",
"fmodel.set_operation_model(turbine_operation_model)\n",
"fmodel.run()\n",
"\n",
"print(\"Powers [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n",
"print(\"Thrust coefficients [-]:\\n\", fmodel.get_turbine_thrust_coefficients(), \"\\n\")\n",
"print(\"Axial induction factors [-]:\\n\", fmodel.get_turbine_axial_induction_factors(), \"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "08be8e42",
"metadata": {},
"source": [
"## More complex example\n",
"\n",
"Now, let's use an example where some parameters are defined on the `power_thrust_table` on the turbine input yaml, and some parameters are set upon instantiation of the class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "38731e16",
"metadata": {},
"outputs": [],
"source": [
"from floris.core.turbine import SimpleTurbine\n",
"\n",
"@define\n",
"class ScaledTurbine(BaseOperationModel):\n",
" \"\"\"\n",
" A turbine operation model that scales power and thrust coefficient\n",
" based on a user-defined scaling factor. This will use methods from the\n",
" prepackaged SimpleTurbine model, leaving some values as default.\n",
"\n",
" Scaling only applies to power, not to thrust_coefficient or\n",
" axial_induction. We also demonstrate that other \"nonfundamental\" methods\n",
" can be used on the class.\n",
" \"\"\"\n",
" scaling_factor = field(init=True, default=1.0, type=floris_float_type)\n",
"\n",
" def power(\n",
" self,\n",
" power_thrust_table: dict,\n",
" velocities: NDArrayFloat,\n",
" air_density: float,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" unscaled_power = SimpleTurbine.power(\n",
" power_thrust_table=power_thrust_table,\n",
" velocities=velocities,\n",
" air_density=air_density,\n",
" )\n",
" scaled_power = self._compute_scaled_power(unscaled_power)\n",
" return scaled_power\n",
"\n",
" def _compute_scaled_power(self, power: NDArrayFloat) -> NDArrayFloat:\n",
" return self.scaling_factor * power\n",
"\n",
" def thrust_coefficient(\n",
" self,\n",
" power_thrust_table: dict,\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" unscaled_thrust_coefficient = SimpleTurbine.thrust_coefficient(\n",
" power_thrust_table=power_thrust_table,\n",
" velocities=velocities,\n",
" )\n",
" return unscaled_thrust_coefficient\n",
"\n",
" def axial_induction(\n",
" self,\n",
" power_thrust_table: dict,\n",
" velocities: NDArrayFloat,\n",
" **_\n",
" ) -> NDArrayFloat:\n",
" unscaled_axial_induction = SimpleTurbine.axial_induction(\n",
" power_thrust_table=power_thrust_table,\n",
" velocities=velocities,\n",
" )\n",
" return unscaled_axial_induction"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "72ce900e",
"metadata": {},
"outputs": [],
"source": [
"# First, run with the unscaled SimpleTurbine model for comparison\n",
"fmodel.set_operation_model(SimpleTurbine)\n",
"fmodel.run()\n",
"initial_powers = fmodel.get_turbine_powers()\n",
"print(\"Unscaled Powers [W]:\\n\", initial_powers, \"\\n\")\n",
"\n",
"# Then, run with the scaled model\n",
"fmodel.set_operation_model(ScaledTurbine(scaling_factor=1.2))\n",
"fmodel.run()\n",
"\n",
"print(\"ScaledTurbine powers [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")"
]
},
{
"cell_type": "markdown",
"id": "0f3409fc",
"metadata": {},
"source": [
"## Prepackaged operation models\n",
"\n",
"Naturally, prepackaged operation models can also be used in this way. In fact, we just did that with the `SimpleTurbine` model! Let's take a look at using the `CosineLossTurbine` operation model from FLORIS, either as one of the preset defaults or by passing the class in directly."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1dd9ed49",
"metadata": {},
"outputs": [],
"source": [
"from floris.core.turbine import CosineLossTurbine\n",
"\n",
"fmodel.set_operation_model(\"simple\")\n",
"fmodel.set(\n",
" yaw_angles=np.array([[0.0, 20.0], [0.0, 20.0], [0.0, 20.0]]),\n",
")\n",
"fmodel.run()\n",
"\n",
"# Simple model does not respond to yaw angles, so powers are unaffected\n",
"print(\"Powers under simple model [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n",
"\n",
"# Now, switch to the cosine loss model as a built-in option\n",
"fmodel.set_operation_model(\"cosine-loss\")\n",
"fmodel.run()\n",
"\n",
"print(\"Powers under cosine-loss model [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")\n",
"\n",
"# Instead, we can pass in the class directly\n",
"fmodel.set_operation_model(CosineLossTurbine)\n",
"fmodel.run()\n",
"\n",
"print(\"Powers under cosine-loss model (class) [W]:\\n\", fmodel.get_turbine_powers(), \"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "814df049",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "floris",
"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.13.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading