diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e920f68ef7..6217c5a76c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -70,5 +70,5 @@ Release checklist: - [ ] README.md (appears twice in README.md) - [ ] pyproject.toml - [ ] Verify docs builds correctly -- [ ] Create a tag in the NREL/FLORIS repository +- [ ] Create a tag in the NatLabRockies/FLORIS repository --> diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f4d4dc604..287fe9a836 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,4 +23,4 @@ repos: hooks: - id: isort name: isort - stages: [commit] + stages: [pre-commit] diff --git a/README.md b/README.md index a08067743d..e86469c76c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ FLORIS is a controls-focused wind farm simulation software incorporating steady-state engineering wake models into a performance-focused Python framework. It has been in active development at NLR since 2013 and the latest -release is [FLORIS v.4.6.4](https://github.com/NatLabRockies/floris/releases/latest). +release is [FLORIS v.4.6.6](https://github.com/NatLabRockies/floris/releases/latest). Online documentation is available at https://natlabrockies.github.io/floris. The software is in active development and engagement with the development team @@ -89,7 +89,7 @@ PACKAGE CONTENTS wind_data VERSION - 4.6.4 + 4.6.6 FILE ~/floris/floris/__init__.py diff --git a/docs/dev_guide.md b/docs/dev_guide.md index 890d52e6da..e1dcf553d0 100644 --- a/docs/dev_guide.md +++ b/docs/dev_guide.md @@ -406,6 +406,5 @@ def function( Some models require a special grid and/or solver, and that mapping happens in [floris.core.core.Core](https://github.com/NatLabRockies/floris/blob/main/floris/core/core.py). Generally, a specific kind of solver requires one or a number of specific grid-types. -For example, `full_flow_sequential_solver` requires either `FlowFieldGrid` or -`FlowFieldPlanarGrid`. +For example, `full_flow_sequential_solver` requires a `FlowFieldPlanarGrid`. So, it is often the case that adding a new solver will require adding a new grid type, as well. diff --git a/docs/installation.md b/docs/installation.md index 9022932e60..679b4a4383 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -91,7 +91,7 @@ PACKAGE CONTENTS wind_data VERSION - 4.6.4 + 4.6.6 FILE ~/floris/floris/__init__.py diff --git a/docs/user_defined_operation_models.ipynb b/docs/user_defined_operation_models.ipynb new file mode 100644 index 0000000000..99e963e7e4 --- /dev/null +++ b/docs/user_defined_operation_models.ipynb @@ -0,0 +1,405 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ba9ae6ce", + "metadata": {}, + "source": [ + "# User-defined Turbine Operation Models\n", + "\n", + "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", + "\n", + " def __attrs_post_init__(self):\n", + " # Ensure that the provided values are of the correct type\n", + " self.power_value = floris_float_type(self.power_value)\n", + " self.axial_induction_value = floris_float_type(self.axial_induction_value)\n", + "\n", + " # NOTE: If you add an __attrs_post_init__ method (not really needed here), you need to call\n", + " # the parent class's __attrs_post_init__ method to ensure proper serialization and\n", + " # reinstantiation across multiple solves.\n", + " super().__attrs_post_init__()\n", + "\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 +} diff --git a/docs/user_defined_wake_models.ipynb b/docs/user_defined_wake_models.ipynb new file mode 100644 index 0000000000..29f064fabd --- /dev/null +++ b/docs/user_defined_wake_models.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ba9ae6ce", + "metadata": {}, + "source": [ + "# User-defined Wake Models\n", + "\n", + "Beginning in v5, FLORIS supports user-defined wake models that can be passed directly into FLORIS using `fmodel.set_wake_model`. A user-defined wake model may be a dynamic or static class, but will usually be dynamic to allow model parameters to be set as attributes. It must conform to the `attrs` package for declaring attributes (in particular, wake model parameters). Additionally all user-defined operation models should inherit from the abstract parent class `BaseWakeModel`, available in FLORIS.\n", + "\n", + "All operation models must implement the following \"fundamental\" methods:\n", + "- `turbine_solve`: computes the flow solution at all turbine locations, part of the main FLORIS `run` procedure.\n", + "- `point_solve`: computes the flow solution at arbitrary, user-provided points in the flow, or for cut planes for visualization purposes.\n", + "\n", + "Wake models may then implement additional methods as needed.\n", + "\n", + "The following arguments are passed to either `turbine_solve` or `point_solve` at runtime:\n", + "\n", + "| Argument | Data type | Description |\n", + "|----------|-----------|----------|\n", + "| `farm` | `floris.core.Farm` | text |\n", + "| `flow_field` | `floris.core.FlowField` | The flow field object, which contains the flow solution and other flow-related quantities. |\n", + "| `grid` | `floris.core.TurbineGrid` or `floris.core.FlowFieldPlanarGrid` or `floris.core.PointsGrid` | The grid object corresponding to the type of solve being performed. For `turbine_solve`, this will be a `TurbineGrid`. For `point_solve`, this will be either a `FlowFieldPlanarGrid` or `PointsGrid`, depending on the type of points being solved for (visualization-type solves or individual point solves, respectively). |\n", + "\n", + "The `turbine_solve` and `point_solve` methods do not return any values, but instead update the `flow_field` argument in-place." + ] + }, + { + "cell_type": "markdown", + "id": "aefe4f59", + "metadata": {}, + "source": [ + "### Static example\n", + "\n", + "We begin with a very simple example that will produce a \"straight\" wake behind each turbine, whose velocity deficit (as a fraction of the free stream velocity) is constant and user-definable. This is not a good wake model (and doesn't adhere to momentum conservation)! We're just using it as a basic example to demonstrate the functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d751aa3c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from attrs import define, field\n", + "from floris.type_dec import floris_float_type, NDArrayFloat\n", + "from floris.core.wake_model import BaseWakeModel\n", + "from floris.flow_visualization import visualize_cut_plane\n", + "\n", + "from floris.core import (\n", + " BaseModel,\n", + " Farm,\n", + " FlowField,\n", + " FlowFieldPlanarGrid,\n", + " PointsGrid,\n", + " TurbineGrid,\n", + ")\n", + "\n", + "@define\n", + "class StraightWake(BaseWakeModel):\n", + " \"\"\"\n", + " A simple wake model that produces a straight wake behind each turbine.\n", + " \"\"\"\n", + "\n", + " # Using attrs, we can define model parameters as class attributes.\n", + " velocity_deficit: floris_float_type = field(default=0.2)\n", + " wake_width: floris_float_type = field(default=100.0)\n", + "\n", + " def __attrs_post_init__(self):\n", + " # Ensure that the provided values are of the correct type\n", + " self.velocity_deficit = floris_float_type(self.velocity_deficit)\n", + " self.wake_width = floris_float_type(self.wake_width)\n", + "\n", + " # NOTE: If you add an __attrs_post_init__ method (not really needed here), you need to call\n", + " # the parent class's __attrs_post_init__ method to ensure proper serialization and\n", + " # reinstantiation across multiple solves.\n", + " super().__attrs_post_init__()\n", + "\n", + " # Define a method for determining whether a test point is within the wake of at least one\n", + " # turbine\n", + " def _is_in_wake(self, grid, turbine_i_x, turbine_i_y, turbine_i_z):\n", + "\n", + " # Declare all True to start\n", + " in_wake_i = np.full(grid.x_sorted.shape, True)\n", + "\n", + " # Check if downstream of any turbine\n", + " in_wake_i &= (grid.x_sorted > turbine_i_x.mean(axis=(2,3), keepdims=True))\n", + "\n", + " # Check if within wake width of any turbine\n", + " in_wake_i &= (\n", + " np.abs(grid.y_sorted - turbine_i_y.mean(axis=(2,3), keepdims=True))\n", + " < self.wake_width / 2\n", + " )\n", + " in_wake_i &= (\n", + " np.abs(grid.z_sorted - turbine_i_z.mean(axis=(2,3), keepdims=True))\n", + " < self.wake_width / 2\n", + " )\n", + "\n", + " # Return resulting boolean array\n", + " return in_wake_i\n", + "\n", + " # Define the main turbine_solve method for solving at turbine locations\n", + " def turbine_solve(\n", + " self,\n", + " farm: Farm,\n", + " flow_field: FlowField,\n", + " grid: TurbineGrid,\n", + " ) -> None:\n", + "\n", + " # Initialize an array to keep track of whether each point is in the wake of any turbine\n", + " in_wake = np.full(grid.x_sorted.shape, False)\n", + "\n", + " for i in range(grid.n_turbines):\n", + "\n", + " # Check if the points are in the wake of turbine i\n", + " in_wake_i = self._is_in_wake(\n", + " grid,\n", + " grid.x_sorted[:, i:i+1, :, :],\n", + " grid.y_sorted[:, i:i+1, :, :],\n", + " grid.z_sorted[:, i:i+1, :, :]\n", + " )\n", + "\n", + " # Update the overall in_wake array to include the wake of turbine i\n", + " in_wake |= in_wake_i\n", + "\n", + " # Apply velocity deficits\n", + " flow_field.u_sorted = flow_field.u_initial_sorted * (1 - self.velocity_deficit * in_wake)\n", + "\n", + " self.evaluate_turbine_power(grid, farm, flow_field)\n", + " self.evaluate_turbine_thrust_coefficient(grid, farm, flow_field)\n", + " print(\"turbine_solve completed with StraightWake model!\")\n", + "\n", + " return None\n", + "\n", + " # Define the secondary point_solve method for solving at arbitrary points in the flow\n", + " def point_solve(\n", + " self,\n", + " farm: Farm,\n", + " flow_field: FlowField,\n", + " grid: FlowFieldPlanarGrid | PointsGrid,\n", + " ) -> None:\n", + " # Use parent class method to access the turbine grid\n", + " turbine_grid = self.generate_turbine_grid_objects(farm, flow_field)[2]\n", + "\n", + " # Initialize an array to keep track of whether each point is in the wake of any turbine\n", + " in_wake = np.full(grid.x_sorted.shape, False)\n", + "\n", + " for i in range(turbine_grid.n_turbines):\n", + "\n", + " # Check if the turbine is in the wake of any other turbine\n", + " in_wake_i = self._is_in_wake(\n", + " grid,\n", + " turbine_grid.x_sorted[:, i:i+1, :, :],\n", + " turbine_grid.y_sorted[:, i:i+1, :, :],\n", + " turbine_grid.z_sorted[:, i:i+1, :, :]\n", + " )\n", + "\n", + " # Update the overall in_wake array to include the wake of turbine i\n", + " in_wake |= in_wake_i\n", + "\n", + " # Apply velocity deficits\n", + " flow_field.u_sorted = flow_field.u_initial_sorted * (1 - self.velocity_deficit * in_wake)\n", + " print(\"point_solve completed with StraightWake model!\")\n", + "\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "29ea6830", + "metadata": {}, + "source": [ + "Let's now use this straight wake 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_wake_model(StraightWake(velocity_deficit=0.2, wake_width=100.0))\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\")" + ] + }, + { + "cell_type": "markdown", + "id": "aa2b73b0", + "metadata": {}, + "source": [ + "## Visualization example\n", + "\n", + "Now, we will perform a flow visualization, which uses the `point_solve` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93f1e99f", + "metadata": {}, + "outputs": [], + "source": [ + "# Rotate flow to visualize separate wakes\n", + "fmodel.set(wind_speeds=[8.0], wind_directions=[280.0], turbulence_intensities=[0.06])\n", + "\n", + "horizontal_plane = fmodel.calculate_horizontal_plane(\n", + " x_resolution=200,\n", + " y_resolution=100,\n", + " height=90.0,\n", + ")\n", + "\n", + "fig, ax = plt.subplots()\n", + "visualize_cut_plane(\n", + " horizontal_plane,\n", + " ax=ax,\n", + " label_contours=False,\n", + " title=\"Horizontal Flow with Turbine Rotors and labels\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0f3409fc", + "metadata": {}, + "source": [ + "## Prepackaged wake models\n", + "\n", + "Naturally, prepackaged wake models can also be used in this way. Let's take a look at using the `Gauss` wake 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.wake_model import Gauss\n", + "\n", + "fmodel.set_wake_model(Gauss()) # Use Gauss defaults\n", + "horizontal_plane = fmodel.calculate_horizontal_plane(\n", + " x_resolution=200,\n", + " y_resolution=100,\n", + " height=90.0,\n", + ")\n", + "\n", + "fig, ax = plt.subplots()\n", + "visualize_cut_plane(\n", + " horizontal_plane,\n", + " ax=ax,\n", + " label_contours=False,\n", + " title=\"Horizontal Flow with Turbine Rotors and labels\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ef42e63", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "eni", + "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.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/examples_control_types/001_derating_control.py b/examples/examples_control_types/001_derating_control.py index 41bf3ea2ac..fd96f8e843 100644 --- a/examples/examples_control_types/001_derating_control.py +++ b/examples/examples_control_types/001_derating_control.py @@ -28,8 +28,8 @@ # Convert to a simple two turbine layout with derating turbines fmodel.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0]) -# For reference, load the turbine type -turbine_type = fmodel.core.farm.turbine_definitions[0] +# For reference, load the turbine alone +turbine = fmodel.core.farm.turbines[0] # Set the wind directions and speeds to be constant over n_findex = N time steps N = 50 @@ -80,7 +80,7 @@ ) ax.plot( power_setpoints[:, 1] / 1000, - np.ones(N) * np.max(turbine_type["power_thrust_table"]["power"]), + np.ones(N) * np.max(turbine.power_thrust_table["power"]), color="k", linestyle="dashed", label="Rated power", diff --git a/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py b/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py index d8da4d1330..8b8e26d386 100644 --- a/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py +++ b/examples/examples_emgauss/001_empirical_gauss_velocity_deficit_parameters.py @@ -128,10 +128,7 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Increase the base recovery rate fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] = [ - 0.03, - 0.015, -] +fmodel_dict_mod["wake"]["parameters"]["wake_expansion_rates"] = [0.03, 0.015] fmodel = FlorisModel(fmodel_dict_mod) fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) @@ -149,11 +146,10 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Add new expansion rate fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] = ( - fmodel_dict["wake"]["wake_velocity_parameters"]["empirical_gauss"]["wake_expansion_rates"] - + [0.0] +fmodel_dict_mod["wake"]["parameters"]["wake_expansion_rates"] = ( + fmodel_dict["wake"]["parameters"]["wake_expansion_rates"] + [0.0] ) -fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["breakpoints_D"] = [5, 10] +fmodel_dict_mod["wake"]["parameters"]["breakpoints_D"] = [5, 10] fmodel = FlorisModel(fmodel_dict_mod) fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) @@ -172,7 +168,7 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Increase the wake-induced mixing gain fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod["wake"]["wake_velocity_parameters"]["empirical_gauss"]["mixing_gain_velocity"] = 3.0 +fmodel_dict_mod["wake"]["parameters"]["mixing_gain_velocity"] = 3.0 fmodel = FlorisModel(fmodel_dict_mod) fmodel.set(wind_speeds=[8.0], wind_directions=[270.0]) diff --git a/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py b/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py index e1051ad6d2..44bf004577 100644 --- a/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py +++ b/examples/examples_emgauss/002_empirical_gauss_deflection_parameters.py @@ -141,9 +141,7 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Increase the maximum deflection attained fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod["wake"]["wake_deflection_parameters"]["empirical_gauss"][ - "horizontal_deflection_gain_D" -] = 5.0 +fmodel_dict_mod["wake"]["parameters"]["horizontal_deflection_gain_D"] = 5.0 fmodel = FlorisModel(fmodel_dict_mod) fmodel.set( @@ -166,9 +164,7 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): # Add (increase) influence of wake added mixing fmodel_dict_mod = copy.deepcopy(fmodel_dict) -fmodel_dict_mod["wake"]["wake_deflection_parameters"]["empirical_gauss"][ - "mixing_gain_deflection" -] = 100.0 +fmodel_dict_mod["wake"]["parameters"]["mixing_gain_deflection"] = 100.0 fmodel = FlorisModel(fmodel_dict_mod) fmodel.set( @@ -193,12 +189,8 @@ def generate_wake_visualization(fmodel: FlorisModel, title=None): fmodel_dict_mod = copy.deepcopy(fmodel_dict) # Include a WIM gain so that YAM is reflected in deflection as well # as deficit -fmodel_dict_mod["wake"]["wake_deflection_parameters"]["empirical_gauss"][ - "mixing_gain_deflection" -] = 100.0 -fmodel_dict_mod["wake"]["wake_deflection_parameters"]["empirical_gauss"][ - "yaw_added_mixing_gain" -] = 1.0 +fmodel_dict_mod["wake"]["parameters"]["mixing_gain_deflection"] = 100.0 +fmodel_dict_mod["wake"]["parameters"]["yaw_added_mixing_gain"] = 1.0 fmodel = FlorisModel(fmodel_dict_mod) fmodel.set( wind_speeds=[8.0], diff --git a/examples/examples_floating/001_floating_turbine_models.py b/examples/examples_floating/001_floating_turbine_models.py index 900b588fe9..d7f45e3a85 100644 --- a/examples/examples_floating/001_floating_turbine_models.py +++ b/examples/examples_floating/001_floating_turbine_models.py @@ -29,6 +29,7 @@ import numpy as np from floris import FlorisModel, TimeSeries +from floris.core.rotor_velocity import calculate_tilt_for_rotor_effective_velocities # Create the Floris instances @@ -65,16 +66,21 @@ # Grab turbine tilt angles eff_vels = fmodel_fixed.turbine_average_velocities -tilt_angles_fixed = np.squeeze(fmodel_fixed.core.farm.calculate_tilt_for_eff_velocities(eff_vels)) +tilt_angles_fixed = np.squeeze( + calculate_tilt_for_rotor_effective_velocities(fmodel_fixed.core.farm, eff_vels) +) eff_vels = fmodel_floating.turbine_average_velocities tilt_angles_floating = np.squeeze( - fmodel_floating.core.farm.calculate_tilt_for_eff_velocities(eff_vels) + calculate_tilt_for_rotor_effective_velocities(fmodel_floating.core.farm, eff_vels) ) eff_vels = fmodel_floating_defined_floating.turbine_average_velocities tilt_angles_floating_defined_floating = np.squeeze( - fmodel_floating_defined_floating.core.farm.calculate_tilt_for_eff_velocities(eff_vels) + calculate_tilt_for_rotor_effective_velocities( + fmodel_floating_defined_floating.core.farm, + eff_vels + ) ) # Plot results diff --git a/examples/examples_floating/002_floating_vs_fixedbottom_farm.py b/examples/examples_floating/002_floating_vs_fixedbottom_farm.py index 0400ac7f1d..bfcd683b59 100644 --- a/examples/examples_floating/002_floating_vs_fixedbottom_farm.py +++ b/examples/examples_floating/002_floating_vs_fixedbottom_farm.py @@ -24,8 +24,6 @@ import matplotlib.pyplot as plt import numpy as np -import pandas as pd -from scipy.interpolate import NearestNDInterpolator import floris.flow_visualization as flowviz from floris import FlorisModel, WindRose diff --git a/examples/examples_get_flow/004_plot_velocity_deficit_profiles.py b/examples/examples_get_flow/004_plot_velocity_deficit_profiles.py index 1b8cabc772..f5fc61d926 100644 --- a/examples/examples_get_flow/004_plot_velocity_deficit_profiles.py +++ b/examples/examples_get_flow/004_plot_velocity_deficit_profiles.py @@ -99,7 +99,17 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): # Change velocity model to jensen, get the velocity deficit profiles, # and add them to the figure. floris_dict = fmodel.core.as_dict() - floris_dict["wake"]["model_strings"]["velocity_model"] = "jensen" + floris_dict["wake"]["model"] = "jensen" + floris_dict["wake"]["parameters"] = { + "initial": 0.1, + "constant": 0.5, + "ai": 0.8, + "downstream": -0.32, + "ad": 0.0, + "bd": 0.0, + "kd": 0.05, + "we": 0.05, + } fmodel = FlorisModel(floris_dict) profiles = fmodel.sample_velocity_deficit_profiles( direction="cross-stream", @@ -129,7 +139,23 @@ def annotate_coordinate_system(x_origin, y_origin, quiver_length): wind_direction = 315.0 # Try to change this downstream_dists = D * np.array([3, 5]) floris_dict = fmodel.core.as_dict() - floris_dict["wake"]["model_strings"]["velocity_model"] = "gauss" + floris_dict["wake"]["model"] = "gauss" + floris_dict["wake"]["parameters"] = { + "enable_secondary_steering": True, + "enable_yaw_added_recovery": True, + "enable_transverse_velocities": True, + "ad": 0.0, + "alpha": 0.58, + "bd": 0.0, + "beta": 0.077, + "dm": 1.0, + "ka": 0.38, + "kb": 0.004, + "initial": 0.1, + "constant": 0.5, + "ai": 0.8, + "downstream": -0.32, + } fmodel = FlorisModel(floris_dict) # Let (x_t1, y_t1) be the location of the second turbine x_t1 = 2 * D diff --git a/examples/examples_operation_models/002_define_operation_model.py b/examples/examples_operation_models/002_define_operation_model.py new file mode 100644 index 0000000000..29e597baf2 --- /dev/null +++ b/examples/examples_operation_models/002_define_operation_model.py @@ -0,0 +1,108 @@ +"""Example: Create and supply a user-defined operation model + +This example shows how to create a user-defined operation model and supply it to FLORIS. +It is based on an idealized actuator disk model that does not curtail (i.e. has no rated +wind speed). +""" + +import matplotlib.pyplot as plt +import numpy as np +from attrs import define, field + +from floris import FlorisModel +from floris.core.rotor_velocity import average_velocity +from floris.core.turbine.operation_models import BaseOperationModel +from floris.type_dec import floris_float_type, NDArrayFloat + + +# Declare the new operation model, inheriting from BaseOperationModel. The `@define` decorator from +# the `attrs` package is used to declare attributes for the class. +@define +class IdealizedActuatorDiskModel(BaseOperationModel): + """ + Idealized actuator disk model that does not curtail (i.e. has no rated wind speed). + """ + + # Declare attributes for the class using the `field` function from the `attrs` package. + constant_axial_induction: floris_float_type = field(default=1.0 / 3.0) + rotor_diameter: floris_float_type = field(default=126.0) # Default to NREL 5MW rotor diameter + + # Declare the required `power`, `thrust`, and `axial_induction` methods. + def power(self, velocities, air_density, **_): + """ + Compute the power output of the turbine in Watts. + """ + rotor_average_velocities = average_velocity(velocities) + axial_induction = self.axial_induction(velocities) + power_coefficient = 4 * axial_induction * (1 - axial_induction) ** 2 + return 0.5 * air_density * self._area() * rotor_average_velocities**3 * power_coefficient + + def thrust_coefficient(self, velocities, air_density, **_): + """ + Compute the thrust force on the turbine in Newtons. + """ + rotor_average_velocities = average_velocity(velocities) + axial_induction = self.axial_induction(velocities) + thrust_coefficient = 4 * axial_induction * (1 - axial_induction) + return 0.5 * air_density * self._area() * rotor_average_velocities**2 * thrust_coefficient + + def axial_induction(self, velocities, **_): + """ + Return the (user-provided) axial induction factor of the turbine. + """ + return self.constant_axial_induction * np.ones(velocities.shape[0:2]) + + def _area(self): + """Compute the rotor swept area of the turbine.""" + return np.pi * (self.rotor_diameter / 2) ** 2 + + +# Create a sweep over wind speed to evaluate the turbine operation model +ws_array = np.arange(0.1, 30.0, 0.2) +wd_array = 270.0 * np.ones_like(ws_array) +turbulence_intensities = 0.06 * np.ones_like(ws_array) + +# Instantiate FLORIS with a single wind turbine +fmodel = FlorisModel("../inputs/gch.yaml") +fmodel.set( + layout_x=[0], + layout_y=[0], + wind_speeds=ws_array, + wind_directions=wd_array, + turbulence_intensities=turbulence_intensities +) + +# First, solve with the default operation model and store the results +fmodel.run() +powers = fmodel.get_turbine_powers() + +fig, ax = plt.subplots(1, 1, figsize=(10, 5)) +ax.plot(ws_array, powers/1e3, color="k", linestyle="--", label="Default operation model") + +# Now, create an instance of the user-defined operation model and supply it to FLORIS +fmodel.set_operation_model(IdealizedActuatorDiskModel()) +fmodel.run() +powers = fmodel.get_turbine_powers() +ax.plot(ws_array, powers/1e3, label="User-defined operation model (defaults)") + +# Change the axial induction factor +fmodel.set_operation_model(IdealizedActuatorDiskModel(constant_axial_induction=0.2)) +fmodel.run() +powers = fmodel.get_turbine_powers() +ax.plot(ws_array, powers/1e3, label="User-defined operation model (low axial induction)") + +# Reset the axial induction factor; reduce the rotor diameter to 100 m +fmodel.set_operation_model(IdealizedActuatorDiskModel(rotor_diameter=100.0)) +fmodel.run() +powers = fmodel.get_turbine_powers() +ax.plot(ws_array, powers/1e3, label="User-defined operation model (smaller rotor)") + +# Plot aesthetics +ax.set_xlabel("Wind speed [m/s]") +ax.set_ylabel("Power [kW]") +ax.set_ylim([-2e3, 10e3]) +ax.set_xlim([ws_array[0], ws_array[-1]]) +ax.grid(True) +ax.legend(loc="upper right") + +plt.show() diff --git a/examples/examples_turbine/001_reference_turbines.py b/examples/examples_turbine/001_reference_turbines.py index 1ca53e1e81..19abc279c2 100644 --- a/examples/examples_turbine/001_reference_turbines.py +++ b/examples/examples_turbine/001_reference_turbines.py @@ -33,7 +33,7 @@ # multi-dimensional power/thrust coefficient turbine definitions as they require different handling turbines = [ t.stem - for t in fmodel.core.farm.internal_turbine_library.iterdir() + for t in fmodel.core.farm.internal_turbine_library_path.iterdir() if t.suffix == ".yaml" and ("multi_dim" not in t.stem) ] @@ -51,16 +51,16 @@ # Plot power and ct onto the fig_pow_ct plot axarr_pow_ct[0].plot( - fmodel.core.farm.turbine_map[0].power_thrust_table["wind_speed"], - fmodel.core.farm.turbine_map[0].power_thrust_table["power"], + fmodel.core.farm.turbines[0].power_thrust_table["wind_speed"], + fmodel.core.farm.turbines[0].power_thrust_table["power"], label=t, ) axarr_pow_ct[0].grid(True) axarr_pow_ct[0].legend() axarr_pow_ct[0].set_ylabel("Power (kW)") axarr_pow_ct[1].plot( - fmodel.core.farm.turbine_map[0].power_thrust_table["wind_speed"], - fmodel.core.farm.turbine_map[0].power_thrust_table["thrust_coefficient"], + fmodel.core.farm.turbines[0].power_thrust_table["wind_speed"], + fmodel.core.farm.turbines[0].power_thrust_table["thrust_coefficient"], label=t, ) axarr_pow_ct[1].grid(True) diff --git a/examples/examples_turbopark/001_compare_turbopark_implementations.py b/examples/examples_turbopark/001_compare_turbopark_implementations.py index 14b85f0815..b76bfc0914 100644 --- a/examples/examples_turbopark/001_compare_turbopark_implementations.py +++ b/examples/examples_turbopark/001_compare_turbopark_implementations.py @@ -1,8 +1,7 @@ """Example: Compare TurbOPark model implementations -This example demonstrates a new implementation of the TurbOPark model that is -more faithful to the original description provided by Pedersen et al and uses -the sequential_solver, and compares it to the existing implementation in -Floris. +This example demonstrates the TurbOPark model, comparing it to the original +description provided by Pedersen et al. The model is also compared to an +earlier, now retired, TurbOPark implementation in FLORIS. """ import matplotlib.pyplot as plt @@ -14,11 +13,6 @@ from floris.turbine_library import build_cosine_loss_turbine_dict -# Note: "new" is used to refer to the new implementation of TurbOPark, which is -# more faithful to the description provided by Pedersen et al. (2022). "orig" -# is used to refer to the existing TurbOPark implementation in Floris (which -# was based on Ørsted's Matlab code, originally from Nygaard et al. (2020). - ### Build a constant CT turbine model for use in comparisons (not realistic) const_CT_turb = build_cosine_loss_turbine_dict( turbine_data_dict={ @@ -34,13 +28,13 @@ ### Start by visualizing a single turbine in and its wake with the new model # Load the new TurboPark implementation and switch to constant CT turbine -fmodel_new = FlorisModel("../inputs/turboparkgauss_cubature.yaml") -fmodel_new.set( +fmodel = FlorisModel("../inputs/turboparkgauss_cubature.yaml") +fmodel.set( turbine_type=[const_CT_turb], - reference_wind_height=fmodel_new.reference_wind_height + reference_wind_height=fmodel.reference_wind_height ) -fmodel_new.run() -u0 = fmodel_new.wind_speeds[0] +fmodel.run() +u0 = fmodel.wind_speeds[0] col_orig = "C0" col_new = "C1" @@ -52,7 +46,7 @@ z_resolution=100 x_bounds = [-5*rotor_diameter, 25*rotor_diameter] -horizontal_plane = fmodel_new.calculate_horizontal_plane( +horizontal_plane = fmodel.calculate_horizontal_plane( x_resolution=x_resolution, y_resolution=y_resolution, height=100.0, @@ -95,12 +89,6 @@ ax[2].set_xlim([-2, 2]) ### Look at the wake profile at a single downstream distance for a range of wind directions -# Load the original TurboPark implementation and switch to constant CT turbine -fmodel_orig = FlorisModel("../inputs/turbopark_cubature.yaml") -fmodel_orig.set( - turbine_type=[const_CT_turb], - reference_wind_height=fmodel_orig.reference_wind_height -) # Set up and solve flows wd_array = np.arange(225,315,0.1) @@ -109,36 +97,35 @@ wind_directions=wd_array, turbulence_intensities=0.06 ) -fmodel_orig.set( - layout_x = [0.0, 600.0], - layout_y = [0.0, 0.0], - wind_data=wind_data_wd_sweep -) -fmodel_orig.run() # Extract output velocities at downstream turbine -orig_vels_ds = fmodel_orig.turbine_average_velocities[:,1] -u0 = fmodel_orig.wind_speeds[0] # Get freestream wind speed for normalization +u0 = fmodel.wind_speeds[0] # Get freestream wind speed for normalization # Set up and solve flows; extract velocities at downstream turbine -fmodel_new.set( +fmodel.set( layout_x = [0.0, 600.0], layout_y = [0.0, 0.0], wind_data=wind_data_wd_sweep ) -fmodel_new.run() -new_vels_ds = fmodel_new.turbine_average_velocities[:,1] +fmodel.run() +new_vels_ds = fmodel.turbine_average_velocities[:,1] + +df_new = pd.DataFrame({"wd": wd_array, "wws": new_vels_ds/u0}) # Load comparison data (generated by running Ørsted's Matlab code # https://github.com/OrstedRD/TurbOPark) df_twinpark = pd.read_csv("comparison_data/WindDirection_Sweep_Orsted.csv") +# Load comparison from retired Floris TurbOPark implementation +df_orig = pd.read_csv("comparison_data/WindDirection_Sweep_FlorisOrig.csv") # Plot the data and compare fig, ax = plt.subplots(2, 1) fig.set_size_inches(7, 10) -ax[0].plot(wd_array, orig_vels_ds/u0, label="Floris - TurbOPark", c=col_orig) -ax[0].plot(wd_array, new_vels_ds/u0, label="Floris - TurbOPark-Gauss", c=col_new) -df_twinpark.plot("wd", "wws", ax=ax[0], linestyle="--", color="k", label="Orsted - TurbOPark") +ax[0].plot(df_orig["wd"], df_orig["wws"], color=col_orig, label="Floris - TurbOPark (retired)") +ax[0].plot(df_new["wd"], df_new["wws"], color=col_new, label="Floris - TurbOPark-Gauss") +ax[0].plot( + df_twinpark["wd"], df_twinpark["wws"], linestyle="--", color="k", label="Orsted - TurbOPark" +) ax[0].set_xlabel("Wind direction [deg]") ax[0].set_ylabel("Normalized rotor averaged waked wind speed [-]") @@ -156,25 +143,20 @@ wind_directions=270.0, turbulence_intensities=0.06 ) -fmodel_orig.set( - layout_x=layout_x, - layout_y=layout_y, - wind_data=wind_data_row -) -fmodel_new.set( +fmodel.set( layout_x=layout_x, layout_y=layout_y, wind_data=wind_data_row ) # Run and extract flow velocities at the turbines -fmodel_orig.run() -orig_vels_row = fmodel_orig.turbine_average_velocities -fmodel_new.run() -new_vels_row = fmodel_new.turbine_average_velocities -u0 = fmodel_orig.wind_speeds[0] # Get freestream wind speed for normalization +fmodel.run() +new_vels_row = fmodel.turbine_average_velocities +u0 = fmodel.wind_speeds[0] # Get freestream wind speed for normalization +df_new = pd.DataFrame({"wtg_nr": turbines, "wws": new_vels_row.ravel()/u0}) # Load comparison data +df_orig = pd.read_csv("comparison_data/Rowpark_FlorisOrig.csv") df_rowpark = pd.read_csv("comparison_data/Rowpark_Orsted.csv") # Plot the data and compare @@ -182,10 +164,10 @@ turbines, df_rowpark["wws"], s=80, marker="o", c="k", label="Orsted - TurbOPark" ) ax[1].scatter( - turbines, orig_vels_row/u0, s=20, marker="o", c=col_orig, label="Floris - TurbOPark" + turbines, df_orig["wws"], s=20, marker="o", c=col_orig, label="Floris - TurbOPark (retired)" ) ax[1].scatter( - turbines, new_vels_row/u0, s=20, marker="o", c=col_new, label="Floris - TurbOPark_Gauss" + turbines, df_new["wws"], s=20, marker="o", c=col_new, label="Floris - TurbOPark-Gauss" ) ax[1].set_xlabel("Turbine number") ax[1].set_ylabel("Normalized rotor averaged wind speed [-]") diff --git a/examples/examples_turbopark/comparison_data/Rowpark_FlorisOrig.csv b/examples/examples_turbopark/comparison_data/Rowpark_FlorisOrig.csv new file mode 100644 index 0000000000..0e18432afb --- /dev/null +++ b/examples/examples_turbopark/comparison_data/Rowpark_FlorisOrig.csv @@ -0,0 +1,11 @@ +wtg_nr,wws +0,1.0000000000000002 +1,0.7790529295022297 +2,0.7044837474665188 +3,0.653027066228054 +4,0.6131014078339698 +5,0.5803786146373723 +6,0.5526915571419454 +7,0.528771448509819 +8,0.5077983674980792 +9,0.48920491202514593 diff --git a/examples/examples_turbopark/comparison_data/WindDirection_Sweep_FlorisOrig.csv b/examples/examples_turbopark/comparison_data/WindDirection_Sweep_FlorisOrig.csv new file mode 100644 index 0000000000..3008567825 --- /dev/null +++ b/examples/examples_turbopark/comparison_data/WindDirection_Sweep_FlorisOrig.csv @@ -0,0 +1,901 @@ +wd,wws +225.0,1.0000000000000002 +225.1,1.0000000000000002 +225.2,1.0000000000000002 +225.29999999999998,1.0000000000000002 +225.39999999999998,1.0000000000000002 +225.49999999999997,1.0000000000000002 +225.59999999999997,1.0000000000000002 +225.69999999999996,1.0000000000000002 +225.79999999999995,1.0000000000000002 +225.89999999999995,1.0000000000000002 +225.99999999999994,1.0000000000000002 +226.09999999999994,1.0000000000000002 +226.19999999999993,1.0000000000000002 +226.29999999999993,1.0000000000000002 +226.39999999999992,1.0000000000000002 +226.49999999999991,1.0000000000000002 +226.5999999999999,1.0000000000000002 +226.6999999999999,1.0000000000000002 +226.7999999999999,1.0000000000000002 +226.8999999999999,1.0000000000000002 +226.9999999999999,1.0000000000000002 +227.09999999999988,1.0000000000000002 +227.19999999999987,1.0000000000000002 +227.29999999999987,1.0000000000000002 +227.39999999999986,1.0000000000000002 +227.49999999999986,1.0000000000000002 +227.59999999999985,1.0000000000000002 +227.69999999999985,1.0000000000000002 +227.79999999999984,1.0000000000000002 +227.89999999999984,1.0000000000000002 +227.99999999999983,1.0000000000000002 +228.09999999999982,1.0000000000000002 +228.19999999999982,1.0000000000000002 +228.2999999999998,1.0000000000000002 +228.3999999999998,1.0000000000000002 +228.4999999999998,1.0000000000000002 +228.5999999999998,1.0000000000000002 +228.6999999999998,1.0000000000000002 +228.79999999999978,1.0000000000000002 +228.89999999999978,1.0000000000000002 +228.99999999999977,1.0000000000000002 +229.09999999999977,1.0000000000000002 +229.19999999999976,1.0000000000000002 +229.29999999999976,1.0000000000000002 +229.39999999999975,1.0000000000000002 +229.49999999999974,1.0000000000000002 +229.59999999999974,1.0000000000000002 +229.69999999999973,1.0000000000000002 +229.79999999999973,1.0000000000000002 +229.89999999999972,1.0000000000000002 +229.99999999999972,1.0000000000000002 +230.0999999999997,1.0000000000000002 +230.1999999999997,1.0000000000000002 +230.2999999999997,1.0000000000000002 +230.3999999999997,1.0000000000000002 +230.4999999999997,1.0000000000000002 +230.59999999999968,1.0000000000000002 +230.69999999999968,1.0000000000000002 +230.79999999999967,1.0000000000000002 +230.89999999999966,1.0000000000000002 +230.99999999999966,1.0000000000000002 +231.09999999999965,1.0000000000000002 +231.19999999999965,1.0000000000000002 +231.29999999999964,1.0000000000000002 +231.39999999999964,1.0000000000000002 +231.49999999999963,1.0000000000000002 +231.59999999999962,1.0000000000000002 +231.69999999999962,1.0000000000000002 +231.7999999999996,1.0000000000000002 +231.8999999999996,1.0000000000000002 +231.9999999999996,1.0000000000000002 +232.0999999999996,1.0000000000000002 +232.1999999999996,1.0000000000000002 +232.29999999999959,1.0000000000000002 +232.39999999999958,1.0000000000000002 +232.49999999999957,1.0000000000000002 +232.59999999999957,1.0000000000000002 +232.69999999999956,1.0000000000000002 +232.79999999999956,1.0000000000000002 +232.89999999999955,1.0000000000000002 +232.99999999999955,1.0000000000000002 +233.09999999999954,1.0000000000000002 +233.19999999999953,1.0000000000000002 +233.29999999999953,1.0000000000000002 +233.39999999999952,1.0000000000000002 +233.49999999999952,1.0000000000000002 +233.5999999999995,1.0000000000000002 +233.6999999999995,1.0000000000000002 +233.7999999999995,1.0000000000000002 +233.8999999999995,1.0000000000000002 +233.9999999999995,1.0000000000000002 +234.09999999999948,1.0000000000000002 +234.19999999999948,1.0000000000000002 +234.29999999999947,1.0000000000000002 +234.39999999999947,1.0000000000000002 +234.49999999999946,1.0000000000000002 +234.59999999999945,1.0000000000000002 +234.69999999999945,1.0000000000000002 +234.79999999999944,1.0000000000000002 +234.89999999999944,1.0000000000000002 +234.99999999999943,1.0000000000000002 +235.09999999999943,1.0000000000000002 +235.19999999999942,1.0000000000000002 +235.29999999999941,1.0000000000000002 +235.3999999999994,1.0000000000000002 +235.4999999999994,1.0000000000000002 +235.5999999999994,1.0000000000000002 +235.6999999999994,1.0000000000000002 +235.7999999999994,1.0000000000000002 +235.89999999999938,1.0000000000000002 +235.99999999999937,1.0000000000000002 +236.09999999999937,1.0000000000000002 +236.19999999999936,1.0000000000000002 +236.29999999999936,1.0000000000000002 +236.39999999999935,1.0000000000000002 +236.49999999999935,1.0000000000000002 +236.59999999999934,1.0000000000000002 +236.69999999999933,1.0000000000000002 +236.79999999999933,1.0000000000000002 +236.89999999999932,1.0000000000000002 +236.99999999999932,1.0000000000000002 +237.0999999999993,1.0000000000000002 +237.1999999999993,1.0000000000000002 +237.2999999999993,1.0000000000000002 +237.3999999999993,1.0000000000000002 +237.4999999999993,1.0000000000000002 +237.59999999999928,1.0000000000000002 +237.69999999999928,1.0000000000000002 +237.79999999999927,1.0000000000000002 +237.89999999999927,1.0000000000000002 +237.99999999999926,1.0000000000000002 +238.09999999999926,1.0000000000000002 +238.19999999999925,1.0000000000000002 +238.29999999999924,1.0000000000000002 +238.39999999999924,1.0000000000000002 +238.49999999999923,1.0000000000000002 +238.59999999999923,1.0000000000000002 +238.69999999999922,1.0000000000000002 +238.79999999999922,1.0000000000000002 +238.8999999999992,1.0000000000000002 +238.9999999999992,1.0000000000000002 +239.0999999999992,1.0000000000000002 +239.1999999999992,1.0000000000000002 +239.2999999999992,1.0000000000000002 +239.39999999999918,1.0000000000000002 +239.49999999999918,1.0000000000000002 +239.59999999999917,1.0000000000000002 +239.69999999999916,1.0000000000000002 +239.79999999999916,1.0000000000000002 +239.89999999999915,1.0000000000000002 +239.99999999999915,1.0000000000000002 +240.09999999999914,1.0000000000000002 +240.19999999999914,1.0000000000000002 +240.29999999999913,1.0000000000000002 +240.39999999999912,1.0000000000000002 +240.49999999999912,1.0000000000000002 +240.5999999999991,1.0000000000000002 +240.6999999999991,1.0000000000000002 +240.7999999999991,1.0000000000000002 +240.8999999999991,1.0000000000000002 +240.9999999999991,1.0000000000000002 +241.09999999999908,1.0000000000000002 +241.19999999999908,1.0000000000000002 +241.29999999999907,1.0000000000000002 +241.39999999999907,1.0000000000000002 +241.49999999999906,1.0000000000000002 +241.59999999999906,1.0000000000000002 +241.69999999999905,1.0000000000000002 +241.79999999999905,1.0000000000000002 +241.89999999999904,1.0000000000000002 +241.99999999999903,1.0000000000000002 +242.09999999999903,1.0000000000000002 +242.19999999999902,1.0000000000000002 +242.29999999999902,1.0000000000000002 +242.399999999999,1.0000000000000002 +242.499999999999,1.0000000000000002 +242.599999999999,1.0000000000000002 +242.699999999999,1.0000000000000002 +242.799999999999,1.0000000000000002 +242.89999999999898,1.0000000000000002 +242.99999999999898,1.0000000000000002 +243.09999999999897,1.0000000000000002 +243.19999999999897,1.0000000000000002 +243.29999999999896,1.0000000000000002 +243.39999999999895,1.0000000000000002 +243.49999999999895,1.0000000000000002 +243.59999999999894,1.0000000000000002 +243.69999999999894,1.0000000000000002 +243.79999999999893,1.0000000000000002 +243.89999999999893,1.0000000000000002 +243.99999999999892,1.0000000000000002 +244.09999999999891,1.0000000000000002 +244.1999999999989,1.0000000000000002 +244.2999999999989,1.0000000000000002 +244.3999999999989,1.0000000000000002 +244.4999999999989,1.0000000000000002 +244.5999999999989,1.0000000000000002 +244.69999999999888,1.0000000000000002 +244.79999999999887,1.0000000000000002 +244.89999999999887,1.0000000000000002 +244.99999999999886,1.0000000000000002 +245.09999999999886,1.0000000000000002 +245.19999999999885,1.0000000000000002 +245.29999999999885,1.0000000000000002 +245.39999999999884,1.0000000000000002 +245.49999999999883,1.0000000000000002 +245.59999999999883,1.0000000000000002 +245.69999999999882,1.0000000000000002 +245.79999999999882,1.0000000000000002 +245.8999999999988,1.0000000000000002 +245.9999999999988,1.0000000000000002 +246.0999999999988,1.0000000000000002 +246.1999999999988,1.0000000000000002 +246.2999999999988,1.0000000000000002 +246.39999999999878,1.0000000000000002 +246.49999999999878,1.0000000000000002 +246.59999999999877,1.0000000000000002 +246.69999999999877,1.0000000000000002 +246.79999999999876,1.0000000000000002 +246.89999999999876,1.0000000000000002 +246.99999999999875,1.0000000000000002 +247.09999999999874,1.0000000000000002 +247.19999999999874,1.0000000000000002 +247.29999999999873,1.0000000000000002 +247.39999999999873,1.0000000000000002 +247.49999999999872,1.0000000000000002 +247.59999999999872,1.0000000000000002 +247.6999999999987,1.0000000000000002 +247.7999999999987,1.0000000000000002 +247.8999999999987,1.0000000000000002 +247.9999999999987,1.0000000000000002 +248.0999999999987,1.0000000000000002 +248.19999999999868,1.0000000000000002 +248.29999999999868,1.0000000000000002 +248.39999999999867,1.0000000000000002 +248.49999999999866,1.0000000000000002 +248.59999999999866,1.0000000000000002 +248.69999999999865,1.0000000000000002 +248.79999999999865,1.0000000000000002 +248.89999999999864,1.0000000000000002 +248.99999999999864,1.0000000000000002 +249.09999999999863,1.0000000000000002 +249.19999999999862,1.0000000000000002 +249.29999999999862,1.0000000000000002 +249.3999999999986,1.0000000000000002 +249.4999999999986,1.0000000000000002 +249.5999999999986,1.0000000000000002 +249.6999999999986,1.0000000000000002 +249.7999999999986,1.0000000000000002 +249.89999999999858,1.0000000000000002 +249.99999999999858,1.0000000000000002 +250.09999999999857,1.0000000000000002 +250.19999999999857,1.0000000000000002 +250.29999999999856,1.0000000000000002 +250.39999999999856,0.999707374515043 +250.49999999999855,0.9996908616337409 +250.59999999999854,0.9996735298017003 +250.69999999999854,0.9996553735512808 +250.79999999999853,0.999636371230791 +250.89999999999853,0.9996164931224109 +250.99999999999852,0.9995956663829679 +251.09999999999852,0.9995738948207941 +251.1999999999985,0.9995511522922575 +251.2999999999985,0.9995274061178354 +251.3999999999985,0.9995025710990713 +251.4999999999985,0.9994766656783073 +251.5999999999985,0.9994496569793152 +251.69999999999848,0.9989629158069973 +251.79999999999848,0.9989072993778664 +251.89999999999847,0.9988492120595297 +251.99999999999847,0.9987885745154048 +252.09999999999846,0.9987252524503055 +252.19999999999845,0.9986591188480626 +252.29999999999845,0.9983053322791703 +252.39999999999844,0.9982177474130067 +252.49999999999844,0.9981263121431615 +252.59999999999843,0.9980309991807516 +252.69999999999843,0.997931746325353 +252.79999999999842,0.9978284279581295 +252.8999999999984,0.9977207741235318 +252.9999999999984,0.9976087898364424 +253.0999999999984,0.9970243190811249 +253.1999999999984,0.9968769462792076 +253.2999999999984,0.9967235654786611 +253.39999999999839,0.9965641353462564 +253.49999999999838,0.9963984478447929 +253.59999999999837,0.9962261541868238 +253.69999999999837,0.9960472652400867 +253.79999999999836,0.9955772527273403 +253.89999999999836,0.9953682365083759 +253.99999999999835,0.9951511231140994 +254.09999999999835,0.9949259488592185 +254.19999999999834,0.9946925261522984 +254.29999999999833,0.9944504930671878 +254.39999999999833,0.9939112767730209 +254.49999999999832,0.9936350480448637 +254.59999999999832,0.9933488368560338 +254.6999999999983,0.9930524672528729 +254.7999999999983,0.9927458258718439 +254.8999999999983,0.9924286385973081 +254.9999999999983,0.9921006193481013 +255.0999999999983,0.9917616474452993 +255.19999999999828,0.9909564837083636 +255.29999999999828,0.9902846948919797 +255.39999999999827,0.9895727748273475 +255.49999999999827,0.9891270078909267 +255.59999999999826,0.9886663676234895 +255.69999999999825,0.9881906087639377 +255.79999999999825,0.9876996272885835 +255.89999999999824,0.9871931127297755 +255.99999999999824,0.9866707156980858 +256.0999999999982,0.9861322768405398 +256.1999999999982,0.9855775232941864 +256.29999999999825,0.9850061047925769 +256.3999999999982,0.9841253028165925 +256.4999999999982,0.9835033237091965 +256.5999999999982,0.9828632868341963 +256.6999999999982,0.9822050514274083 +256.7999999999982,0.9815284464119816 +256.89999999999816,0.9808330241866615 +256.9999999999982,0.9801187588430627 +257.0999999999982,0.9793855966114662 +257.19999999999817,0.978344599798834 +257.29999999999814,0.9775559674309117 +257.39999999999816,0.9762801253883582 +257.4999999999982,0.9754247319074659 +257.59999999999815,0.9745473594336437 +257.6999999999981,0.973351105886555 +257.79999999999814,0.9724122991896917 +257.89999999999816,0.9714503713010036 +257.9999999999981,0.9704652371476039 +258.0999999999981,0.9694567938581549 +258.1999999999981,0.9681344308367577 +258.29999999999814,0.9670629362123804 +258.3999999999981,0.9659671675157716 +258.49999999999807,0.9648472444023819 +258.5999999999981,0.9637026547581845 +258.6999999999981,0.9625333232967191 +258.7999999999981,0.9613397782065477 +258.89999999999804,0.9601216565745043 +258.99999999999807,0.958408496736319 +259.0999999999981,0.957114639398446 +259.19999999999806,0.9557950166173098 +259.299999999998,0.9544496788331271 +259.39999999999804,0.9530791531076434 +259.49999999999807,0.9516834095518435 +259.59999999999803,0.950262123364283 +259.699999999998,0.9488159461081397 +259.799999999998,0.9468760956623515 +259.89999999999804,0.9453528127674247 +259.999999999998,0.9438038796904968 +260.099999999998,0.9422296286923078 +260.199999999998,0.9403360274508145 +260.299999999998,0.9386946641867877 +260.399999999998,0.9370280252626062 +260.49999999999795,0.9353360888454674 +260.599999999998,0.9336192425133854 +260.699999999998,0.931878092903223 +260.79999999999797,0.9301128361946228 +260.89999999999793,0.9283239712709102 +260.99999999999795,0.9265118755549965 +261.099999999998,0.9243915979410118 +261.19999999999794,0.9225177989875196 +261.2999999999979,0.9206213479160047 +261.39999999999793,0.9187029399482634 +261.49999999999795,0.9167632275716172 +261.5999999999979,0.9148026199356762 +261.6999999999979,0.912821693415382 +261.7999999999979,0.9108213163180233 +261.89999999999793,0.9088022429567041 +261.9999999999979,0.9067647711629748 +262.09999999999786,0.9047098860027284 +262.1999999999979,0.9026383653185359 +262.2999999999979,0.9005509421565918 +262.3999999999979,0.8984483890478937 +262.49999999999784,0.8963316328818863 +262.59999999999786,0.894201586389154 +262.6999999999979,0.8920590733586956 +262.79999999999785,0.8899049635812113 +262.8999999999978,0.8877401780970771 +262.99999999999784,0.8855658262416738 +263.09999999999786,0.8833828011284532 +263.19999999999783,0.8811921426412789 +263.2999999999978,0.8789948934541498 +263.3999999999978,0.8767921921624726 +263.49999999999784,0.8745849838351183 +263.5999999999978,0.8723744998292627 +263.6999999999978,0.8701618421684739 +263.7999999999978,0.8679479864560663 +263.8999999999978,0.8657340306545118 +263.9999999999978,0.8635215561410813 +264.09999999999775,0.861311451452525 +264.1999999999978,0.8591050376295402 +264.2999999999978,0.8569036214561416 +264.39999999999776,0.8547082111127946 +264.4999999999977,0.8525200935832988 +264.59999999999775,0.8503408542582144 +264.6999999999978,0.8481712897030138 +264.79999999999774,0.8460129917764797 +264.8999999999977,0.8438671237802859 +264.9999999999977,0.8417350912482828 +265.09999999999775,0.8396177959646552 +265.1999999999977,0.8375173428119874 +265.2999999999977,0.835434184028436 +265.3999999999977,0.8333701147395343 +265.4999999999977,0.8313262458057824 +265.5999999999977,0.8293040049857668 +265.69999999999766,0.827304637163126 +265.7999999999977,0.8253293251617159 +265.8999999999977,0.8233796598120212 +265.99999999999767,0.8214565251871246 +266.09999999999764,0.8195617500291257 +266.19999999999766,0.8176959998010099 +266.2999999999977,0.8158610180103487 +266.39999999999765,0.8140576199192083 +266.4999999999976,0.8122876669440517 +266.59999999999764,0.8105516418748959 +266.69999999999766,0.8088511838708727 +266.7999999999976,0.8071874428735512 +266.8999999999976,0.8055616283154478 +266.9999999999976,0.8039745857558516 +267.09999999999764,0.8024279586411249 +267.1999999999976,0.8009223049753217 +267.29999999999757,0.7994589751193862 +267.3999999999976,0.798039281859173 +267.4999999999976,0.796663811808208 +267.5999999999976,0.7953337995009153 +267.69999999999754,0.7940501252804111 +267.79999999999757,0.7928137829433611 +267.8999999999976,0.7916254555475937 +267.99999999999756,0.7904865523947724 +268.0999999999975,0.789397466256369 +268.19999999999754,0.7883591925340242 +268.29999999999757,0.7873724920580741 +268.39999999999753,0.7864381486498906 +268.4999999999975,0.7855567404368192 +268.5999999999975,0.784728864736532 +268.69999999999754,0.7839556002132645 +268.7999999999975,0.7832371079225532 +268.8999999999975,0.7825739200760979 +268.9999999999975,0.7819668999276385 +269.0999999999975,0.7814160340120163 +269.1999999999975,0.7809221999936584 +269.29999999999745,0.7804853885192558 +269.3999999999975,0.780106261778171 +269.4999999999975,0.7797848593781957 +269.59999999999746,0.7795216822754591 +269.69999999999743,0.779316660491125 +269.79999999999745,0.7791701831803391 +269.8999999999975,0.7790822901971391 +269.99999999999744,0.7790529295022298 +270.0999999999974,0.7790822901971362 +270.19999999999743,0.7791701831803333 +270.29999999999745,0.779316660491116 +270.3999999999974,0.7795216822754472 +270.4999999999974,0.7797848593781808 +270.5999999999974,0.7801062617781531 +270.69999999999743,0.7804853885192349 +270.7999999999974,0.7809221999936344 +270.89999999999736,0.7814160340119894 +270.9999999999974,0.781966899927609 +271.0999999999974,0.7825739200760653 +271.1999999999974,0.7832371079225178 +271.29999999999734,0.7839556002132262 +271.39999999999736,0.7847288647364913 +271.4999999999974,0.7855567404367753 +271.59999999999735,0.7864381486498444 +271.6999999999973,0.7873724920580248 +271.79999999999734,0.7883591925339727 +271.89999999999736,0.7893974662563142 +271.9999999999973,0.7904865523947157 +272.0999999999973,0.7916254555475338 +272.1999999999973,0.7928137829432991 +272.29999999999734,0.7940501252803466 +272.3999999999973,0.7953337995008485 +272.49999999999727,0.7966638118081385 +272.5999999999973,0.798039281859102 +272.6999999999973,0.7994589751193124 +272.7999999999973,0.8009223049752462 +272.89999999999725,0.8024279586410468 +272.99999999999727,0.8039745857557716 +273.0999999999973,0.8055616283153654 +273.19999999999726,0.8071874428734674 +273.2999999999972,0.8088511838707864 +273.39999999999725,0.8105516418748082 +273.49999999999727,0.8122876669439619 +273.59999999999724,0.8140576199191174 +273.6999999999972,0.8158610180102553 +273.7999999999972,0.8176959998009146 +273.89999999999725,0.8195617500290292 +273.9999999999972,0.8214565251870263 +274.0999999999972,0.823379659811922 +274.1999999999972,0.825329325161615 +274.2999999999972,0.8273046371630243 +274.3999999999972,0.8293040049856637 +274.49999999999716,0.8313262458056782 +274.5999999999972,0.8333701147394285 +274.6999999999972,0.83543418402833 +274.79999999999717,0.8375173428118798 +274.89999999999714,0.8396177959645471 +274.99999999999716,0.8417350912481735 +275.0999999999972,0.8438671237801768 +275.19999999999715,0.846012991776369 +275.2999999999971,0.8481712897029035 +275.39999999999714,0.8503408542581026 +275.49999999999716,0.8525200935831873 +275.5999999999971,0.8547082111126819 +275.6999999999971,0.8569036214560293 +275.7999999999971,0.8591050376294271 +275.89999999999714,0.8613114514524121 +275.9999999999971,0.8635215561409677 +276.09999999999707,0.8657340306543986 +276.1999999999971,0.8679479864559527 +276.2999999999971,0.870161842168361 +276.3999999999971,0.8723744998291495 +276.49999999999704,0.8745849838350055 +276.59999999999707,0.8767921921623595 +276.6999999999971,0.8789948934540375 +276.79999999999706,0.8811921426411664 +276.899999999997,0.8833828011283416 +276.99999999999704,0.8855658262415621 +277.09999999999707,0.8877401780969664 +277.19999999999703,0.8899049635811007 +277.299999999997,0.8920590733585857 +277.399999999997,0.8942015863890442 +277.49999999999704,0.896331632881778 +277.599999999997,0.8984483890477857 +277.699999999997,0.9005509421564848 +277.799999999997,0.9026383653184291 +277.899999999997,0.9047098860026226 +277.999999999997,0.9067647711628699 +278.09999999999695,0.9088022429566005 +278.199999999997,0.9108213163179203 +278.299999999997,0.9128216934152794 +278.39999999999696,0.9148026199355752 +278.49999999999693,0.9167632275715181 +278.59999999999695,0.9187029399481644 +278.699999999997,0.9206213479159067 +278.79999999999694,0.9225177989874231 +278.8999999999969,0.9243915979409171 +278.99999999999693,0.9265118755549029 +279.09999999999695,0.9283239712708176 +279.1999999999969,0.9301128361945319 +279.2999999999969,0.9318780929031328 +279.3999999999969,0.9336192425132965 +279.49999999999693,0.9353360888453801 +279.5999999999969,0.9370280252625203 +279.69999999999686,0.9386946641867026 +279.7999999999969,0.940336027450731 +279.8999999999969,0.9422296286922263 +279.9999999999969,0.9438038796904169 +280.09999999999684,0.9453528127673457 +280.19999999999686,0.9468760956622742 +280.2999999999969,0.9488159461080651 +280.39999999999685,0.9502621233642097 +280.4999999999968,0.9516834095517708 +280.59999999999684,0.9530791531075727 +280.69999999999686,0.9544496788330572 +280.7999999999968,0.9557950166172413 +280.8999999999968,0.9571146393983789 +280.9999999999968,0.9584084967362535 +281.09999999999684,0.9601216565744407 +281.1999999999968,0.9613397782064861 +281.29999999999677,0.9625333232966583 +281.3999999999968,0.9637026547581251 +281.4999999999968,0.9648472444023236 +281.5999999999968,0.9659671675157148 +281.69999999999675,0.9670629362123248 +281.79999999999677,0.9681344308367034 +281.8999999999968,0.9694567938581022 +281.99999999999676,0.9704652371475526 +282.0999999999967,0.9714503713009535 +282.19999999999675,0.972412299189643 +282.29999999999677,0.9733511058865072 +282.39999999999674,0.9745473594335979 +282.4999999999967,0.9754247319074213 +282.5999999999967,0.9762801253883148 +282.69999999999675,0.9775559674308707 +282.7999999999967,0.9783445997987942 +282.8999999999967,0.9793855966114281 +282.9999999999967,0.9801187588430257 +283.0999999999967,0.9808330241866252 +283.1999999999967,0.9815284464119463 +283.29999999999666,0.982205051427374 +283.3999999999967,0.9828632868341628 +283.4999999999967,0.983503323709164 +283.59999999999667,0.9841253028165605 +283.69999999999663,0.985006104792547 +283.79999999999666,0.9855775232941575 +283.8999999999967,0.9861322768405115 +283.99999999999665,0.9866707156980582 +284.0999999999966,0.9871931127297486 +284.19999999999663,0.9876996272885579 +284.29999999999666,0.9881906087639126 +284.3999999999966,0.9886663676234652 +284.4999999999966,0.9891270078909034 +284.5999999999966,0.9895727748273249 +284.69999999999663,0.9902846948919587 +284.7999999999966,0.9909564837083439 +284.89999999999657,0.991761647445282 +284.9999999999966,0.992100619348084 +285.0999999999966,0.9924286385972915 +285.1999999999966,0.9927458258718279 +285.29999999999654,0.9930524672528573 +285.39999999999657,0.9933488368560186 +285.4999999999966,0.9936350480448493 +285.59999999999656,0.9939112767730072 +285.6999999999965,0.9944504930671751 +285.79999999999654,0.994692526152286 +285.89999999999657,0.9949259488592066 +285.99999999999653,0.9951511231140882 +286.0999999999965,0.995368236508365 +286.1999999999965,0.9955772527273301 +286.29999999999654,0.9960472652400773 +286.3999999999965,0.9962261541868147 +286.4999999999965,0.996398447844784 +286.5999999999965,0.9965641353462479 +286.6999999999965,0.996723565478653 +286.7999999999965,0.9968769462791998 +286.89999999999645,0.9970243190811173 +286.9999999999965,0.9976087898364364 +287.0999999999965,0.9977207741235262 +287.19999999999646,0.9978284279581242 +287.29999999999643,0.9979317463253479 +287.39999999999645,0.9980309991807464 +287.4999999999965,0.9981263121431563 +287.59999999999644,0.9982177474130017 +287.6999999999964,0.9983053322791657 +287.79999999999643,0.9986591188480589 +287.89999999999645,0.9987252524503017 +287.9999999999964,0.9987885745154013 +288.0999999999964,0.9988492120595267 +288.1999999999964,0.9989072993778636 +288.29999999999643,0.998962915806994 +288.3999999999964,0.9994496569793139 +288.49999999999636,0.9994766656783055 +288.5999999999964,0.9995025710990697 +288.6999999999964,0.9995274061178342 +288.7999999999964,0.9995511522922561 +288.89999999999634,0.9995738948207928 +288.99999999999636,0.9995956663829667 +289.0999999999964,0.9996164931224096 +289.19999999999635,0.9996363712307897 +289.2999999999963,0.9996553735512799 +289.39999999999634,0.9996735298016994 +289.49999999999636,0.9996908616337399 +289.5999999999963,0.9997073745150422 +289.6999999999963,1.0000000000000002 +289.7999999999963,1.0000000000000002 +289.89999999999634,1.0000000000000002 +289.9999999999963,1.0000000000000002 +290.09999999999627,1.0000000000000002 +290.1999999999963,1.0000000000000002 +290.2999999999963,1.0000000000000002 +290.3999999999963,1.0000000000000002 +290.49999999999625,1.0000000000000002 +290.59999999999627,1.0000000000000002 +290.6999999999963,1.0000000000000002 +290.79999999999626,1.0000000000000002 +290.8999999999962,1.0000000000000002 +290.99999999999625,1.0000000000000002 +291.09999999999627,1.0000000000000002 +291.19999999999624,1.0000000000000002 +291.2999999999962,1.0000000000000002 +291.3999999999962,1.0000000000000002 +291.49999999999625,1.0000000000000002 +291.5999999999962,1.0000000000000002 +291.6999999999962,1.0000000000000002 +291.7999999999962,1.0000000000000002 +291.8999999999962,1.0000000000000002 +291.9999999999962,1.0000000000000002 +292.09999999999616,1.0000000000000002 +292.1999999999962,1.0000000000000002 +292.2999999999962,1.0000000000000002 +292.39999999999617,1.0000000000000002 +292.49999999999613,1.0000000000000002 +292.59999999999616,1.0000000000000002 +292.6999999999962,1.0000000000000002 +292.79999999999615,1.0000000000000002 +292.8999999999961,1.0000000000000002 +292.99999999999613,1.0000000000000002 +293.09999999999616,1.0000000000000002 +293.1999999999961,1.0000000000000002 +293.2999999999961,1.0000000000000002 +293.3999999999961,1.0000000000000002 +293.49999999999613,1.0000000000000002 +293.5999999999961,1.0000000000000002 +293.69999999999607,1.0000000000000002 +293.7999999999961,1.0000000000000002 +293.8999999999961,1.0000000000000002 +293.9999999999961,1.0000000000000002 +294.09999999999604,1.0000000000000002 +294.19999999999607,1.0000000000000002 +294.2999999999961,1.0000000000000002 +294.39999999999606,1.0000000000000002 +294.499999999996,1.0000000000000002 +294.59999999999604,1.0000000000000002 +294.69999999999607,1.0000000000000002 +294.79999999999603,1.0000000000000002 +294.899999999996,1.0000000000000002 +294.999999999996,1.0000000000000002 +295.09999999999604,1.0000000000000002 +295.199999999996,1.0000000000000002 +295.299999999996,1.0000000000000002 +295.399999999996,1.0000000000000002 +295.499999999996,1.0000000000000002 +295.599999999996,1.0000000000000002 +295.69999999999595,1.0000000000000002 +295.799999999996,1.0000000000000002 +295.899999999996,1.0000000000000002 +295.99999999999596,1.0000000000000002 +296.09999999999593,1.0000000000000002 +296.19999999999595,1.0000000000000002 +296.299999999996,1.0000000000000002 +296.39999999999594,1.0000000000000002 +296.4999999999959,1.0000000000000002 +296.59999999999593,1.0000000000000002 +296.69999999999595,1.0000000000000002 +296.7999999999959,1.0000000000000002 +296.8999999999959,1.0000000000000002 +296.9999999999959,1.0000000000000002 +297.09999999999593,1.0000000000000002 +297.1999999999959,1.0000000000000002 +297.29999999999586,1.0000000000000002 +297.3999999999959,1.0000000000000002 +297.4999999999959,1.0000000000000002 +297.5999999999959,1.0000000000000002 +297.69999999999584,1.0000000000000002 +297.79999999999586,1.0000000000000002 +297.8999999999959,1.0000000000000002 +297.99999999999585,1.0000000000000002 +298.0999999999958,1.0000000000000002 +298.19999999999584,1.0000000000000002 +298.29999999999586,1.0000000000000002 +298.3999999999958,1.0000000000000002 +298.4999999999958,1.0000000000000002 +298.5999999999958,1.0000000000000002 +298.69999999999584,1.0000000000000002 +298.7999999999958,1.0000000000000002 +298.89999999999577,1.0000000000000002 +298.9999999999958,1.0000000000000002 +299.0999999999958,1.0000000000000002 +299.1999999999958,1.0000000000000002 +299.29999999999575,1.0000000000000002 +299.39999999999577,1.0000000000000002 +299.4999999999958,1.0000000000000002 +299.59999999999576,1.0000000000000002 +299.6999999999957,1.0000000000000002 +299.79999999999575,1.0000000000000002 +299.89999999999577,1.0000000000000002 +299.99999999999574,1.0000000000000002 +300.0999999999957,1.0000000000000002 +300.1999999999957,1.0000000000000002 +300.29999999999575,1.0000000000000002 +300.3999999999957,1.0000000000000002 +300.4999999999957,1.0000000000000002 +300.5999999999957,1.0000000000000002 +300.6999999999957,1.0000000000000002 +300.7999999999957,1.0000000000000002 +300.89999999999566,1.0000000000000002 +300.9999999999957,1.0000000000000002 +301.0999999999957,1.0000000000000002 +301.19999999999567,1.0000000000000002 +301.29999999999563,1.0000000000000002 +301.39999999999566,1.0000000000000002 +301.4999999999957,1.0000000000000002 +301.59999999999565,1.0000000000000002 +301.6999999999956,1.0000000000000002 +301.79999999999563,1.0000000000000002 +301.89999999999566,1.0000000000000002 +301.9999999999956,1.0000000000000002 +302.0999999999956,1.0000000000000002 +302.1999999999956,1.0000000000000002 +302.29999999999563,1.0000000000000002 +302.3999999999956,1.0000000000000002 +302.49999999999557,1.0000000000000002 +302.5999999999956,1.0000000000000002 +302.6999999999956,1.0000000000000002 +302.7999999999956,1.0000000000000002 +302.89999999999554,1.0000000000000002 +302.99999999999557,1.0000000000000002 +303.0999999999956,1.0000000000000002 +303.19999999999555,1.0000000000000002 +303.2999999999955,1.0000000000000002 +303.39999999999554,1.0000000000000002 +303.49999999999557,1.0000000000000002 +303.59999999999553,1.0000000000000002 +303.6999999999955,1.0000000000000002 +303.7999999999955,1.0000000000000002 +303.89999999999554,1.0000000000000002 +303.9999999999955,1.0000000000000002 +304.0999999999955,1.0000000000000002 +304.1999999999955,1.0000000000000002 +304.2999999999955,1.0000000000000002 +304.3999999999955,1.0000000000000002 +304.49999999999545,1.0000000000000002 +304.5999999999955,1.0000000000000002 +304.6999999999955,1.0000000000000002 +304.79999999999546,1.0000000000000002 +304.89999999999543,1.0000000000000002 +304.99999999999545,1.0000000000000002 +305.0999999999955,1.0000000000000002 +305.19999999999544,1.0000000000000002 +305.2999999999954,1.0000000000000002 +305.39999999999543,1.0000000000000002 +305.49999999999545,1.0000000000000002 +305.5999999999954,1.0000000000000002 +305.6999999999954,1.0000000000000002 +305.7999999999954,1.0000000000000002 +305.89999999999543,1.0000000000000002 +305.9999999999954,1.0000000000000002 +306.09999999999536,1.0000000000000002 +306.1999999999954,1.0000000000000002 +306.2999999999954,1.0000000000000002 +306.3999999999954,1.0000000000000002 +306.49999999999534,1.0000000000000002 +306.59999999999536,1.0000000000000002 +306.6999999999954,1.0000000000000002 +306.79999999999535,1.0000000000000002 +306.8999999999953,1.0000000000000002 +306.99999999999534,1.0000000000000002 +307.09999999999536,1.0000000000000002 +307.1999999999953,1.0000000000000002 +307.2999999999953,1.0000000000000002 +307.3999999999953,1.0000000000000002 +307.49999999999534,1.0000000000000002 +307.5999999999953,1.0000000000000002 +307.69999999999527,1.0000000000000002 +307.7999999999953,1.0000000000000002 +307.8999999999953,1.0000000000000002 +307.9999999999953,1.0000000000000002 +308.09999999999525,1.0000000000000002 +308.19999999999527,1.0000000000000002 +308.2999999999953,1.0000000000000002 +308.39999999999526,1.0000000000000002 +308.4999999999952,1.0000000000000002 +308.59999999999525,1.0000000000000002 +308.69999999999527,1.0000000000000002 +308.79999999999524,1.0000000000000002 +308.8999999999952,1.0000000000000002 +308.9999999999952,1.0000000000000002 +309.09999999999525,1.0000000000000002 +309.1999999999952,1.0000000000000002 +309.2999999999952,1.0000000000000002 +309.3999999999952,1.0000000000000002 +309.4999999999952,1.0000000000000002 +309.5999999999952,1.0000000000000002 +309.69999999999516,1.0000000000000002 +309.7999999999952,1.0000000000000002 +309.8999999999952,1.0000000000000002 +309.99999999999517,1.0000000000000002 +310.09999999999513,1.0000000000000002 +310.19999999999516,1.0000000000000002 +310.2999999999952,1.0000000000000002 +310.39999999999515,1.0000000000000002 +310.4999999999951,1.0000000000000002 +310.59999999999513,1.0000000000000002 +310.69999999999516,1.0000000000000002 +310.7999999999951,1.0000000000000002 +310.8999999999951,1.0000000000000002 +310.9999999999951,1.0000000000000002 +311.09999999999513,1.0000000000000002 +311.1999999999951,1.0000000000000002 +311.29999999999507,1.0000000000000002 +311.3999999999951,1.0000000000000002 +311.4999999999951,1.0000000000000002 +311.5999999999951,1.0000000000000002 +311.69999999999504,1.0000000000000002 +311.79999999999507,1.0000000000000002 +311.8999999999951,1.0000000000000002 +311.99999999999505,1.0000000000000002 +312.099999999995,1.0000000000000002 +312.19999999999504,1.0000000000000002 +312.29999999999507,1.0000000000000002 +312.39999999999503,1.0000000000000002 +312.499999999995,1.0000000000000002 +312.599999999995,1.0000000000000002 +312.69999999999504,1.0000000000000002 +312.799999999995,1.0000000000000002 +312.899999999995,1.0000000000000002 +312.999999999995,1.0000000000000002 +313.099999999995,1.0000000000000002 +313.199999999995,1.0000000000000002 +313.29999999999495,1.0000000000000002 +313.399999999995,1.0000000000000002 +313.499999999995,1.0000000000000002 +313.59999999999496,1.0000000000000002 +313.69999999999493,1.0000000000000002 +313.79999999999495,1.0000000000000002 +313.899999999995,1.0000000000000002 +313.99999999999494,1.0000000000000002 +314.0999999999949,1.0000000000000002 +314.19999999999493,1.0000000000000002 +314.29999999999495,1.0000000000000002 +314.3999999999949,1.0000000000000002 +314.4999999999949,1.0000000000000002 +314.5999999999949,1.0000000000000002 +314.69999999999493,1.0000000000000002 +314.7999999999949,1.0000000000000002 +314.89999999999486,1.0000000000000002 diff --git a/examples/inputs/cc.yaml b/examples/inputs/cc.yaml index 1935c004f3..b1084ec72f 100644 --- a/examples/inputs/cc.yaml +++ b/examples/inputs/cc.yaml @@ -1,8 +1,6 @@ - name: CC description: Three turbines using Cumulative Gauss Curl model -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - nrel_5MW - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,54 +33,29 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: cc - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.01 - constant: 0.9 - ai: 0.83 - downstream: -0.25 + model: cc + parameters: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + combination_model: sosfs diff --git a/examples/inputs/emgauss.yaml b/examples/inputs/emgauss.yaml index 40f8fab8ee..55558b11b8 100644 --- a/examples/inputs/emgauss.yaml +++ b/examples/inputs/emgauss.yaml @@ -1,8 +1,6 @@ - -name: Emperical Gaussian -description: Three turbines using emperical Gaussian model -floris_version: v4 - +name: Empirical Gaussian +description: Three turbines using Empirical Gaussian model +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - nrel_5MW - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,70 +33,21 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: true - enable_active_wake_mixing: false - enable_transverse_velocities: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + combination_model: sosfs diff --git a/examples/inputs/emgauss_helix.yaml b/examples/inputs/emgauss_helix.yaml index 48a6add0d7..3a5cfce9a6 100644 --- a/examples/inputs/emgauss_helix.yaml +++ b/examples/inputs/emgauss_helix.yaml @@ -1,8 +1,6 @@ - name: Emperical Gaussian description: Three turbines using empirical Gaussian model -floris_version: v4.0 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - iea_15MW - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,72 +33,25 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_active_wake_mixing: true - enable_transverse_velocities: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 30 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - awc_wake_exp: 1.2 - awc_wake_denominator: 400 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 30 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + awc_wake_exp: 1.2 + awc_wake_denominator: 400 + enable_yaw_added_recovery: false + enable_active_wake_mixing: true + combination_model: sosfs diff --git a/examples/inputs/gch.yaml b/examples/inputs/gch.yaml index 29e0e2de21..0315218be4 100644 --- a/examples/inputs/gch.yaml +++ b/examples/inputs/gch.yaml @@ -157,87 +157,29 @@ flow_field: wake: ### - # Group for selecting the model elements for the simulation. + # Wake model to use. String type. # See :py:mod:`~.wake` for a list of available models and their descriptions. - model_strings: - - ### - # Wake combination model. String type.. - combination_model: sosfs - - ### - # Wake deflection model. String type. - deflection_model: gauss - - ### - # Wake turbulence model. String type.. - turbulence_model: crespo_hernandez - - ### - # Wake velocity deficit model. String type. - velocity_model: gauss - - ### - # Flag to include secondary steering effects. Only used in some models. Boolean type. - enable_secondary_steering: true - - ### - # Flag to include yaw added recovery effects. Only used in some models. Boolean type. - enable_yaw_added_recovery: true - - ### - # Flag to include active wake mixing effects. Only used in Empirical Guassian model. Boolean type. - enable_active_wake_mixing: false + model: gauss ### - # Flag to compute transverse velocities across turbine rotors. Only used in some models. - # Boolean type. - enable_transverse_velocities: true - - ### - # Parameters for the wake deflection model. See model descriptions and implementations for - # details of each parameter and its use. - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - ### - # Parameters for the wake velocity deficit model. See model descriptions and implementations for - # details of each parameter and its use. - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - ### - # Parameters for the wake turbulence model. See model descriptions and implementations for + # Parameters for the wake model. See model descriptions and implementations for # details of each parameter and its use. - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + parameters: + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + + ### + # Wake combination model to use. String type. + combination_model: sosfs diff --git a/examples/inputs/gch_heterogeneous_inflow.yaml b/examples/inputs/gch_heterogeneous_inflow.yaml index 28f9bf6f5f..cf59f6fa2c 100644 --- a/examples/inputs/gch_heterogeneous_inflow.yaml +++ b/examples/inputs/gch_heterogeneous_inflow.yaml @@ -1,7 +1,6 @@ name: GCH description: Three turbines using Gauss Curl Hybrid model -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -9,7 +8,6 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 1 @@ -24,25 +22,24 @@ farm: - 0.0 turbine_type: - nrel_5MW - flow_field: air_density: 1.225 heterogeneous_inflow_config: speed_multipliers: - - - 2.0 - - 1.0 - - 2.0 - - 1.0 + - - 2.0 + - 1.0 + - 2.0 + - 1.0 x: - - -300. - - -300. - - 2600. - - 2600. + - -300.0 + - -300.0 + - 2600.0 + - 2600.0 y: - - -300. - - 300. - - -300. - - 300. + - -300.0 + - 300.0 + - -300.0 + - 300.0 reference_wind_height: -1 turbulence_intensities: - 0.06 @@ -52,53 +49,18 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: gauss + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/examples/inputs/gch_multi_dim_cp_ct.yaml b/examples/inputs/gch_multi_dim_cp_ct.yaml index b3d1e27ed7..91b33693d7 100644 --- a/examples/inputs/gch_multi_dim_cp_ct.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct.yaml @@ -1,8 +1,6 @@ - name: GCH multi dimensional power/thrust coefficient description: Three turbines using GCH model -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,13 +22,12 @@ farm: - 0.0 turbine_type: - iea_15MW_floating_multi_dim_cp_ct - flow_field: multidim_conditions: Tp: 2.5 Hs: 3.01 air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -41,74 +36,18 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - turbopark: - A: 0.04 - sigma_max_rel: 4.0 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.01 - constant: 0.9 - ai: 0.83 - downstream: -0.25 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: gauss + parameters: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/examples/inputs/gch_multi_dim_cp_ct_TI.yaml b/examples/inputs/gch_multi_dim_cp_ct_TI.yaml index 23969b4767..6a4e133619 100644 --- a/examples/inputs/gch_multi_dim_cp_ct_TI.yaml +++ b/examples/inputs/gch_multi_dim_cp_ct_TI.yaml @@ -1,8 +1,6 @@ - name: GCH multi dimensional power/thrust coefficient description: Three turbines using GCH model -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,13 +22,12 @@ farm: - 0.0 turbine_type: - iea_15MW_multi_dim_TI.yaml - turbine_library_path: ../inputs/turbine_files/ - + external_turbine_library_path: ../inputs/turbine_files/ flow_field: multidim_conditions: TI: 0.06 air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -41,74 +36,18 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - turbopark: - A: 0.04 - sigma_max_rel: 4.0 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.01 - constant: 0.9 - ai: 0.83 - downstream: -0.25 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: gauss + parameters: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/examples/inputs/gch_multiple_turbine_types.yaml b/examples/inputs/gch_multiple_turbine_types.yaml index 80682aa281..08da1e01f1 100644 --- a/examples/inputs/gch_multiple_turbine_types.yaml +++ b/examples/inputs/gch_multiple_turbine_types.yaml @@ -1,8 +1,6 @@ - name: GCH description: Three turbines using Gauss Curl Hybrid model -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -25,10 +21,9 @@ farm: turbine_type: - nrel_5MW - iea_10MW - flow_field: air_density: 1.225 - reference_wind_height: 90.0 # Since multiple defined turbines, must specify explicitly the reference wind height + reference_wind_height: 90.0 turbulence_intensities: - 0.06 wind_directions: @@ -37,54 +32,18 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: gauss + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/examples/inputs/jensen.yaml b/examples/inputs/jensen.yaml index f3b81747d6..3a9d69ad6c 100644 --- a/examples/inputs/jensen.yaml +++ b/examples/inputs/jensen.yaml @@ -1,8 +1,6 @@ - name: Jensen-Jimenez description: Three turbines using Jensen / Jimenez models -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - nrel_5MW - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,54 +33,15 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: jimenez - turbulence_model: crespo_hernandez - velocity_model: jensen - - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: jensen + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + bd: 0.0 + kd: 0.05 + we: 0.05 + combination_model: sosfs diff --git a/examples/inputs/turbopark.yaml b/examples/inputs/turbopark.yaml deleted file mode 100644 index c4ffbfa439..0000000000 --- a/examples/inputs/turbopark.yaml +++ /dev/null @@ -1,94 +0,0 @@ - -name: TurbOPark -description: Three turbines using TurbOPark model -floris_version: v4 - -logging: - console: - enable: false - level: WARNING - file: - enable: false - level: WARNING - -solver: - type: turbine_grid - turbine_grid_points: 1 - -farm: - layout_x: - - 0.0 - - 630.0 - - 1260.0 - layout_y: - - 0.0 - - 0.0 - - 0.0 - turbine_type: - - nrel_5MW - -flow_field: - air_density: 1.225 - reference_wind_height: 90.0 - turbulence_intensities: - - 0.06 - wind_directions: - - 270.0 - wind_shear: 0.12 - wind_speeds: - - 8.0 - wind_veer: 0.0 - -wake: - model_strings: - combination_model: fls - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: turbopark - - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - turbopark: - A: 0.04 - sigma_max_rel: 4.0 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 diff --git a/examples/inputs/turbopark_cubature.yaml b/examples/inputs/turbopark_cubature.yaml deleted file mode 100644 index 11805f0d45..0000000000 --- a/examples/inputs/turbopark_cubature.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: Case TwinPark FLORIS v4.0 -description: Two aligned wind turbines with 5D spacing as currently implemented in v4.0 - -floris_version: v4.0 - -logging: - console: - enable: true - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". - level: WARNING - file: - enable: false - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". - level: WARNING - -solver: - type: turbine_cubature_grid # turbine_grid - turbine_grid_points: 6 - -farm: - layout_x: - - 0.0 - layout_y: - - 0.0 - turbine_type: # Constant CT turbine (TODO: implement in script?) - - nrel_5MW # Will be replaced in script - -# Configure the atmospheric conditions. -flow_field: - air_density: 1.225 - reference_wind_height: -1 - turbulence_intensities: - - 0.06 - wind_directions: - - 270.0 - wind_speeds: - - 8.0 - wind_shear: 0.0 # Needed to reproduce Pedersen et al's results with new model - wind_veer: 0.0 - -# Configure the wake model. -wake: - model_strings: - combination_model: sosfs - deflection_model: none - turbulence_model: none - velocity_model: turbopark # current TurboPark model in FLORIS - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_active_wake_mixing: false - enable_transverse_velocities: false - wake_velocity_parameters: # Parameters for the wake velocity deficit model - turbopark: - A: 0.04 - sigma_max_rel: 4.0 - wake_deflection_parameters: - none: - wake_turbulence_parameters: - none: diff --git a/examples/inputs/turboparkgauss.yaml b/examples/inputs/turboparkgauss.yaml index f9274cb769..a8659027c2 100644 --- a/examples/inputs/turboparkgauss.yaml +++ b/examples/inputs/turboparkgauss.yaml @@ -1,8 +1,6 @@ - name: TurbOParkGauss description: Three turbines using TurbOParkGauss model -floris_version: v4 - +floris_version: v5 logging: console: enable: false @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: - type: turbine_cubature_grid # turboparkgauss does not work with type: turbine_grid - turbine_grid_points: 4 # 4 is sufficient in nearly all cases - + type: turbine_cubature_grid + turbine_grid_points: 4 farm: layout_x: - 0.0 @@ -26,7 +22,6 @@ farm: - 0.0 turbine_type: - nrel_5MW - flow_field: air_density: 1.225 reference_wind_height: -1 @@ -38,26 +33,9 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: none - turbulence_model: none - velocity_model: turboparkgauss - - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - none: - - wake_velocity_parameters: - turboparkgauss: - A: 0.04 - include_mirror_wake: true - - wake_turbulence_parameters: - none: + model: turboparkgauss + parameters: + A: 0.04 + include_mirror_wake: true + combination_model: sosfs diff --git a/examples/inputs/turboparkgauss_cubature.yaml b/examples/inputs/turboparkgauss_cubature.yaml index 05209fd927..7c56355876 100644 --- a/examples/inputs/turboparkgauss_cubature.yaml +++ b/examples/inputs/turboparkgauss_cubature.yaml @@ -1,59 +1,37 @@ name: Case TwinPark new implementation description: Two aligned wind turbines with 5D spacing under the new implementation - -floris_version: v4.0 - +floris_version: v5 logging: console: enable: true - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". level: WARNING file: enable: false - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". level: WARNING - solver: - type: turbine_cubature_grid # turboparkgauss does not work with type: turbine_grid - turbine_grid_points: 6 # 4 is sufficient in nearly all cases - + type: turbine_cubature_grid + turbine_grid_points: 6 farm: layout_x: - - 0.0 + - 0.0 layout_y: - - 0.0 - turbine_type: # Constant CT turbine (TODO: implement in script?) - - nrel_5MW # Will be replaced in script - -# Configure the atmospheric conditions. + - 0.0 + turbine_type: + - nrel_5MW flow_field: air_density: 1.225 reference_wind_height: -1 turbulence_intensities: - - 0.06 + - 0.06 wind_directions: - - 270.0 + - 270.0 wind_speeds: - - 8.0 - wind_shear: 0.0 # Needed to reproduce Pedersen et al's results + - 8.0 + wind_shear: 0.0 wind_veer: 0.0 - -# Configure the wake model. wake: - model_strings: - combination_model: sosfs - deflection_model: none - turbulence_model: none - velocity_model: turboparkgauss # New model - enable_secondary_steering: false - enable_yaw_added_recovery: false - enable_active_wake_mixing: false - enable_transverse_velocities: false - wake_velocity_parameters: # Parameters for the wake velocity deficit model - turboparkgauss: - A: 0.04 - include_mirror_wake: true - wake_deflection_parameters: - none: - wake_turbulence_parameters: - none: + model: turboparkgauss + parameters: + A: 0.04 + include_mirror_wake: true + combination_model: sosfs diff --git a/examples/inputs_floating/emgauss_fixed.yaml b/examples/inputs_floating/emgauss_fixed.yaml index cc72921804..b978c3e569 100644 --- a/examples/inputs_floating/emgauss_fixed.yaml +++ b/examples/inputs_floating/emgauss_fixed.yaml @@ -1,8 +1,6 @@ - name: Emperical Gaussian description: Example of single fixed-bottom turbine -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_fixed.yaml - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,70 +33,23 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: true - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + enable_yaw_added_recovery: true + enable_active_wake_mixing: false + combination_model: sosfs diff --git a/examples/inputs_floating/emgauss_floating.yaml b/examples/inputs_floating/emgauss_floating.yaml index 9a078adb7b..f49f29fd89 100644 --- a/examples/inputs_floating/emgauss_floating.yaml +++ b/examples/inputs_floating/emgauss_floating.yaml @@ -1,8 +1,6 @@ - name: Emperical Gaussian description: Example of single floating turbine -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -26,10 +22,9 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_floating.yaml - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -38,70 +33,23 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: true - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + enable_yaw_added_recovery: true + enable_active_wake_mixing: false + combination_model: sosfs diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml index ad8ac5dce7..ea6d20ad97 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt15.yaml @@ -1,8 +1,6 @@ - name: Emperical Gaussian floating description: Single turbine using emperical Gaussian model for floating -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -22,10 +18,9 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_floating_fixedtilt15.yaml - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -34,70 +29,23 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: true - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + enable_yaw_added_recovery: true + enable_active_wake_mixing: false + combination_model: sosfs diff --git a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml index 8f9d10fd2f..4750f220b8 100644 --- a/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml +++ b/examples/inputs_floating/emgauss_floating_fixedtilt5.yaml @@ -1,8 +1,6 @@ - name: Emperical Gaussian floating description: Single turbine using emperical Gaussian model for floating -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -22,10 +18,9 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_floating_fixedtilt5.yaml - flow_field: air_density: 1.225 - reference_wind_height: -1 # -1 is code for use the hub height + reference_wind_height: -1 turbulence_intensities: - 0.06 wind_directions: @@ -34,70 +29,23 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: empirical_gauss - turbulence_model: wake_induced_mixing - velocity_model: empirical_gauss - - enable_secondary_steering: false - enable_yaw_added_recovery: true - enable_transverse_velocities: false - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - empirical_gauss: - wake_expansion_rates: - - 0.023 - - 0.008 - breakpoints_D: - - 10 - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: empirical_gauss + parameters: + atmospheric_ti_gain: 0.0 + horizontal_deflection_gain_D: 3.0 + vertical_deflection_gain_D: -1 + deflection_rate: 22 + mixing_gain_deflection: 0.0 + yaw_added_mixing_gain: 0.0 + wake_expansion_rates: + - 0.023 + - 0.008 + breakpoints_D: + - 10 + sigma_0_D: 0.28 + smoothing_length_D: 2.0 + mixing_gain_velocity: 2.0 + enable_yaw_added_recovery: true + enable_active_wake_mixing: false + combination_model: sosfs diff --git a/examples/inputs_floating/gch_fixed.yaml b/examples/inputs_floating/gch_fixed.yaml index d9f9617012..1be952bcea 100644 --- a/examples/inputs_floating/gch_fixed.yaml +++ b/examples/inputs_floating/gch_fixed.yaml @@ -1,8 +1,6 @@ - name: GCH description: Example of single fixed-bottom turbine -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -10,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -22,7 +18,6 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_fixed.yaml - flow_field: air_density: 1.225 reference_wind_height: -1 @@ -34,54 +29,21 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: gauss + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + combination_model: sosfs diff --git a/examples/inputs_floating/gch_floating.yaml b/examples/inputs_floating/gch_floating.yaml index 4af183aca2..6eaa673ed2 100644 --- a/examples/inputs_floating/gch_floating.yaml +++ b/examples/inputs_floating/gch_floating.yaml @@ -1,9 +1,6 @@ - - name: GCH description: Example of single floating turbine -floris_version: v4 - +floris_version: v5 logging: console: enable: true @@ -11,11 +8,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -23,7 +18,6 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_floating.yaml - flow_field: air_density: 1.225 reference_wind_height: -1 @@ -35,54 +29,21 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: gauss + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + combination_model: sosfs diff --git a/examples/inputs_floating/gch_floating_defined_floating.yaml b/examples/inputs_floating/gch_floating_defined_floating.yaml index ecb5b3b0a5..51942d6377 100644 --- a/examples/inputs_floating/gch_floating_defined_floating.yaml +++ b/examples/inputs_floating/gch_floating_defined_floating.yaml @@ -1,8 +1,7 @@ - name: GCH -description: Example of single floating turbine where the cp/ct is calculated with floating tilt included -floris_version: v4 - +description: Example of single floating turbine where the cp/ct is calculated with + floating tilt included +floris_version: v5 logging: console: enable: true @@ -10,11 +9,9 @@ logging: file: enable: false level: WARNING - solver: type: turbine_grid turbine_grid_points: 3 - farm: layout_x: - 0.0 @@ -22,7 +19,6 @@ farm: - 0.0 turbine_type: - !include turbine_files/nrel_5MW_floating_defined_floating.yaml - flow_field: air_density: 1.225 reference_wind_height: -1 @@ -34,54 +30,21 @@ flow_field: wind_speeds: - 8.0 wind_veer: 0.0 - wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 + model: gauss + parameters: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + combination_model: sosfs diff --git a/floris/convert_floris_input_v3_to_v4.py b/floris/convert_floris_input_v3_to_v4.py deleted file mode 100644 index be2e99a507..0000000000 --- a/floris/convert_floris_input_v3_to_v4.py +++ /dev/null @@ -1,93 +0,0 @@ -import sys -from pathlib import Path - -import yaml - - -""" -This script is intended to be called with an argument and converts a floris input -yaml file specified for FLORIS v3 to one specified for FLORIS v4. - -Usage: -python convert_floris_input_v3_to_v4.py .yaml - -The resulting floris input file is placed in the same directory as the original yaml, -and is appended _v4. -""" - - -def ignore_include(loader, node): - # Parrot back the !include tag - return node.tag + " " + node.value - - -if __name__ == "__main__": - if len(sys.argv) != 2: - raise Exception( - "Usage: python convert_floris_input_v3_to_v4.py .yaml" - ) - - # Set the yaml loader to ignore the !include tag - yaml.SafeLoader.add_constructor("!include", ignore_include) - - input_yaml = sys.argv[1] - - # Handling the path and new filename - input_path = Path(input_yaml) - split_input = input_path.parts - [filename_v3, extension] = split_input[-1].split(".") - filename_v4 = filename_v3 + "_v4" - split_output = list(split_input[:-1]) + [filename_v4 + "." + extension] - output_path = Path(*split_output) - - # Load existing v3 model - with open(input_yaml, "r") as file: - v3_floris_input_dict = yaml.safe_load(file) - v4_floris_input_dict = v3_floris_input_dict.copy() - - # Change turbulence_intensity field to turbulence_intensities as list - if "turbulence_intensities" in v3_floris_input_dict["flow_field"]: - if "turbulence_intensity" in v3_floris_input_dict["flow_field"]: - del v4_floris_input_dict["flow_field"]["turbulence_intensity"] - elif "turbulence_intensity" in v3_floris_input_dict["flow_field"]: - v4_floris_input_dict["flow_field"]["turbulence_intensities"] = [ - v3_floris_input_dict["flow_field"]["turbulence_intensity"] - ] - del v4_floris_input_dict["flow_field"]["turbulence_intensity"] - - # Change multidim_cp_ct velocity model to gauss - if v3_floris_input_dict["wake"]["model_strings"]["velocity_model"] == "multidim_cp_ct": - print( - "multidim_cp_ct velocity model specified. Changing to gauss, " - + "but note that other velocity models are also compatible with multidimensional " - + "turbines in FLORIS v4. " - + "You will also need to convert your multidimensional turbine yaml files and their " - + "corresponding power/thrust csv files to be compatible with FLORIS v4 and to reflect " - + " the absolute power curve, rather than the power coefficient curve." - ) - v4_floris_input_dict["wake"]["model_strings"]["velocity_model"] = "gauss" - - # Add enable_active_wake_mixing field - v4_floris_input_dict["wake"]["enable_active_wake_mixing"] = False - - # Write the new v4 model to a new file, note that the in order to ignore the !include tag - # it is wrapped in single quotes by the ignore include/load/dump sequence and these will - # need to be removed in the next block of code - yaml.dump(v4_floris_input_dict, open(output_path, "w"), sort_keys=False) - - # Open the output file and loop through line by line - # if a line contains the substring !include, then strip all - # occurrences of ' from the line to remove the extra single quotes - # added by the ignore include/load/dump sequence - temp_output_path = output_path.with_name("temp.yaml") - with open(temp_output_path, "w") as file: - with open(output_path, "r") as f: - for line in f: - if "!include" in line: - line = line.replace("'", "") - file.write(line) - - # Move the temp file to the output file - temp_output_path.replace(output_path) - - print(output_path, "created.") diff --git a/floris/convert_floris_input_v4_to_v5.py b/floris/convert_floris_input_v4_to_v5.py new file mode 100644 index 0000000000..bef737a445 --- /dev/null +++ b/floris/convert_floris_input_v4_to_v5.py @@ -0,0 +1,171 @@ +import sys +from pathlib import Path + +import yaml + + +""" +This script is intended to be called with an argument and converts a floris input +yaml file specified for FLORIS v4 to one specified for FLORIS v5. + +Usage: +python convert_floris_input_v4_to_v5.py .yaml + +The resulting floris input file is placed in the same directory as the original yaml, +and is appended _v5. +""" + + +def ignore_include(loader, node): + # Parrot back the !include tag + return node.tag + " " + node.value + +def check_wake_model_compatibility(wake_v4): + + """ + Checks if the wake model specified in the v4 input file is compatible with v5. + If not, raises an exception. + + Args: + wake_v4 (dict): The wake model dictionary from the v4 input file. + """ + velocity_model = wake_v4["model_strings"]["velocity_model"] + deflection_model = wake_v4["model_strings"]["deflection_model"] + turbulence_model = wake_v4["model_strings"]["turbulence_model"] + + def deflection_model_warning(deflection_model, velocity_model, valid_deflection_model): + return ( + f"Deflection model '{deflection_model}' is not compatible with velocity model " + f"'{velocity_model}' in FLORIS v5. Only the '{valid_deflection_model}' deflection " + f"model is compatible with the '{velocity_model}' velocity model." + ) + + def turbulence_model_warning(turbulence_model, velocity_model, valid_turbulence_model): + return ( + f"Turbulence model '{turbulence_model}' is not compatible with velocity model " + f"'{velocity_model}' in FLORIS v5. Only the '{valid_turbulence_model}' turbulence " + f"model is compatible with the '{velocity_model}' velocity model." + ) + + if velocity_model == "turbopark": + print("The original TurbOPark velocity model is not compatible with FLORIS v5. " + "Please use the TurbOParkGauss velocity model instead.") + elif velocity_model == "gauss": + if deflection_model != "gauss": + raise Exception(deflection_model_warning(deflection_model, velocity_model, "gauss")) + if turbulence_model != "crespo_hernandez": + raise Exception( + turbulence_model_warning(turbulence_model, velocity_model, "crespo_hernandez") + ) + elif velocity_model == "jensen": + if deflection_model != "jimenez": + raise Exception(deflection_model_warning(deflection_model, velocity_model, "jimenez")) + if turbulence_model != "crespo_hernandez": + raise Exception( + turbulence_model_warning(turbulence_model, velocity_model, "crespo_hernandez") + ) + elif velocity_model == "empirical_gauss": + if deflection_model != "empirical_gauss": + raise Exception( + deflection_model_warning(deflection_model, velocity_model, "empirical_gauss") + ) + if turbulence_model != "wake_induced_mixing": + raise Exception( + turbulence_model_warning(turbulence_model, velocity_model, "wake_induced_mixing") + ) + elif velocity_model == "cc": + if deflection_model != "gauss": + raise Exception(deflection_model_warning(deflection_model, velocity_model, "gauss")) + elif velocity_model == "turboparkgauss": + if deflection_model != "none": + raise Exception(deflection_model_warning(deflection_model, velocity_model, "none")) + if turbulence_model != "none": + raise Exception( + turbulence_model_warning(turbulence_model, velocity_model, "none") + ) + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise Exception( + "Usage: python convert_floris_input_v4_to_v5.py .yaml" + ) + + # Set the yaml loader to ignore the !include tag + yaml.SafeLoader.add_constructor("!include", ignore_include) + + input_yaml = sys.argv[1] + + # Handling the path and new filename + input_path = Path(input_yaml) + split_input = input_path.parts + [filename_v4, extension] = split_input[-1].split(".") + filename_v5 = filename_v4 + "_v5" + split_output = list(split_input[:-1]) + [filename_v5 + "." + extension] + output_path = Path(*split_output) + + # Load existing v4 model + with open(input_yaml, "r") as file: + floris_input_dict = yaml.safe_load(file) + + # Reorganize wake model and disallow combinations that are not supported in v5 + wake_v4 = floris_input_dict["wake"] + check_wake_model_compatibility(wake_v4) + velocity_model_parameters_v4 = ( + wake_v4["wake_velocity_parameters"] + [wake_v4["model_strings"]["velocity_model"]] + ) or {} + deflection_model_parameters_v4 = ( + wake_v4["wake_deflection_parameters"] + [wake_v4["model_strings"]["deflection_model"]] + ) or {} + turbulence_model_parameters_v4 = ( + wake_v4["wake_turbulence_parameters"] + [wake_v4["model_strings"]["turbulence_model"]] + ) or {} + if wake_v4["model_strings"]["velocity_model"] in ["gauss", "cc"]: + enable_parameters_v4 = { + "enable_secondary_steering": wake_v4["enable_secondary_steering"], + "enable_yaw_added_recovery": wake_v4["enable_yaw_added_recovery"], + "enable_transverse_velocities": wake_v4["enable_transverse_velocities"], + } + elif wake_v4["model_strings"]["velocity_model"] in ["empirical_gauss"]: + enable_parameters_v4 = { + "enable_yaw_added_recovery": wake_v4["enable_yaw_added_recovery"], + "enable_active_wake_mixing": wake_v4["enable_active_wake_mixing"] + } + else: + enable_parameters_v4 = {} + + wake_v5 = { + "model": wake_v4["model_strings"]["velocity_model"], + "parameters": ( + turbulence_model_parameters_v4 | + deflection_model_parameters_v4 | + velocity_model_parameters_v4 | + enable_parameters_v4 + ), + "combination_model": wake_v4["model_strings"]["combination_model"], + } + + floris_input_dict["wake"] = wake_v5 + floris_input_dict["floris_version"] = "v5" + + with open(output_path, "w") as file: + yaml.dump(floris_input_dict, file, sort_keys=False) + + # Open the output file and loop through line by line + # if a line contains the substring !include, then strip all + # occurrences of ' from the line to remove the extra single quotes + # added by the ignore include/load/dump sequence + temp_output_path = output_path.with_name("temp.yaml") + with open(temp_output_path, "w") as file: + with open(output_path, "r") as f: + for line in f: + if "!include" in line: + line = line.replace("'", "") + file.write(line) + + # Move the temp file to the output file + temp_output_path.replace(output_path) + + print(output_path, "created.") diff --git a/floris/convert_turbine_v3_to_v4.py b/floris/convert_turbine_v3_to_v4.py deleted file mode 100644 index 5cf55f3d57..0000000000 --- a/floris/convert_turbine_v3_to_v4.py +++ /dev/null @@ -1,86 +0,0 @@ - -import sys -from pathlib import Path - -from floris.turbine_library import build_cosine_loss_turbine_dict, check_smooth_power_curve -from floris.utilities import load_yaml - - -""" -This script is intended to be called with an argument and converts a turbine -yaml file specified for FLORIS v3 to one specified for FLORIS v4. - -Usage: -python convert_turbine_v3_to_v4.py .yaml - -The resulting turbine is placed in the same directory as the original yaml, -and is appended _v4. -""" - - -if __name__ == "__main__": - if len(sys.argv) != 2: - raise Exception("Usage: python convert_turbine_v3_to_v4.py .yaml") - - input_yaml = sys.argv[1] - - # Handling the path and new filename - input_path = Path(input_yaml) - split_input = input_path.parts - [filename_v3, extension] = split_input[-1].split(".") - filename_v4 = filename_v3 + "_v4" - split_output = list(split_input[:-1]) + [filename_v4+"."+extension] - output_path = Path(*split_output) - - # Load existing v3 model - v3_turbine_dict = load_yaml(input_yaml) - - # Split into components expected by build_turbine_dict - power_thrust_table = v3_turbine_dict["power_thrust_table"] - if "power_thrust_data_file" in power_thrust_table: - raise ValueError( - "Cannot convert multidimensional turbine model. Please manually update your " - + "turbine yaml. Note that the power_thrust_data_file csv needs to be updated to " - + "reflect the absolute power curve, rather than the power coefficient curve," - + "and that `thrust` has been replaced by `thrust_coefficient`." - ) - power_thrust_table["power_coefficient"] = power_thrust_table["power"] - power_thrust_table["thrust_coefficient"] = power_thrust_table["thrust"] - power_thrust_table.pop("power") - power_thrust_table.pop("thrust") - - valid_properties = [ - "generator_efficiency", - "hub_height", - "cosine_loss_exponent_yaw", - "cosine_loss_exponent_tilt", - "rotor_diameter", - "TSR", - "ref_air_density", - "ref_tilt" - ] - - turbine_properties = {k:v for k,v in v3_turbine_dict.items() if k in valid_properties} - turbine_properties["ref_air_density"] = v3_turbine_dict["ref_density_cp_ct"] - turbine_properties["cosine_loss_exponent_yaw"] = v3_turbine_dict["pP"] - if "ref_tilt_cp_ct" in v3_turbine_dict: - turbine_properties["ref_tilt"] = v3_turbine_dict["ref_tilt_cp_ct"] - if "pT" in v3_turbine_dict: - turbine_properties["cosine_loss_exponent_tilt"] = v3_turbine_dict["pT"] - - # Convert to v4 and print new yaml - v4_turbine_dict = build_cosine_loss_turbine_dict( - power_thrust_table, - v3_turbine_dict["turbine_type"], - output_path, - **turbine_properties - ) - - if not check_smooth_power_curve( - v4_turbine_dict["power_thrust_table"]["power"], - tolerance=0.001 - ): - print( - "Non-smoothness detected in output power curve. ", - "Check above-rated power in generated v4 yaml file." - ) diff --git a/floris/core/__init__.py b/floris/core/__init__.py index e37f9c113d..4ed6965a2d 100644 --- a/floris/core/__init__.py +++ b/floris/core/__init__.py @@ -22,7 +22,7 @@ import floris.logging_manager -from .base import BaseClass, BaseModel, State +from .base import BaseClass, BaseLibrary, BaseModel, State from .turbine.turbine import ( axial_induction, power, @@ -36,7 +36,6 @@ ) from .farm import Farm from .grid import ( - FlowFieldGrid, FlowFieldPlanarGrid, Grid, PointsGrid, @@ -45,16 +44,6 @@ ) from .flow_field import FlowField from .wake import WakeModelManager -from .solver import ( - cc_solver, - empirical_gauss_solver, - full_flow_cc_solver, - full_flow_empirical_gauss_solver, - full_flow_sequential_solver, - full_flow_turbopark_solver, - sequential_solver, - turbopark_solver, -) from .core import Core # initialize the logger diff --git a/floris/core/base.py b/floris/core/base.py index 76c131597f..7979eadb27 100644 --- a/floris/core/base.py +++ b/floris/core/base.py @@ -1,4 +1,5 @@ +import importlib from abc import abstractmethod from enum import Enum from typing import ( @@ -63,3 +64,27 @@ def prepare_function() -> dict: @abstractmethod def function() -> None: raise NotImplementedError("BaseModel.function") + +@define +class BaseLibrary(BaseClass): + """ + Base class that writes the name and module of the class into the attrs dictionary. + """ + __classinfo__: dict = {"module": "", "name": ""} + def __attrs_post_init__(self) -> None: + self.__classinfo__ = { + "module": type(self).__module__, + "name": type(self).__name__ + } + + @staticmethod + def from_dict(data_dict): + """Recreate instance from dictionary with class information""" + data_noinfo = data_dict.copy() + class_info = data_noinfo.pop("__classinfo__") + + # Import the module and get the class + module = importlib.import_module(class_info["module"]) + cls = getattr(module, class_info["name"]) + + return cls(**data_noinfo) diff --git a/floris/core/core.py b/floris/core/core.py index 1b78423f2a..2ed336487f 100644 --- a/floris/core/core.py +++ b/floris/core/core.py @@ -10,25 +10,25 @@ from floris import logging_manager from floris.core import ( BaseClass, - cc_solver, - empirical_gauss_solver, + BaseLibrary, Farm, FlowField, - FlowFieldGrid, FlowFieldPlanarGrid, - full_flow_cc_solver, - full_flow_empirical_gauss_solver, - full_flow_sequential_solver, - full_flow_turbopark_solver, Grid, PointsGrid, - sequential_solver, State, TurbineCubatureGrid, TurbineGrid, - turbopark_solver, WakeModelManager, ) +from floris.core.wake_model import ( + CumulativeCurl, + EmpiricalGauss, + Gauss, + JensenJimenez, + NoneWake, + TurbOParkGauss, +) from floris.type_dec import NDArrayFloat from floris.utilities import ( load_yaml, @@ -56,7 +56,9 @@ class Core(BaseClass): description: str = field(converter=str) floris_version: str = field(converter=str) - grid: Grid = field(init=False) + grid: Grid | TurbineGrid | TurbineCubatureGrid | FlowFieldPlanarGrid | PointsGrid = field( + init=False + ) def __attrs_post_init__(self) -> None: @@ -71,23 +73,7 @@ def __attrs_post_init__(self) -> None: ) # Initialize farm quantities that depend on other objects - self.farm.construct_turbine_map() - self.farm.construct_turbine_thrust_coefficient_functions() - self.farm.construct_turbine_axial_induction_functions() - self.farm.construct_turbine_power_functions() - self.farm.construct_turbine_power_thrust_tables() - self.farm.construct_hub_heights() - self.farm.construct_rotor_diameters() - self.farm.construct_turbine_TSRs() - self.farm.construct_turbine_ref_tilts() - self.farm.construct_turbine_tilt_interps() - self.farm.construct_turbine_correct_cp_ct_for_tilt() - self.farm.set_yaw_angles_to_ref_yaw(self.flow_field.n_findex) - self.farm.set_tilt_to_ref_tilt(self.flow_field.n_findex) - self.farm.set_power_setpoints_to_ref_power(self.flow_field.n_findex) - self.farm.set_awc_modes_to_ref_mode(self.flow_field.n_findex) - self.farm.set_awc_amplitudes_to_ref_amp(self.flow_field.n_findex) - self.farm.set_awc_frequencies_to_ref_freq(self.flow_field.n_findex) + self.farm.set_control_setpoints_to_reference(self.flow_field.n_findex) if self.solver["type"] == "turbine_grid": self.grid = TurbineGrid( @@ -103,13 +89,6 @@ def __attrs_post_init__(self) -> None: wind_directions=self.flow_field.wind_directions, grid_resolution=self.solver["turbine_grid_points"], ) - elif self.solver["type"] == "flow_field_grid": - self.grid = FlowFieldGrid( - turbine_coordinates=self.farm.coordinates, - turbine_diameters=self.farm.rotor_diameters, - wind_directions=self.flow_field.wind_directions, - grid_resolution=self.solver["flow_field_grid_points"], - ) elif self.solver["type"] == "flow_field_planar_grid": self.grid = FlowFieldPlanarGrid( turbine_coordinates=self.farm.coordinates, @@ -129,10 +108,11 @@ def __attrs_post_init__(self) -> None: ) if isinstance(self.grid, (TurbineGrid, TurbineCubatureGrid)): - self.farm.expand_farm_properties( - self.flow_field.n_findex, - self.grid.sorted_coord_indices - ) + self.farm.set_sorted_indices(self.grid.sorted_coord_indices) + self.farm.construct_turbine_type_map() + + if isinstance(self.wake.model, dict): + self.wake.model = BaseLibrary.from_dict(self.wake.model) def initialize_domain(self): """Initialize solution space prior to wake calculations""" @@ -143,68 +123,15 @@ def initialize_domain(self): self.flow_field.initialize_velocity_field(self.grid) # Initialize farm quantities - self.farm.initialize(self.grid.sorted_indices) + self.farm.initialize() self.state.INITIALIZED - def steady_state_atmospheric_condition(self): + def solve_for_turbines(self): """Perform the steady-state wind farm wake calculations. Note that initialize_domain() is required to be called before this function.""" - vel_model = self.wake.model_strings["velocity_model"] - - if vel_model not in ["empirical_gauss"] and \ - self.farm.correct_cp_ct_for_tilt.any(): - self.logger.warning( - "The current model does not account for vertical wake deflection due to " + - "tilt. Corrections to power and thrust coefficient can be included, but no " + - "vertical wake deflection will occur." - ) - - operation_model_awc = False - for td in self.farm.turbine_definitions: - if "operation_model" in td and td["operation_model"] == "awc": - operation_model_awc = True - if vel_model != "empirical_gauss" and operation_model_awc: - self.logger.warning( - f"The current model `{vel_model}` does not account for additional wake mixing " + - "due to active wake control. Corrections to power and thrust coefficient can " + - "be included, but no enhanced wake recovery will occur." - ) - - if vel_model=="cc": - cc_solver( - self.farm, - self.flow_field, - self.grid, - self.wake - ) - elif vel_model=="turbopark": - self.logger.warning( - "The turbopark model has been superseded by the turboparkgauss model. We " + - "recommend using `velocity_model: turboparkgauss` instead." - ) - turbopark_solver( - self.farm, - self.flow_field, - self.grid, - self.wake - ) - elif vel_model=="empirical_gauss": - empirical_gauss_solver( - self.farm, - self.flow_field, - self.grid, - self.wake - ) - else: - sequential_solver( - self.farm, - self.flow_field, - self.grid, - self.wake - ) - + self.wake.model.turbine_solve(self.farm, self.flow_field, self.grid) self.finalize() def solve_for_viz(self): @@ -216,16 +143,8 @@ def solve_for_viz(self): self.flow_field.initialize_velocity_field(self.grid) - vel_model = self.wake.model_strings["velocity_model"] - - if vel_model=="cc": - full_flow_cc_solver(self.farm, self.flow_field, self.grid, self.wake) - elif vel_model=="turbopark": - full_flow_turbopark_solver(self.farm, self.flow_field, self.grid, self.wake) - elif vel_model=="empirical_gauss": - full_flow_empirical_gauss_solver(self.farm, self.flow_field, self.grid, self.wake) - else: - full_flow_sequential_solver(self.farm, self.flow_field, self.grid, self.wake) + # Solve wake at visualization points + self.wake.model.point_solve(self.farm, self.flow_field, self.grid) def solve_for_points(self, x, y, z): # Do the calculation with the TurbineGrid for a single wind speed @@ -249,19 +168,8 @@ def solve_for_points(self, x, y, z): self.flow_field.initialize_velocity_field(field_grid) - vel_model = self.wake.model_strings["velocity_model"] - - if vel_model == "turbopark": - raise NotImplementedError( - "solve_for_points is not available for the legacy \'turbopark\' model. " - "However, it is available for \'turboparkgauss\'." - ) - elif vel_model == "empirical_gauss": - full_flow_empirical_gauss_solver(self.farm, self.flow_field, field_grid, self.wake) - elif vel_model == "cc": - full_flow_cc_solver(self.farm, self.flow_field, field_grid, self.wake) - else: - full_flow_sequential_solver(self.farm, self.flow_field, field_grid, self.wake) + # Solve wake at specified points + self.wake.model.point_solve(self.farm, self.flow_field, field_grid) return self.flow_field.u_sorted[:,:,0,0] # Remove turbine grid dimensions @@ -283,6 +191,11 @@ def solve_for_velocity_deficit_profiles( for more details. """ + self.logger.warning( + "Velocity deficit profiles will move to a Numpy data structure in the next release. " + "See https://github.com/NatLabRockies/floris/pull/1194." + ) + # Create a grid that contains coordinates for all the sample points in all profiles. # Effectively, this is a grid of parallel lines. n_lines = len(downstream_dists) @@ -344,7 +257,7 @@ def finalize(self): # Once the wake calculation is finished, unsort the values to match # the user-supplied order of things. self.flow_field.finalize(self.grid.unsorted_indices) - self.farm.finalize(self.grid.unsorted_indices) + self.farm.finalize() self.state = State.USED ## I/O @@ -361,7 +274,7 @@ def from_file(cls, input_file_path: str | Path) -> Core: Floris: The class object instance. """ input_dict = load_yaml(Path(input_file_path).resolve()) - check_input_file_for_v3_keys(input_dict) + check_input_file_for_retired_keys(input_dict) return Core.from_dict(input_dict) def to_file(self, output_file_path: str) -> None: @@ -378,36 +291,22 @@ def to_file(self, output_file_path: str) -> None: default_flow_style=False ) -def check_input_file_for_v3_keys(input_dict) -> None: +def check_input_file_for_retired_keys(input_dict) -> None: """ - Checks if any FLORIS v3 keys are present in the input file and raises special errors if - the extra keys belong to a v3 definition of the input_dct. - and raises special errors if the extra arguments belong to a v3 definition of the class. + Checks if any FLORIS v4 keys are present in the input file and raises special errors if + the extra keys belong to a v4 definition of the input_dct. Args: input_dict (dict): The input dictionary to be checked for v3 keys. """ - v3_deprecation_msg = ( - "Consider using the convert_floris_input_v3_to_v4.py utility in floris/tools " - "to convert from a FLORIS v3 input file to FLORIS v4. " - "See https://natlabrockies.github.io/floris/upgrade_guides/v3_to_v4.html " + v4_deprecation_msg = ( + "Consider using the floris/convert_floris_input_v4_to_v5.py utility " + "to convert from a FLORIS v4 input file to FLORIS v5. " + "See https://natlabrockies.github.io/floris/upgrade_guides/v4_to_v5.html " "for more information." ) - if "turbulence_intensity" in input_dict["flow_field"]: - raise AttributeError( - "turbulence_intensity has been updated to turbulence_intensities in FLORIS v4. " - + v3_deprecation_msg - ) - elif not hasattr(input_dict["flow_field"]["turbulence_intensities"], "__len__"): - raise AttributeError( - "turbulence_intensities must be a list of floats in FLORIS v4. " - + v3_deprecation_msg - ) - - if input_dict["wake"]["model_strings"]["velocity_model"] == "multidim_cp_ct": + if "model_strings" in input_dict["wake"]: raise AttributeError( - "Dedicated 'multidim_cp_ct' velocity model has been removed in FLORIS v4 in favor of " - + "supporting all available wake models. To recover previous operation, set " - + "velocity_model to gauss. " - + v3_deprecation_msg + "The wake model specification has changed substantially in FLORIS v5. " + + v4_deprecation_msg ) diff --git a/floris/core/farm.py b/floris/core/farm.py index e447823cdb..56672857b2 100644 --- a/floris/core/farm.py +++ b/floris/core/farm.py @@ -1,29 +1,30 @@ import copy -from collections.abc import Callable from pathlib import Path from typing import ( Any, - Dict, List, ) import attrs import numpy as np -from attrs import define, field -from scipy.interpolate import interp1d +from attrs import ( + define, + field, + setters, +) from floris.core import ( BaseClass, State, Turbine, ) -from floris.core.rotor_velocity import compute_tilt_angles_for_floating_turbines_map from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT from floris.type_dec import ( convert_to_path, floris_array_converter, iter_validator, NDArrayFloat, + NDArrayInt, NDArrayObject, NDArrayStr, ) @@ -54,72 +55,58 @@ class Farm(BaseClass): turbine_type (list[dict | str]): A list of turbine definition dictionaries, or string references to the filename of the turbine type in either the FLORIS-provided turbine library (.../floris/turbine_library/), or a user-provided - :py:attr:`turbine_library_path`. - turbine_library_path (:obj:`str`): Either an absolute file path to the turbine library, or a - path relative to the file that is running the analysis. + :py:attr:`external_turbine_library_path`. + external_turbine_library_path (:obj:`str`): Either an absolute file path to the turbine + library, or a path relative to the file that is running the analysis. """ - layout_x: NDArrayFloat = field(converter=floris_array_converter) - layout_y: NDArrayFloat = field(converter=floris_array_converter) - # TODO: turbine_type should be immutable - turbine_type: List = field(validator=iter_validator(list, (dict, str))) - turbine_library_path: Path = field( - default=default_turbine_library_path, converter=convert_to_path + layout_x: NDArrayFloat = field(init=True, converter=floris_array_converter) + layout_y: NDArrayFloat = field(init=True, converter=floris_array_converter) + + turbine_type: List = field( + init=True, + validator=iter_validator(list, (dict, str)), + on_setattr=setters.frozen ) - turbine_definitions: list = field(init=False, validator=iter_validator(list, dict)) + external_turbine_library_path: Path = field( + init=True, + default=default_turbine_library_path, + converter=convert_to_path + ) - turbine_thrust_coefficient_functions: Dict[str, Callable] = field(init=False, factory=list) - turbine_axial_induction_functions: Dict[str, Callable] = field(init=False, factory=list) + # Generated after initialization + internal_turbine_library_path: Path = field(init=False, default=default_turbine_library_path) - turbine_tilt_interps: dict[str, interp1d] = field(init=False, factory=dict) + turbines: List[Turbine] = field(init=False, factory=list) + turbine_type_map_sorted: NDArrayObject = field(init=False, factory=list) + # TODO (later): Collect into a ControlSetpoint class yaw_angles: NDArrayFloat = field(init=False) - yaw_angles_sorted: NDArrayFloat = field(init=False) - - tilt_angles: NDArrayFloat = field(init=False) - tilt_angles_sorted: NDArrayFloat = field(init=False) - power_setpoints: NDArrayFloat = field(init=False) - power_setpoints_sorted: NDArrayFloat = field(init=False) - awc_modes: NDArrayStr = field(init=False) - awc_modes_sorted: NDArrayStr = field(init=False) - awc_amplitudes: NDArrayFloat = field(init=False) - awc_amplitudes_sorted: NDArrayFloat = field(init=False) - awc_frequencies: NDArrayFloat = field(init=False) - awc_frequencies_sorted: NDArrayFloat = field(init=False) + # TODO: Are TSRs control_setpoints? What models need them, and when/how? What is the eventual + # use? Perhaps these are the "optimal" TSRs, which could be considered control setpoints, but + # dont affect power. + TSRs: NDArrayFloat = field(init=False, factory=list) + # Convenience attributes extracted from the Turbine objects. hub_heights: NDArrayFloat = field(init=False) - hub_heights_sorted: NDArrayFloat = field(init=False, factory=list) - - turbine_map: List[Turbine] = field(init=False, factory=list) - - turbine_type_map: NDArrayObject = field(init=False, factory=list) - turbine_type_map_sorted: NDArrayObject = field(init=False, factory=list) - - turbine_power_functions: Dict[str, Callable] = field(init=False, factory=list) - turbine_power_thrust_tables: Dict[str, dict] = field(init=False, factory=list) - rotor_diameters: NDArrayFloat = field(init=False, factory=list) - rotor_diameters_sorted: NDArrayFloat = field(init=False, factory=list) - TSRs: NDArrayFloat = field(init=False, factory=list) - TSRs_sorted: NDArrayFloat = field(init=False, factory=list) - - ref_tilts: NDArrayFloat = field(init=False, factory=list) - ref_tilts_sorted: NDArrayFloat = field(init=False, factory=list) - - correct_cp_ct_for_tilt: NDArrayFloat = field(init=False, factory=list) - correct_cp_ct_for_tilt_sorted: NDArrayFloat = field(init=False, factory=list) - - internal_turbine_library: Path = field(init=False, default=default_turbine_library_path) + # Post-turbine solve attributes + turbine_powers_sorted: NDArrayFloat = field(init=False, factory=list) + turbine_thrust_coefficients_sorted: NDArrayFloat = field(init=False, factory=list) + turbine_axial_inductions_sorted: NDArrayFloat = field(init=False, factory=list) + turbine_rotor_average_velocities_sorted: NDArrayFloat = field(init=False, factory=list) # Private attributes + # Private attributes. _turbine_types: List = field(init=False, validator=iter_validator(list, str), factory=list) _turbine_definition_cache: dict = field(init=False, factory=dict) + _sorted_indices: NDArrayInt = field(init=False, factory=list) def __attrs_post_init__(self) -> None: # Turbine definitions can be supplied in three ways: @@ -129,7 +116,7 @@ def __attrs_post_init__(self) -> None: # library preprocessing the inputs and loading the specified file directly into # the main input file. The result is that floris sees the turbine definition as a dict. # - A string selecting an turbine that exists in an external turbine library - # specified in `turbine_library_path` + # specified in `external_turbine_library_path` # Load all the turbine types into a cache to be mapped to specific turbine indices later. # This allows to read the yaml input files once rather than every time they're given. @@ -152,7 +139,7 @@ def __attrs_post_init__(self) -> None: ) self._turbine_definition_cache[t["turbine_type"]] = t self._turbine_definition_cache[t["turbine_type"]]["turbine_library_path"] = ( - self.turbine_library_path + self.external_turbine_library_path ) # If a turbine type is a string, then it is expected in the internal or external @@ -162,14 +149,14 @@ def __attrs_post_init__(self) -> None: continue # Skip t if already loaded # Check if the file exists in the internal and/or external library - internal_fn = (self.internal_turbine_library / t).with_suffix(".yaml") - external_fn = (self.turbine_library_path / t).with_suffix(".yaml") + internal_fn = (self.internal_turbine_library_path / t).with_suffix(".yaml") + external_fn = (self.external_turbine_library_path / t).with_suffix(".yaml") in_internal = internal_fn.exists() in_external = external_fn.exists() # If an external library is used and there's a duplicate of an internal # definition, then raise an error - is_unique_path = self.turbine_library_path != default_turbine_library_path + is_unique_path = self.external_turbine_library_path != default_turbine_library_path if is_unique_path and in_external and in_internal: raise ValueError( f"The turbine type: {t} exists in both the internal and external" @@ -187,7 +174,7 @@ def __attrs_post_init__(self) -> None: ) self._turbine_definition_cache[t] = load_yaml(full_path) self._turbine_definition_cache[t]["turbine_library_path"] = ( - self.turbine_library_path + self.external_turbine_library_path ) # Convert any dict entries in the turbine_type list to the type string. Since the @@ -208,27 +195,20 @@ def __attrs_post_init__(self) -> None: if len(self._turbine_types) == 1: self._turbine_types *= self.n_turbines - # Check that turbine definitions contain any v3 keys - for _, v in self._turbine_definition_cache.items(): - check_turbine_definition_for_v3_keys(v) - - # Map each turbine definition to its index in this list - self.turbine_definitions = [ - copy.deepcopy(self._turbine_definition_cache[t]) for t in self._turbine_types - ] + self.construct_turbines() @layout_x.validator - def check_x(self, attribute: attrs.Attribute, value: Any) -> None: + def _check_x(self, attribute: attrs.Attribute, value: Any) -> None: if len(value) != len(self.layout_y): raise ValueError("layout_x and layout_y must have the same number of entries.") @layout_y.validator - def check_y(self, attribute: attrs.Attribute, value: Any) -> None: + def _check_y(self, attribute: attrs.Attribute, value: Any) -> None: if len(value) != len(self.layout_x): raise ValueError("layout_x and layout_y must have the same number of entries.") @turbine_type.validator - def check_turbine_type(self, attribute: attrs.Attribute, value: Any) -> None: + def _check_turbine_type(self, attribute: attrs.Attribute, value: Any) -> None: # Check that the list of turbines is either of length 1 or N turbines if len(value) != 1 and len(value) != self.n_turbines: raise ValueError( @@ -237,138 +217,56 @@ def check_turbine_type(self, attribute: attrs.Attribute, value: Any) -> None: "alter the operation model before setting the layout." ) - @turbine_library_path.validator - def check_library_path(self, attribute: attrs.Attribute, value: Path) -> None: + @external_turbine_library_path.validator + def _check_library_path(self, attribute: attrs.Attribute, value: Path) -> None: """Ensures that the input to `library_path` exists and is a directory.""" if not value.is_dir(): raise FileExistsError(f"The input file path: {str(value)} is not a valid directory.") - def initialize(self, sorted_indices): - # Sort yaw angles from most upstream to most downstream wind turbine - self.yaw_angles_sorted = np.take_along_axis( - self.yaw_angles, - sorted_indices[:, :, 0, 0], - axis=1, - ) - self.tilt_angles_sorted = np.take_along_axis( - self.tilt_angles, - sorted_indices[:, :, 0, 0], - axis=1, - ) - self.power_setpoints_sorted = np.take_along_axis( - self.power_setpoints, - sorted_indices[:, :, 0, 0], - axis=1, + def initialize(self): + # Create structures for storing the turbine outputs + if not hasattr(self, "_sorted_indices"): + raise ValueError( + "The Farm object must be initialized with the sorted indices from a Grid object " + "before it can be used. Please call Farm.set_sorted_indices() first." + ) + + self.turbine_powers_sorted = np.full( + (self._sorted_indices.shape[0], self.n_turbines), np.nan ) - self.awc_modes_sorted = np.take_along_axis( - self.awc_modes, - sorted_indices[:, :, 0, 0], - axis=1, + self.turbine_thrust_coefficients_sorted = np.full( + (self._sorted_indices.shape[0], self.n_turbines), np.nan ) - self.awc_amplitudes_sorted = np.take_along_axis( - self.awc_amplitudes, - sorted_indices[:, :, 0, 0], - axis=1, + self.turbine_axial_inductions_sorted = np.full( + (self._sorted_indices.shape[0], self.n_turbines), np.nan ) - self.awc_frequencies_sorted = np.take_along_axis( - self.awc_frequencies, - sorted_indices[:, :, 0, 0], - axis=1, + self.turbine_rotor_average_velocities_sorted = np.full( + (self._sorted_indices.shape[0], self.n_turbines), np.nan ) - self.state = State.INITIALIZED - - def construct_hub_heights(self): - self.hub_heights = np.array([turb['hub_height'] for turb in self.turbine_definitions]) - def construct_rotor_diameters(self): - self.rotor_diameters = np.array([ - turb['rotor_diameter'] for turb in self.turbine_definitions - ]) - - def construct_turbine_TSRs(self): - self.TSRs = np.array([turb['TSR'] for turb in self.turbine_definitions]) - - def construct_turbine_ref_tilts(self): - self.ref_tilts = np.array( - [turb['power_thrust_table']['ref_tilt'] for turb in self.turbine_definitions] - ) - - def construct_turbine_correct_cp_ct_for_tilt(self): - self.correct_cp_ct_for_tilt = np.array( - [turb.correct_cp_ct_for_tilt for turb in self.turbine_map] - ) + self.state = State.INITIALIZED - def construct_turbine_map(self): - turbine_map_unique = { + def construct_turbines(self): + turbines_unique = { k: Turbine.from_dict(v) for k, v in self._turbine_definition_cache.items() } - self.turbine_map = [turbine_map_unique[k] for k in self._turbine_types] - - def construct_turbine_thrust_coefficient_functions(self): - self.turbine_thrust_coefficient_functions = { - turb.turbine_type: turb.thrust_coefficient_function for turb in self.turbine_map - } - - def construct_turbine_axial_induction_functions(self): - self.turbine_axial_induction_functions = { - turb.turbine_type: turb.axial_induction_function for turb in self.turbine_map - } - - def construct_turbine_tilt_interps(self): - self.turbine_tilt_interps = { - turb.turbine_type: turb.tilt_interp for turb in self.turbine_map - } - - def construct_turbine_power_functions(self): - self.turbine_power_functions = { - turb.turbine_type: turb.power_function for turb in self.turbine_map - } + self.turbines = [turbines_unique[k] for k in self._turbine_types] - def construct_turbine_power_thrust_tables(self): - self.turbine_power_thrust_tables = { - turb.turbine_type: turb.power_thrust_table for turb in self.turbine_map - } + # Extract various attributes for convenience. + self.hub_heights = np.array([t.hub_height for t in self.turbines]) + self.rotor_diameters = np.array([t.rotor_diameter for t in self.turbines]) + self.TSRs = np.array([t.TSR for t in self.turbines]) - def expand_farm_properties(self, n_findex: int, sorted_coord_indices): - template_shape = np.ones_like(sorted_coord_indices) - self.hub_heights_sorted = np.take_along_axis( - self.hub_heights * template_shape, - sorted_coord_indices, - axis=1 - ) - self.rotor_diameters_sorted = np.take_along_axis( - self.rotor_diameters * template_shape, - sorted_coord_indices, - axis=1 - ) - self.TSRs_sorted = np.take_along_axis( - self.TSRs * template_shape, - sorted_coord_indices, - axis=1 - ) - self.ref_tilts_sorted = np.take_along_axis( - self.ref_tilts * template_shape, - sorted_coord_indices, - axis=1 - ) - self.correct_cp_ct_for_tilt_sorted = np.take_along_axis( - self.correct_cp_ct_for_tilt * template_shape, - sorted_coord_indices, - axis=1 - ) + def set_sorted_indices(self, sorted_indices: NDArrayInt): + self._sorted_indices = sorted_indices - # NOTE: Tilt angles are sorted twice - here and in initialize() - self.tilt_angles_sorted = np.take_along_axis( - self.tilt_angles * template_shape, - sorted_coord_indices, - axis=1 - ) + def construct_turbine_type_map(self): self.turbine_type_map_sorted = np.take_along_axis( np.reshape( - [turb["turbine_type"] for turb in self.turbine_definitions] * n_findex, - np.shape(sorted_coord_indices) + [t.turbine_type for t in self.turbines] * self._sorted_indices.shape[0], + np.shape(self._sorted_indices) ), - sorted_coord_indices, + self._sorted_indices, axis=1 ) @@ -378,17 +276,6 @@ def set_yaw_angles(self, yaw_angles: NDArrayFloat | list[float]): def set_yaw_angles_to_ref_yaw(self, n_findex: int): yaw_angles = np.zeros((n_findex, self.n_turbines)) self.set_yaw_angles(yaw_angles) - self.yaw_angles_sorted = np.zeros((n_findex, self.n_turbines)) - - def set_tilt_to_ref_tilt(self, n_findex: int): - self.tilt_angles = ( - np.ones((n_findex, self.n_turbines)) - * self.ref_tilts - ) - self.tilt_angles_sorted = ( - np.ones((n_findex, self.n_turbines)) - * self.ref_tilts - ) def set_power_setpoints(self, power_setpoints: NDArrayFloat): self.power_setpoints = np.array(power_setpoints) @@ -396,17 +283,13 @@ def set_power_setpoints(self, power_setpoints: NDArrayFloat): def set_power_setpoints_to_ref_power(self, n_findex: int): power_setpoints = POWER_SETPOINT_DEFAULT * np.ones((n_findex, self.n_turbines)) self.set_power_setpoints(power_setpoints) - self.power_setpoints_sorted = POWER_SETPOINT_DEFAULT * np.ones((n_findex, self.n_turbines)) def set_awc_modes(self, awc_modes: NDArrayStr): self.awc_modes = np.array(awc_modes) def set_awc_modes_to_ref_mode(self, n_findex: int): - # awc_modes = np.empty((n_findex, self.n_turbines))\ awc_modes = np.array([["baseline"]*self.n_turbines]*n_findex) self.set_awc_modes(awc_modes) - # self.awc_modes_sorted = np.empty((n_findex, self.n_turbines)) - self.awc_modes_sorted = np.array([["baseline"]*self.n_turbines]*n_findex) def set_awc_amplitudes(self, awc_amplitudes: NDArrayFloat): self.awc_amplitudes = np.array(awc_amplitudes) @@ -414,7 +297,6 @@ def set_awc_amplitudes(self, awc_amplitudes: NDArrayFloat): def set_awc_amplitudes_to_ref_amp(self, n_findex: int): awc_amplitudes = np.zeros((n_findex, self.n_turbines)) self.set_awc_amplitudes(awc_amplitudes) - self.awc_amplitudes_sorted = np.zeros((n_findex, self.n_turbines)) def set_awc_frequencies(self, awc_frequencies: NDArrayFloat): self.awc_frequencies = np.array(awc_frequencies) @@ -422,59 +304,16 @@ def set_awc_frequencies(self, awc_frequencies: NDArrayFloat): def set_awc_frequencies_to_ref_freq(self, n_findex: int): awc_frequencies = np.zeros((n_findex, self.n_turbines)) self.set_awc_frequencies(awc_frequencies) - self.awc_frequencies_sorted = np.zeros((n_findex, self.n_turbines)) - - def calculate_tilt_for_eff_velocities(self, rotor_effective_velocities): - tilt_angles = compute_tilt_angles_for_floating_turbines_map( - self.turbine_type_map_sorted, - self.tilt_angles_sorted, - self.turbine_tilt_interps, - rotor_effective_velocities, - ) - return tilt_angles - def finalize(self, unsorted_indices): - self.yaw_angles = np.take_along_axis( - self.yaw_angles_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.tilt_angles = np.take_along_axis( - self.tilt_angles_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.hub_heights = np.take_along_axis( - self.hub_heights_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.rotor_diameters = np.take_along_axis( - self.rotor_diameters_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.TSRs = np.take_along_axis( - self.TSRs_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.ref_tilts = np.take_along_axis( - self.ref_tilts_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.correct_cp_ct_for_tilt = np.take_along_axis( - self.correct_cp_ct_for_tilt_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.turbine_type_map = np.take_along_axis( - self.turbine_type_map_sorted, - unsorted_indices[:,:,0,0], - axis=1 - ) - self.state.USED + def set_control_setpoints_to_reference(self, n_findex: int): + self.set_yaw_angles_to_ref_yaw(n_findex) + self.set_power_setpoints_to_ref_power(n_findex) + self.set_awc_modes_to_ref_mode(n_findex) + self.set_awc_amplitudes_to_ref_amp(n_findex) + self.set_awc_frequencies_to_ref_freq(n_findex) + + def finalize(self): + self.state = State.USED @property def coordinates(self): @@ -490,37 +329,109 @@ def coordinates(self): def n_turbines(self): return len(self.layout_x) -def check_turbine_definition_for_v3_keys(turbine_definition: dict): - """Check that the turbine definition does not contain any v3 keys.""" - v3_deprecation_msg = ( - "Consider using the convert_turbine_v3_to_v4.py utility in floris/tools " - + "to convert from a FLORIS v3 turbine definition to FLORIS v4. " - + "See https://natlabrockies.github.io/floris/v3_to_v4.html for more information." - ) - if "generator_efficiency" in turbine_definition: - raise ValueError( - "generator_efficiency is no longer supported as power is specified in absolute terms " - + "in FLORIS v4. " - + v3_deprecation_msg + @property + def rotor_diameters_sorted(self): + return _sort_by_coord_indices(self.rotor_diameters, self._sorted_indices) + + @property + def hub_heights_sorted(self): + return _sort_by_coord_indices(self.hub_heights, self._sorted_indices) + + @property + def TSRs_sorted(self): + return _sort_by_coord_indices(self.TSRs, self._sorted_indices) + + @property + def yaw_angles_sorted(self): + return _sort_by_coord_indices(self.yaw_angles, self._sorted_indices) + + @property + def power_setpoints_sorted(self): + return _sort_by_coord_indices(self.power_setpoints, self._sorted_indices) + + @property + def awc_modes_sorted(self): + return _sort_by_coord_indices(self.awc_modes, self._sorted_indices) + + @property + def awc_amplitudes_sorted(self): + return _sort_by_coord_indices(self.awc_amplitudes, self._sorted_indices) + + @property + def awc_frequencies_sorted(self): + return _sort_by_coord_indices(self.awc_frequencies, self._sorted_indices) + + @property + def turbine_type_map(self): + return np.broadcast_to( + np.array([t.turbine_type for t in self.turbines]), + (self._sorted_indices.shape[0], self.n_turbines) ) - v3_renamed_keys = ["pP", "pT", "ref_density_cp_ct", "ref_tilt_cp_ct"] - if any(k in turbine_definition for k in v3_renamed_keys): - v3_list_keys = ", ".join(map(str,v3_renamed_keys[:-1]))+", and "+v3_renamed_keys[-1] - v4_versions = ( - "cosine_loss_exponent_yaw, cosine_loss_exponent_tilt, ref_air_density, and ref_tilt" + @property + def turbine_powers(self): + return _unsort_by_coord_indices(self.turbine_powers_sorted, self._sorted_indices) + + @property + def turbine_thrust_coefficients(self): + return _unsort_by_coord_indices( + self.turbine_thrust_coefficients_sorted, self._sorted_indices ) - raise ValueError( - v3_list_keys - + " have been renamed to " - + v4_versions - + ", respectively, and placed under the power_thrust_table field in FLORIS v4. " - + v3_deprecation_msg + + @property + def turbine_axial_inductions(self): + return _unsort_by_coord_indices(self.turbine_axial_inductions_sorted, self._sorted_indices) + + @property + def turbine_rotor_average_velocities(self): + return _unsort_by_coord_indices( + self.turbine_rotor_average_velocities_sorted, self._sorted_indices ) - if "thrust" in turbine_definition["power_thrust_table"]: - raise ValueError( - "thrust has been renamed thrust_coefficient in FLORIS v4 (and power is now specified " - "in absolute terms with units kW, rather than as a coefficient). " - + v3_deprecation_msg + def set_turbine_outputs_by_original_ordering( + self, + powers: NDArrayFloat | None = None, + thrust_coefficients: NDArrayFloat | None = None, + axial_inductions: NDArrayFloat | None = None, + rotor_average_velocities: NDArrayFloat | None = None + ): + if powers is not None: + self.turbine_powers_sorted = _sort_by_coord_indices(powers, self._sorted_indices) + + if thrust_coefficients is not None: + self.turbine_thrust_coefficients_sorted = _sort_by_coord_indices( + thrust_coefficients, self._sorted_indices + ) + + if axial_inductions is not None: + self.turbine_axial_inductions_sorted = _sort_by_coord_indices( + axial_inductions, self._sorted_indices + ) + + if rotor_average_velocities is not None: + self.turbine_rotor_average_velocities_sorted = _sort_by_coord_indices( + rotor_average_velocities, self._sorted_indices + ) + + +def _sort_by_coord_indices(array, sorted_indices): + if array.ndim != 2: + template_shape = np.ones_like(sorted_indices) + return np.take_along_axis( + array * template_shape, + sorted_indices, + axis=1 ) + elif array.ndim == 2: + return np.take_along_axis( + array, + sorted_indices, + axis=1 + ) + else: + raise ValueError("Array must be 1-dimensional or 2-dimensional to sort.") + +def _unsort_by_coord_indices(array, sorted_indices): + temp = np.zeros_like(array) + np.put_along_axis(temp, sorted_indices, array, axis=1) + return temp diff --git a/floris/core/flow_field.py b/floris/core/flow_field.py index f0cd3e6996..ec9399f3d8 100644 --- a/floris/core/flow_field.py +++ b/floris/core/flow_field.py @@ -169,7 +169,6 @@ def initialize_velocity_field(self, grid: Grid) -> None: * np.power( z, (self.wind_shear - 1), - where=z != 0.0 ) ) # If no heterogeneous inflow defined, then set all speeds ups to 1.0 diff --git a/floris/core/grid.py b/floris/core/grid.py index 3674ffddec..9668d2ef38 100644 --- a/floris/core/grid.py +++ b/floris/core/grid.py @@ -80,22 +80,15 @@ def wind_directions_validator(self, instance: attrs.Attribute, value: NDArrayFlo """Using the validator method to keep the `n_findex` attribute up to date.""" self.n_findex = value.size - @grid_resolution.validator - def grid_resolution_validator(self, instance: attrs.Attribute, value: int | Iterable) -> None: - # TODO move this to the grid types and off of the base class - """Check that grid resolution is given as appropriate for the chosen Grid-type.""" - if isinstance(value, int) and \ - isinstance(self, (TurbineGrid, TurbineCubatureGrid, PointsGrid)): - return - elif isinstance(value, Iterable) and isinstance(self, FlowFieldPlanarGrid): - assert type(value[0]) is int - assert type(value[1]) is int - elif isinstance(value, Iterable) and isinstance(self, FlowFieldGrid): - assert type(value[0]) is int - assert type(value[1]) is int - assert type(value[2]) is int - else: - raise TypeError("`grid_resolution` must be of type int or Iterable(int,)") + @z_sorted.validator + def z_sorted_validator(self, instance: attrs.Attribute, value: NDArrayFloat) -> None: + """Check that the z coordinates are above the ground.""" + if np.any(value <= 0): + self.logger.warning( + "Non-positive z coordinates detected. " + "This may cause issues with the flow model calculations. " + "To fix this, consider adjusting the z coordinates to be positive." + ) @abstractmethod def set_grid(self) -> None: @@ -123,6 +116,9 @@ class TurbineGrid(Grid): average_method = "cubic-mean" def __attrs_post_init__(self) -> None: + if not isinstance(self.grid_resolution, int): + raise TypeError("grid_resolution must be of type 'int' for TurbineGrid") + self.set_grid() def set_grid(self) -> None: @@ -175,8 +171,6 @@ def set_grid(self) -> None: Note that the x coordinates are all the same for the rotor plane. """ - # TODO: Where should we locate the coordinate system? Currently, its at - # the foot of the turbine where the tower meets the ground. # These are the rotated coordinates of the wind turbines based on the wind direction x, y, z, self.x_center_of_rotation, self.y_center_of_rotation = rotate_coordinates_rel_west( @@ -279,6 +273,9 @@ class TurbineCubatureGrid(Grid): average_method = "simple-cubature" def __attrs_post_init__(self) -> None: + if not isinstance(self.grid_resolution, int): + raise TypeError("grid_resolution must be of type 'int' for TurbineCubatureGrid") + self.set_grid() def set_grid(self) -> None: @@ -434,75 +431,6 @@ def get_cubature_coefficients(cls, N: int): "B": np.pi/N, } -@define -class FlowFieldGrid(Grid): - """ - Args: - turbine_coordinates (:py:obj:`NDArrayFloat`): The arrays of turbine coordinates as Numpy - arrays with shape (N coordinates, 3). - turbine_diameters (:py:obj:`NDArrayFloat`): The rotor diameters of each turbine. - wind_directions (:py:obj:`NDArrayFloat`): Wind directions supplied by the user. - grid_resolution (:py:obj:`Iterable(int,)`): The number of grid points to create in each - planar direction. Must be 3 components for resolution in the x, y, and z directions. - """ - x_center_of_rotation: NDArrayFloat = field(init=False) - y_center_of_rotation: NDArrayFloat = field(init=False) - - def __attrs_post_init__(self) -> None: - self.set_grid() - - def set_grid(self) -> None: - """ - Create a structured grid for the entire flow field domain. - - Calculates the domain bounds for the current wake model. The bounds - are calculated based on preset extents from the - given layout. The bounds consist of the minimum and maximum values - in the x-, y-, and z-directions. - - If the Curl model is used, the predefined bounds are always set. - - First, sort the turbines so that we know the bounds in the correct orientation. - Then, create the grid based on this wind-from-left orientation - """ - - # These are the rotated coordinates of the wind turbines based on the wind direction - x, y, z, self.x_center_of_rotation, self.y_center_of_rotation = rotate_coordinates_rel_west( - self.wind_directions, - self.turbine_coordinates - ) - - # Construct the arrays storing the grid points - eps = 0.01 - xmin = min(x[0,0]) - 2 * self.turbine_diameters - xmax = max(x[0,0]) + 10 * self.turbine_diameters - ymin = min(y[0,0]) - 2 * self.turbine_diameters - ymax = max(y[0,0]) + 2 * self.turbine_diameters - zmin = 0 + eps - zmax = 6 * max(z[0,0]) - - x_points, y_points, z_points = np.meshgrid( - np.linspace(xmin, xmax, int(self.grid_resolution[0])), - np.linspace(ymin, ymax, int(self.grid_resolution[1])), - np.linspace(zmin, zmax, int(self.grid_resolution[2])), - indexing="ij" - ) - - self.x_sorted = x_points[None, None, :, :, :] - self.y_sorted = y_points[None, None, :, :, :] - self.z_sorted = z_points[None, None, :, :, :] - - # Now calculate grid coordinates in original frame (from 270 deg perspective) - self.x_sorted_inertial_frame, self.y_sorted_inertial_frame, self.z_sorted_inertial_frame = \ - reverse_rotate_coordinates_rel_west( - wind_directions=self.wind_directions, - grid_x=self.x_sorted, - grid_y=self.y_sorted, - grid_z=self.z_sorted, - x_center_of_rotation=self.x_center_of_rotation, - y_center_of_rotation=self.y_center_of_rotation, - ) - @define class FlowFieldPlanarGrid(Grid): """ @@ -526,6 +454,14 @@ class FlowFieldPlanarGrid(Grid): unsorted_indices: NDArrayInt = field(init=False) def __attrs_post_init__(self) -> None: + if (not isinstance(self.grid_resolution, Iterable) + or (len(self.grid_resolution) != 2) + or any(not isinstance(v, int) for v in self.grid_resolution) + ): + raise TypeError( + "grid_resolution must be an Iterable of length 2 for FlowFieldPlanarGrid" + ) + self.set_grid() def set_grid(self) -> None: @@ -648,6 +584,9 @@ class PointsGrid(Grid): y_center_of_rotation: float | None = field(default=None) def __attrs_post_init__(self) -> None: + if not isinstance(self.grid_resolution, int): + raise TypeError("grid_resolution must be of type 'int' for PointsGrid") + self.set_grid() def set_grid(self) -> None: diff --git a/floris/core/rotor_velocity.py b/floris/core/rotor_velocity.py index 2f93c80a52..2ba5f420ab 100644 --- a/floris/core/rotor_velocity.py +++ b/floris/core/rotor_velocity.py @@ -127,24 +127,32 @@ def average_velocity( else: raise ValueError("Incorrect method given.") +# TODO: Consider breaking following tilt functions out into separate file +def calculate_tilt_for_rotor_effective_velocities(farm, rotor_effective_velocities): + tilt_angles = compute_tilt_angles_for_floating_turbines_map( + farm.turbines, + farm.turbine_type_map_sorted, + rotor_effective_velocities, + ) + return tilt_angles + def compute_tilt_angles_for_floating_turbines_map( + turbines: list, turbine_type_map: NDArrayObject, - tilt_angles: NDArrayFloat, - tilt_interps: dict[str, interp1d], rotor_effective_velocities: NDArrayFloat, ) -> NDArrayFloat: + turbine_dict = {t.turbine_type: t for t in turbines} # Loop over each turbine type given to get tilt angles for all turbines - old_tilt_angles = copy.deepcopy(tilt_angles) tilt_angles = np.zeros(np.shape(rotor_effective_velocities)) turb_types = np.unique(turbine_type_map) for turb_type in turb_types: # If no tilt interpolation is specified, assume no modification to tilt - if tilt_interps[turb_type] is None: # Use passed tilt angles - tilt_angles += old_tilt_angles * (turbine_type_map == turb_type) + if turbine_dict[turb_type].tilt_interp is None: # Use reference tilt angle + tilt_angles += turbine_dict[turb_type].ref_tilt * (turbine_type_map == turb_type) else: # Apply interpolated tilt angle tilt_angles += compute_tilt_angles_for_floating_turbines( tilt_angles, - tilt_interps[turb_type], + turbine_dict[turb_type].tilt_interp, rotor_effective_velocities ) * (turbine_type_map == turb_type) @@ -152,7 +160,7 @@ def compute_tilt_angles_for_floating_turbines_map( def compute_tilt_angles_for_floating_turbines( tilt_angles: NDArrayFloat, - tilt_interp: dict[str, interp1d], + tilt_interp: interp1d | None, rotor_effective_velocities: NDArrayFloat, ) -> NDArrayFloat: # Loop over each turbine type given to get tilt angles for all turbines diff --git a/floris/core/solver.py b/floris/core/solver.py deleted file mode 100644 index a785dbee9b..0000000000 --- a/floris/core/solver.py +++ /dev/null @@ -1,1581 +0,0 @@ -import copy - -import numpy as np - -from floris.core import ( - axial_induction, - Farm, - FlowField, - FlowFieldGrid, - FlowFieldPlanarGrid, - PointsGrid, - thrust_coefficient, - TurbineGrid, -) -from floris.core.rotor_velocity import average_velocity -from floris.core.wake import WakeModelManager -from floris.core.wake_deflection.empirical_gauss import yaw_added_wake_mixing -from floris.core.wake_deflection.gauss import ( - calculate_transverse_velocity, - wake_added_yaw, - yaw_added_turbulence_mixing, -) -from floris.core.wake_velocity.empirical_gauss import awc_added_wake_mixing -from floris.type_dec import NDArrayFloat -from floris.utilities import cosd - - -def calculate_area_overlap(wake_velocities, freestream_velocities, y_ngrid, z_ngrid): - """ - compute wake overlap based on the number of points that are not freestream - velocity, i.e. affected by the wake - """ - # Count all of the rotor points with a negligible difference from freestream - # count = np.sum(freestream_velocities - wake_velocities <= 0.05, axis=(3, 4)) - # return (y_ngrid * z_ngrid - count) / (y_ngrid * z_ngrid) - # return 1 - count / (y_ngrid * z_ngrid) - - # Find the points on the rotor grids with a difference from freestream of greater - # than some tolerance. These are all the points in the wake. The ratio of - # these points to the total points is the portion of wake overlap. - return np.sum(freestream_velocities - wake_velocities > 0.05, axis=(3, 4)) / (y_ngrid * z_ngrid) - - -# @profile -def sequential_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager -) -> None: - # Algorithm - # For each turbine, calculate its effect on every downstream turbine. - # For the current turbine, we are calculating the deficit that it adds to downstream turbines. - # Integrate this into the main data structure. - # Move on to the next turbine. - - # <> - deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) - deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) - - # This is u_wake - wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - - # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) - turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] - turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) - - # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity - # with dimensions expanded for (n_turbines, grid, grid) - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() - ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - u_i = flow_field.u_sorted[:, i:i+1] - v_i = flow_field.v_sorted[:, i:i+1] - - ct_i = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - ct_i = ct_i[:, 0:1, None, None] - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions - ) - # Since we are filtering for the i'th turbine in the axial induction function, - # get the first index here (0:1) - axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] - - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - if model_manager.enable_secondary_steering: - added_yaw = wake_added_yaw( - u_i, - v_i, - flow_field.u_initial_sorted, - grid.y_sorted[:, i:i+1] - y_i, - grid.z_sorted[:, i:i+1], - rotor_diameter_i, - hub_height_i, - ct_i, - TSR_i, - axial_induction_i, - flow_field.wind_shear, - ) - effective_yaw_i += added_yaw - - # Model calculations - # NOTE: exponential - deflection_field = model_manager.deflection_model.function( - x_i, - y_i, - effective_yaw_i, - turbulence_intensity_i, - ct_i, - rotor_diameter_i, - **deflection_model_args, - ) - - if model_manager.enable_transverse_velocities: - v_wake, w_wake = calculate_transverse_velocity( - u_i, - flow_field.u_initial_sorted, - flow_field.dudz_initial_sorted, - grid.x_sorted - x_i, - grid.y_sorted - y_i, - grid.z_sorted, - rotor_diameter_i, - hub_height_i, - yaw_angle_i, - ct_i, - TSR_i, - axial_induction_i, - flow_field.wind_shear, - ) - - if model_manager.enable_yaw_added_recovery: - I_mixing = yaw_added_turbulence_mixing( - u_i, - turbulence_intensity_i, - v_i, - flow_field.w_sorted[:, i:i+1], - v_wake[:, i:i+1], - w_wake[:, i:i+1], - ) - gch_gain = 2 - turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing - - # NOTE: exponential - velocity_deficit = model_manager.velocity_model.function( - x_i, - y_i, - z_i, - axial_induction_i, - deflection_field, - yaw_angle_i, - turbulence_intensity_i, - ct_i, - hub_height_i, - rotor_diameter_i, - **deficit_model_args, - ) - - wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted - ) - - wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, - grid.x_sorted, - x_i, - rotor_diameter_i, - axial_induction_i, - ) - - # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = ( - np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) - / (grid.grid_resolution * grid.grid_resolution) - ) - area_overlap = area_overlap[:, :, None, None] - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (grid.x_sorted > x_i) - * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * (grid.x_sorted <= downstream_influence_length + x_i) - ) - - # Combine turbine TIs with WAT - turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity - ) - - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - - flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity - flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2,3), - keepdims=True - ) - - -def full_flow_sequential_solver( - farm: Farm, - flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, - model_manager: WakeModelManager -) -> None: - - # Get the flow quantities and turbine performance - turbine_grid_farm = copy.deepcopy(farm) - turbine_grid_flow_field = copy.deepcopy(flow_field) - - turbine_grid_farm.construct_turbine_map() - turbine_grid_farm.construct_turbine_thrust_coefficient_functions() - turbine_grid_farm.construct_turbine_axial_induction_functions() - turbine_grid_farm.construct_turbine_power_functions() - turbine_grid_farm.construct_hub_heights() - turbine_grid_farm.construct_rotor_diameters() - turbine_grid_farm.construct_turbine_TSRs() - turbine_grid_farm.construct_turbine_ref_tilts() - turbine_grid_farm.construct_turbine_tilt_interps() - turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() - turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) - - turbine_grid = TurbineGrid( - turbine_coordinates=turbine_grid_farm.coordinates, - turbine_diameters=turbine_grid_farm.rotor_diameters, - wind_directions=turbine_grid_flow_field.wind_directions, - grid_resolution=3, - ) - turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices, - ) - turbine_grid_flow_field.initialize_velocity_field(turbine_grid) - turbine_grid_farm.initialize(turbine_grid.sorted_indices) - sequential_solver(turbine_grid_farm, turbine_grid_flow_field, turbine_grid, model_manager) - - ### Referring to the quantities from above, calculate the wake in the full grid - - # Use full flow_field here to use the full grid in the wake models - deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, - flow_field - ) - deficit_model_args = model_manager.velocity_model.prepare_function( - flow_field_grid, - flow_field - ) - - wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - - # Initialize the turbulence intensity field over the entire flow field grid - n_points = flow_field_grid.x_sorted.shape[1] - ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] - ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) - turbulence_intensity_field = ambient_turbulence_intensities.copy() - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(flow_field_grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] - v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] - - ct_i = thrust_coefficient( - velocities=turbine_grid_flow_field.u_sorted, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - thrust_coefficient_functions=turbine_grid_farm.turbine_thrust_coefficient_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the thrust_coefficient function, - # get the first index here (0:1) - ct_i = ct_i[:, 0:1, None, None] - axial_induction_i = axial_induction( - velocities=turbine_grid_flow_field.u_sorted, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - axial_induction_functions=turbine_grid_farm.turbine_axial_induction_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the axial induction function, - # get the first index here (0:1) - axial_induction_i = axial_induction_i[:, 0:1, None, None] - turbulence_intensity_i = \ - turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] - - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - if model_manager.enable_secondary_steering: - added_yaw = wake_added_yaw( - u_i, - v_i, - turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i:i+1] - y_i, - turbine_grid.z_sorted[:, i:i+1], - rotor_diameter_i, - hub_height_i, - ct_i, - TSR_i, - axial_induction_i, - flow_field.wind_shear, - ) - effective_yaw_i += added_yaw - - # Model calculations - # NOTE: exponential - deflection_field = model_manager.deflection_model.function( - x_i, - y_i, - effective_yaw_i, - turbulence_intensity_i, - ct_i, - rotor_diameter_i, - **deflection_model_args, - ) - - if model_manager.enable_transverse_velocities: - v_wake, w_wake = calculate_transverse_velocity( - u_i, - flow_field.u_initial_sorted, - flow_field.dudz_initial_sorted, - flow_field_grid.x_sorted - x_i, - flow_field_grid.y_sorted - y_i, - flow_field_grid.z_sorted, - rotor_diameter_i, - hub_height_i, - yaw_angle_i, - ct_i, - TSR_i, - axial_induction_i, - flow_field.wind_shear, - ) - - # NOTE: exponential - velocity_deficit = model_manager.velocity_model.function( - x_i, - y_i, - z_i, - axial_induction_i, - deflection_field, - yaw_angle_i, - turbulence_intensity_i, - ct_i, - hub_height_i, - rotor_diameter_i, - **deficit_model_args, - ) - - wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted - ) - - wake_added_ti = model_manager.turbulence_model.function( - ambient_turbulence_intensities, - flow_field_grid.x_sorted, - x_i, - rotor_diameter_i, - axial_induction_i, - ) - - # Calculate locations where wake-added turbulence (WAT) applies - area_overlap = np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0) - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_ti, posinf=0.0) - * (flow_field_grid.x_sorted > x_i) - * (np.abs(y_i - flow_field_grid.y_sorted) < 2 * rotor_diameter_i) - * (flow_field_grid.x_sorted <= downstream_influence_length + x_i) - ) - # Combine turbine TIs with WAT - turbulence_intensity_field = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbulence_intensity_field - ) - - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - - flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field - - -def cc_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager -) -> None: - # <> - deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) - deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) - - # This is u_wake - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) - turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) - - # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] - turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) - - # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities - # with extra dimension to reach 4d - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() - ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] - - shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) - Ctmp = np.zeros((shape)) - # Ctmp = np.zeros((len(x_coord), len(wd), len(ws), len(x_coord), y_ngrid, z_ngrid)) - - # sigma_i = np.zeros((shape)) - # sigma_i = np.zeros((len(x_coord), len(wd), len(ws), len(x_coord), y_ngrid, z_ngrid)) - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - - mask2 = ( - (grid.x_sorted < x_i + 0.01) - * (grid.x_sorted > x_i - 0.01) - * (grid.y_sorted < y_i + 0.51 * rotor_diameter_i) - * (grid.y_sorted > y_i - 0.51 * rotor_diameter_i) - ) - turb_inflow_field = ( - turb_inflow_field * ~mask2 - + (flow_field.u_initial_sorted - turb_u_wake) * mask2 - ) - - turb_avg_vels = average_velocity(turb_inflow_field)[:, :, None, None] - turb_Cts = thrust_coefficient( - turb_avg_vels, - flow_field.turbulence_intensity_field_sorted, - flow_field.air_density, - farm.yaw_angles_sorted, - farm.tilt_angles_sorted, - farm.power_setpoints_sorted, - farm.awc_modes_sorted, - farm.awc_amplitudes_sorted, - farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - turb_Cts = turb_Cts[:, :, None, None] - turb_aIs = axial_induction( - turb_avg_vels, - flow_field.turbulence_intensity_field_sorted, - flow_field.air_density, - farm.yaw_angles_sorted, - farm.tilt_angles_sorted, - farm.power_setpoints_sorted, - farm.awc_modes_sorted, - farm.awc_amplitudes_sorted, - farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - turb_aIs = turb_aIs[:, :, None, None] - - u_i = turb_inflow_field[:, i:i+1] - v_i = flow_field.v_sorted[:, i:i+1] - - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - - axial_induction_i = axial_induction_i[:, :, None, None] - - turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] - - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - if model_manager.enable_secondary_steering: - added_yaw = wake_added_yaw( - u_i, - v_i, - flow_field.u_initial_sorted, - grid.y_sorted[:, i:i+1] - y_i, - grid.z_sorted[:, i:i+1], - rotor_diameter_i, - hub_height_i, - turb_Cts[:, i:i+1], - TSR_i, - axial_induction_i, - flow_field.wind_shear, - scale=2.0, - ) - effective_yaw_i += added_yaw - - # Model calculations - # NOTE: exponential - deflection_field = model_manager.deflection_model.function( - x_i, - y_i, - effective_yaw_i, - turbulence_intensity_i, - turb_Cts[:, i:i+1], - rotor_diameter_i, - **deflection_model_args, - ) - - if model_manager.enable_transverse_velocities: - v_wake, w_wake = calculate_transverse_velocity( - u_i, - flow_field.u_initial_sorted, - flow_field.dudz_initial_sorted, - grid.x_sorted - x_i, - grid.y_sorted - y_i, - grid.z_sorted, - rotor_diameter_i, - hub_height_i, - yaw_angle_i, - turb_Cts[:, i:i+1], - TSR_i, - axial_induction_i, - flow_field.wind_shear, - scale=2.0, - ) - - if model_manager.enable_yaw_added_recovery: - I_mixing = yaw_added_turbulence_mixing( - u_i, - turbulence_intensity_i, - v_i, - flow_field.w_sorted[:, i:i+1], - v_wake[:, i:i+1], - w_wake[:, i:i+1], - ) - gch_gain = 1.0 - turbine_turbulence_intensity[:, i:i+1] = turbulence_intensity_i + gch_gain * I_mixing - - turb_u_wake, Ctmp = model_manager.velocity_model.function( - i, - x_i, - y_i, - z_i, - u_i, - deflection_field, - yaw_angle_i, - turbine_turbulence_intensity, - turb_Cts, - farm.rotor_diameters_sorted[:, :, None, None], - turb_u_wake, - Ctmp, - **deficit_model_args, - ) - - wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, - grid.x_sorted, - x_i, - rotor_diameter_i, - turb_aIs - ) - - # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = 1 - ( - np.sum(turb_u_wake <= 0.05, axis=(2, 3), keepdims=True) - / (grid.grid_resolution * grid.grid_resolution) - ) - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (grid.x_sorted > x_i) - * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * (grid.x_sorted <= downstream_influence_length + x_i) - ) - - # Combine turbine TIs with WAT - turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity - ) - - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - flow_field.u_sorted = turb_inflow_field - - flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity - flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2,3), - keepdims=True - ) - - -def full_flow_cc_solver( - farm: Farm, - flow_field: FlowField, - flow_field_grid: FlowFieldGrid | FlowFieldPlanarGrid | PointsGrid, - model_manager: WakeModelManager, -) -> None: - # Get the flow quantities and turbine performance - turbine_grid_farm = copy.deepcopy(farm) - turbine_grid_flow_field = copy.deepcopy(flow_field) - - turbine_grid_farm.construct_turbine_map() - turbine_grid_farm.construct_turbine_thrust_coefficient_functions() - turbine_grid_farm.construct_turbine_axial_induction_functions() - turbine_grid_farm.construct_turbine_power_functions() - turbine_grid_farm.construct_hub_heights() - turbine_grid_farm.construct_rotor_diameters() - turbine_grid_farm.construct_turbine_TSRs() - turbine_grid_farm.construct_turbine_ref_tilts() - turbine_grid_farm.construct_turbine_tilt_interps() - turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() - turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) - - turbine_grid = TurbineGrid( - turbine_coordinates=turbine_grid_farm.coordinates, - turbine_diameters=turbine_grid_farm.rotor_diameters, - wind_directions=turbine_grid_flow_field.wind_directions, - grid_resolution=3, - ) - turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices, - ) - turbine_grid_flow_field.initialize_velocity_field(turbine_grid) - turbine_grid_farm.initialize(turbine_grid.sorted_indices) - cc_solver(turbine_grid_farm, turbine_grid_flow_field, turbine_grid, model_manager) - - ### Referring to the quantities from above, calculate the wake in the full grid - - # Use full flow_field here to use the full grid in the wake models - deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, - flow_field - ) - deficit_model_args = model_manager.velocity_model.prepare_function( - flow_field_grid, - flow_field - ) - - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) - - # Initialize the turbulence intensity field over the entire flow field grid - n_points = flow_field_grid.x_sorted.shape[1] - ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] - ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) - turbulence_intensity_field = ambient_turbulence_intensities.copy() - - shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) - Ctmp = np.zeros((shape)) - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(flow_field_grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] - v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] - - turb_avg_vels = average_velocity(turbine_grid_flow_field.u_sorted) - turb_Cts = thrust_coefficient( - velocities=turb_avg_vels, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - thrust_coefficient_functions=turbine_grid_farm.turbine_thrust_coefficient_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - turb_Cts = turb_Cts[:, :, None, None] - - axial_induction_i = axial_induction( - velocities=turbine_grid_flow_field.u_sorted, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - axial_induction_functions=turbine_grid_farm.turbine_axial_induction_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - axial_induction_i = axial_induction_i[:, :, None, None] - - turbulence_intensity_i = \ - turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] - - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - if model_manager.enable_secondary_steering: - added_yaw = wake_added_yaw( - u_i, - v_i, - turbine_grid_flow_field.u_initial_sorted, - turbine_grid.y_sorted[:, i:i+1] - y_i, - turbine_grid.z_sorted[:, i:i+1], - rotor_diameter_i, - hub_height_i, - turb_Cts[:, i:i+1], - TSR_i, - axial_induction_i, - flow_field.wind_shear, - scale=2.0, - ) - effective_yaw_i += added_yaw - - # Model calculations - # NOTE: exponential - deflection_field = model_manager.deflection_model.function( - x_i, - y_i, - effective_yaw_i, - turbulence_intensity_i, - turb_Cts[:, i:i+1], - rotor_diameter_i, - **deflection_model_args, - ) - - if model_manager.enable_transverse_velocities: - v_wake, w_wake = calculate_transverse_velocity( - u_i, - flow_field.u_initial_sorted, - flow_field.dudz_initial_sorted, - flow_field_grid.x_sorted - x_i, - flow_field_grid.y_sorted - y_i, - flow_field_grid.z_sorted, - rotor_diameter_i, - hub_height_i, - yaw_angle_i, - turb_Cts[:, i:i+1], - TSR_i, - axial_induction_i, - flow_field.wind_shear, - scale=2.0, - ) - - # NOTE: exponential - turb_u_wake, Ctmp = model_manager.velocity_model.function( - i, - x_i, - y_i, - z_i, - u_i, - deflection_field, - yaw_angle_i, - turbine_grid_flow_field.turbulence_intensity_field_sorted_avg, - turb_Cts, - turbine_grid_farm.rotor_diameters_sorted[:, :, None, None], - turb_u_wake, - Ctmp, - **deficit_model_args, - ) - - wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, - flow_field_grid.x_sorted, - x_i, - rotor_diameter_i, - axial_induction_i - ) - - # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.where(turb_u_wake > 0.05, 1, 0) - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (flow_field_grid.x_sorted > x_i) - * (np.abs(y_i - flow_field_grid.y_sorted) < 2 * rotor_diameter_i) - * (flow_field_grid.x_sorted <= downstream_influence_length + x_i) - ) - - # Combine turbine TIs with WAT - turbulence_intensity_field = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbulence_intensity_field - ) - - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - - flow_field.u_sorted = flow_field.u_initial_sorted - turb_u_wake - flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field - - -def turbopark_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager -) -> None: - # Algorithm - # For each turbine, calculate its effect on every downstream turbine. - # For the current turbine, we are calculating the deficit that it adds to downstream turbines. - # Integrate this into the main data structure. - # Move on to the next turbine. - - # <> - deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) - deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) - - # This is u_wake - wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) - velocity_deficit = np.zeros(shape) - deflection_field = np.zeros_like(flow_field.u_initial_sorted) - - # Set up turbulence arrays - turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] - turbine_turbulence_intensity = np.repeat(turbine_turbulence_intensity, farm.n_turbines, axis=1) - - # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensities - # with extra dimension to reach 4d - ambient_turbulence_intensities = flow_field.turbulence_intensities.copy() - ambient_turbulence_intensities = ambient_turbulence_intensities[:, None, None, None] - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(grid.n_turbines): - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - Cts = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - - ct_i = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - ct_i = ct_i[:, 0:1, None, None] - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the axial induction function, - # get the first index here (0:1) - axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - - if model_manager.enable_secondary_steering: - raise NotImplementedError( - "Secondary steering not available for this model.") - - # Model calculations - # NOTE: exponential - if np.any(farm.yaw_angles_sorted): - model_manager.deflection_model.logger.warning( - "WARNING: Deflection with the TurbOPark model has not been fully validated. " - "This is an initial implementation, and we advise you use at your own risk " - "and perform a thorough examination of the results." - ) - for ii in range(i): - x_ii = np.mean(grid.x_sorted[:, ii:ii+1], axis=(2, 3), keepdims=True) - y_ii = np.mean(grid.y_sorted[:, ii:ii+1], axis=(2, 3), keepdims=True) - - yaw_ii = farm.yaw_angles_sorted[:, ii:ii+1, None, None] - turbulence_intensity_ii = turbine_turbulence_intensity[:, ii:ii+1] - ct_ii = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[ii], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - ct_ii = ct_ii[:, 0:1, None, None] - rotor_diameter_ii = farm.rotor_diameters_sorted[:, ii:ii+1, None, None] - - deflection_field_ii = model_manager.deflection_model.function( - x_ii, - y_ii, - yaw_ii, - turbulence_intensity_ii, - ct_ii, - rotor_diameter_ii, - **deflection_model_args, - ) - - deflection_field[:, ii:ii+1, :, :] = deflection_field_ii[:, i:i+1, :, :] - - if model_manager.enable_transverse_velocities: - raise NotImplementedError( - "Transverse velocities not used in this model.") - - if model_manager.enable_yaw_added_recovery: - raise NotImplementedError( - "Yaw added recovery not used in this model.") - - # NOTE: exponential - velocity_deficit = model_manager.velocity_model.function( - x_i, - y_i, - z_i, - turbine_turbulence_intensity, - Cts[:, :, None, None], - rotor_diameter_i, - farm.rotor_diameters_sorted[:, :, None, None], - i, - deflection_field, - **deficit_model_args, - ) - - wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted - ) - - wake_added_turbulence_intensity = model_manager.turbulence_model.function( - ambient_turbulence_intensities, - grid.x_sorted, - x_i, - rotor_diameter_i, - axial_induction_i - ) - - # TODO: leaving this in for GCH quantities; will need to find another way to - # compute area_overlap as the current wake deficit is solved for only upstream - # turbines; could use WAT_upstream - # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = ( - np.sum( - velocity_deficit * flow_field.u_initial_sorted > 0.05, - axis=(2, 3), - keepdims=True - ) - / (grid.grid_resolution * grid.grid_resolution) - ) - - # Modify wake added turbulence by wake area overlap - downstream_influence_length = 15 * rotor_diameter_i - ti_added = ( - area_overlap - * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) - * (grid.x_sorted > x_i) - * (np.abs(y_i - grid.y_sorted) < 2 * rotor_diameter_i) - * (grid.x_sorted <= downstream_influence_length + x_i) - ) - - # Combine turbine TIs with WAT - turbine_turbulence_intensity = np.maximum( - np.sqrt(ti_added**2 + ambient_turbulence_intensities**2), turbine_turbulence_intensity - ) - - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - - flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity - flow_field.turbulence_intensity_field_sorted_avg = np.mean( - turbine_turbulence_intensity, - axis=(2, 3), - keepdims=True - ) - - -def full_flow_turbopark_solver( - farm: Farm, - flow_field: FlowField, - flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager -) -> None: - raise NotImplementedError("Plotting for the TurbOPark model is not currently implemented.") - - -def empirical_gauss_solver( - farm: Farm, - flow_field: FlowField, - grid: TurbineGrid, - model_manager: WakeModelManager -) -> NDArrayFloat: - """ - Algorithm: - For each turbine, calculate its effect on every downstream turbine. - For the current turbine, we are calculating the deficit that it adds to downstream turbines. - Integrate this into the main data structure. - Move on to the next turbine. - - Args: - farm (Farm) - flow_field (FlowField) - grid (TurbineGrid) - model_manager (WakeModelManager) - - Raises: - NotImplementedError: Raised if secondary steering is enabled with the EmGauss model. - NotImplementedError: Raised if transverse velocities is enabled with the EmGauss model. - - Returns: - NDArrayFloat: wake induced mixing field primarily for use in the full-flow EmGauss solver - """ - - - # <> - deflection_model_args = model_manager.deflection_model.prepare_function(grid, flow_field) - deficit_model_args = model_manager.velocity_model.prepare_function(grid, flow_field) - - # This is u_wake - wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - - x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:,:,None] - downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0,2,1)) - downstream_distance_D = downstream_distance_D / \ - np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) - downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease - # Initialize the mixing factor model using TI if specified - initial_mixing_factor = model_manager.turbulence_model.atmospheric_ti_gain * np.eye( - grid.n_turbines - ) - mixing_factor = np.repeat( - initial_mixing_factor[None, :, :], - flow_field.n_findex, - axis=0 - ) - mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) - - ct_i = thrust_coefficient( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - thrust_coefficient_functions=farm.turbine_thrust_coefficient_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - ct_i = ct_i[:, 0:1, None, None] - axial_induction_i = axial_induction( - velocities=flow_field.u_sorted, - turbulence_intensities=flow_field.turbulence_intensity_field_sorted, - air_density=flow_field.air_density, - yaw_angles=farm.yaw_angles_sorted, - tilt_angles=farm.tilt_angles_sorted, - power_setpoints=farm.power_setpoints_sorted, - awc_modes=farm.awc_modes_sorted, - awc_amplitudes=farm.awc_amplitudes_sorted, - axial_induction_functions=farm.turbine_axial_induction_functions, - tilt_interps=farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=farm.turbine_type_map_sorted, - turbine_power_thrust_tables=farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=grid.average_method, - cubature_weights=grid.cubature_weights, - multidim_condition=flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the axial induction function, - # get the first index here (0:1) - axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] - awc_mode_i = farm.awc_modes_sorted[:, i:i+1, None, None] - awc_amplitude_i = farm.awc_amplitudes_sorted[:, i:i+1, None, None] - awc_frequency_i = farm.awc_frequencies_sorted[:, i:i+1, None, None] - hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] - - # Secondary steering not currently implemented in EmGauss model - # effective_yaw_i = np.zeros_like(yaw_angle_i) - # effective_yaw_i += yaw_angle_i - - average_velocities = average_velocity( - flow_field.u_sorted, - method=grid.average_method, - cubature_weights=grid.cubature_weights - ) - tilt_angle_i = farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] - - if model_manager.enable_secondary_steering: - raise NotImplementedError( - "Secondary steering not available for this model.") - - if model_manager.enable_transverse_velocities: - raise NotImplementedError( - "Transverse velocities not used in this model.") - - if model_manager.enable_yaw_added_recovery: - # Influence of yawing on turbine's own wake - mixing_factor[:, i:i+1, i] += \ - yaw_added_wake_mixing( - axial_induction_i, - yaw_angle_i, - 1, - model_manager.deflection_model.yaw_added_mixing_gain - ) - - if model_manager.enable_active_wake_mixing: - # Influence of awc on turbine's own wake - mixing_factor[:, i:i+1, i] += \ - awc_added_wake_mixing( - awc_mode_i, - awc_amplitude_i, - awc_frequency_i, - model_manager.velocity_model.awc_wake_exp, - model_manager.velocity_model.awc_wake_denominator - ) - - # Extract total wake induced mixing for turbine i - mixing_i = np.linalg.norm( - mixing_factor[:, i:i+1, :, None], - ord=2, axis=2, keepdims=True - ) - - # Model calculations - # NOTE: exponential - deflection_field_y, deflection_field_z = model_manager.deflection_model.function( - x_i, - y_i, - yaw_angle_i, - tilt_angle_i, - mixing_i, - ct_i, - rotor_diameter_i, - **deflection_model_args - ) - - # NOTE: exponential - velocity_deficit = model_manager.velocity_model.function( - x_i, - y_i, - z_i, - axial_induction_i, - deflection_field_y, - deflection_field_z, - yaw_angle_i, - tilt_angle_i, - mixing_i, - ct_i, - hub_height_i, - rotor_diameter_i, - **deficit_model_args - ) - - wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted - ) - - # Calculate wake overlap for wake-added turbulence (WAT) - area_overlap = np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3))\ - / (grid.grid_resolution * grid.grid_resolution) - - # Compute wake induced mixing factor - mixing_factor[:,:,i] += \ - area_overlap * model_manager.turbulence_model.function( - axial_induction_i, downstream_distance_D[:,:,i] - ) - if model_manager.enable_yaw_added_recovery: - mixing_factor[:,:,i] += \ - area_overlap * yaw_added_wake_mixing( - axial_induction_i, - yaw_angle_i, - downstream_distance_D[:,:,i], - model_manager.deflection_model.yaw_added_mixing_gain - ) - - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - - return mixing_factor - - -def full_flow_empirical_gauss_solver( - farm: Farm, - flow_field: FlowField, - flow_field_grid: FlowFieldGrid, - model_manager: WakeModelManager -) -> None: - - # Get the flow quantities and turbine performance - turbine_grid_farm = copy.deepcopy(farm) - turbine_grid_flow_field = copy.deepcopy(flow_field) - - turbine_grid_farm.construct_turbine_map() - turbine_grid_farm.construct_turbine_thrust_coefficient_functions() - turbine_grid_farm.construct_turbine_axial_induction_functions() - turbine_grid_farm.construct_turbine_power_functions() - turbine_grid_farm.construct_hub_heights() - turbine_grid_farm.construct_rotor_diameters() - turbine_grid_farm.construct_turbine_TSRs() - turbine_grid_farm.construct_turbine_ref_tilts() - turbine_grid_farm.construct_turbine_tilt_interps() - turbine_grid_farm.construct_turbine_correct_cp_ct_for_tilt() - turbine_grid_farm.set_tilt_to_ref_tilt(flow_field.n_findex) - - turbine_grid = TurbineGrid( - turbine_coordinates=turbine_grid_farm.coordinates, - turbine_diameters=turbine_grid_farm.rotor_diameters, - wind_directions=turbine_grid_flow_field.wind_directions, - grid_resolution=3, - ) - turbine_grid_farm.expand_farm_properties( - turbine_grid_flow_field.n_findex, - turbine_grid.sorted_coord_indices - ) - turbine_grid_flow_field.initialize_velocity_field(turbine_grid) - turbine_grid_farm.initialize(turbine_grid.sorted_indices) - wim_field = empirical_gauss_solver( - turbine_grid_farm, - turbine_grid_flow_field, - turbine_grid, - model_manager - ) - - # Create placeholder for TI, which is not currently used in the EmG model - n_points = flow_field_grid.x_sorted.shape[1] - ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] - ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) - turbulence_intensity_field = ambient_turbulence_intensities.copy() - - ### Referring to the quantities from above, calculate the wake in the full grid - - # Use full flow_field here to use the full grid in the wake models - deflection_model_args = model_manager.deflection_model.prepare_function( - flow_field_grid, flow_field - ) - deficit_model_args = model_manager.velocity_model.prepare_function(flow_field_grid, flow_field) - - wake_field = np.zeros_like(flow_field.u_initial_sorted) - v_wake = np.zeros_like(flow_field.v_initial_sorted) - w_wake = np.zeros_like(flow_field.w_initial_sorted) - - # Calculate the velocity deficit sequentially from upstream to downstream turbines - for i in range(flow_field_grid.n_turbines): - - # Get the current turbine quantities - x_i = np.mean(turbine_grid.x_sorted[:, i:i+1], axis=(2,3), keepdims=True) - y_i = np.mean(turbine_grid.y_sorted[:, i:i+1], axis=(2,3), keepdims=True) - z_i = np.mean(turbine_grid.z_sorted[:, i:i+1], axis=(2,3), keepdims=True) - - ct_i = thrust_coefficient( - velocities=turbine_grid_flow_field.u_sorted, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - thrust_coefficient_functions=turbine_grid_farm.turbine_thrust_coefficient_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the thrust coefficient function, - # get the first index here (0:1) - ct_i = ct_i[:, 0:1, None, None] - axial_induction_i = axial_induction( - velocities=turbine_grid_flow_field.u_sorted, - turbulence_intensities=turbine_grid_flow_field.turbulence_intensity_field_sorted, - air_density=turbine_grid_flow_field.air_density, - yaw_angles=turbine_grid_farm.yaw_angles_sorted, - tilt_angles=turbine_grid_farm.tilt_angles_sorted, - power_setpoints=turbine_grid_farm.power_setpoints_sorted, - awc_modes=turbine_grid_farm.awc_modes_sorted, - awc_amplitudes=turbine_grid_farm.awc_amplitudes_sorted, - axial_induction_functions=turbine_grid_farm.turbine_axial_induction_functions, - tilt_interps=turbine_grid_farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=turbine_grid_farm.correct_cp_ct_for_tilt_sorted, - turbine_type_map=turbine_grid_farm.turbine_type_map_sorted, - turbine_power_thrust_tables=turbine_grid_farm.turbine_power_thrust_tables, - ix_filter=[i], - average_method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights, - multidim_condition=turbine_grid_flow_field.multidim_conditions, - ) - # Since we are filtering for the i'th turbine in the axial induction function, - # get the first index here (0:1) - axial_induction_i = axial_induction_i[:, 0:1, None, None] - yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] - hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] - rotor_diameter_i = turbine_grid_farm.rotor_diameters_sorted[:, i:i+1, None, None] - wake_induced_mixing_i = wim_field[:, i:i+1, :, None].sum(axis=2, keepdims=1) - effective_yaw_i = np.zeros_like(yaw_angle_i) - effective_yaw_i += yaw_angle_i - - average_velocities = average_velocity( - turbine_grid_flow_field.u_sorted, - method=turbine_grid.average_method, - cubature_weights=turbine_grid.cubature_weights - ) - tilt_angle_i = turbine_grid_farm.calculate_tilt_for_eff_velocities(average_velocities) - tilt_angle_i = tilt_angle_i[:, i:i+1, None, None] - - if model_manager.enable_secondary_steering: - raise NotImplementedError( - "Secondary steering not available for this model.") - - if model_manager.enable_transverse_velocities: - raise NotImplementedError( - "Transverse velocities not used in this model.") - - # Model calculations - # NOTE: exponential - deflection_field_y, deflection_field_z = model_manager.deflection_model.function( - x_i, - y_i, - effective_yaw_i, - tilt_angle_i, - wake_induced_mixing_i, - ct_i, - rotor_diameter_i, - **deflection_model_args - ) - - # NOTE: exponential - velocity_deficit = model_manager.velocity_model.function( - x_i, - y_i, - z_i, - axial_induction_i, - deflection_field_y, - deflection_field_z, - yaw_angle_i, - tilt_angle_i, - wake_induced_mixing_i, - ct_i, - hub_height_i, - rotor_diameter_i, - **deficit_model_args - ) - - wake_field = model_manager.combination_model.function( - wake_field, - velocity_deficit * flow_field.u_initial_sorted - ) - - flow_field.u_sorted = flow_field.u_initial_sorted - wake_field - flow_field.v_sorted += v_wake - flow_field.w_sorted += w_wake - flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field diff --git a/floris/core/turbine/__init__.py b/floris/core/turbine/__init__.py index ada6073c97..8f5839088b 100644 --- a/floris/core/turbine/__init__.py +++ b/floris/core/turbine/__init__.py @@ -1,5 +1,4 @@ - -from floris.core.turbine.controller_dependent_operation_model import ControllerDependentTurbine +from floris.core.turbine.operation_model_base import BaseOperationModel from floris.core.turbine.operation_models import ( AWCTurbine, CosineLossTurbine, @@ -8,4 +7,5 @@ SimpleDeratingTurbine, SimpleTurbine, ) +from floris.core.turbine.controller_dependent_operation_model import ControllerDependentTurbine from floris.core.turbine.unified_momentum_model import UnifiedMomentumModelTurbine diff --git a/floris/core/turbine/controller_dependent_operation_model.py b/floris/core/turbine/controller_dependent_operation_model.py index 7ced79f1df..01114195fc 100644 --- a/floris/core/turbine/controller_dependent_operation_model.py +++ b/floris/core/turbine/controller_dependent_operation_model.py @@ -10,7 +10,7 @@ compute_tilt_angles_for_floating_turbines, rotor_velocity_air_density_correction, ) -from floris.core.turbine.operation_models import BaseOperationModel +from floris.core.turbine import BaseOperationModel from floris.type_dec import ( NDArrayFloat, NDArrayObject, diff --git a/floris/core/turbine/operation_model_base.py b/floris/core/turbine/operation_model_base.py new file mode 100644 index 0000000000..d1a8a0f537 --- /dev/null +++ b/floris/core/turbine/operation_model_base.py @@ -0,0 +1,38 @@ +from abc import abstractmethod + +from attrs import define + +from floris.core import BaseLibrary + + +@define +class BaseOperationModel(BaseLibrary): + """ + Base class for turbine operation models. All turbine operation models must implement static + power(), thrust_coefficient(), and axial_induction() methods, which are called by power() and + thrust_coefficient() through the interface in the turbine.py module. + + Args: + BaseClass (_type_): _description_ + + Raises: + NotImplementedError: _description_ + NotImplementedError: _description_ + """ + @staticmethod + @abstractmethod + def power() -> None: + raise NotImplementedError("BaseOperationModel.power") + + @staticmethod + @abstractmethod + def thrust_coefficient() -> None: + raise NotImplementedError("BaseOperationModel.thrust_coefficient") + + @staticmethod + @abstractmethod + def axial_induction() -> None: + # TODO: Consider whether we can make a generic axial_induction method + # based purely on thrust_coefficient so that we don't need to implement + # axial_induction() in individual operation models. + raise NotImplementedError("BaseOperationModel.axial_induction") diff --git a/floris/core/turbine/operation_models.py b/floris/core/turbine/operation_models.py index 6c093bf132..f0c6badae4 100644 --- a/floris/core/turbine/operation_models.py +++ b/floris/core/turbine/operation_models.py @@ -18,6 +18,7 @@ rotor_velocity_tilt_cosine_correction, rotor_velocity_yaw_cosine_correction, ) +from floris.core.turbine import BaseOperationModel from floris.type_dec import ( NDArrayFloat, NDArrayObject, @@ -28,39 +29,6 @@ POWER_SETPOINT_DEFAULT = 1e12 POWER_SETPOINT_DISABLED = 0.001 - -@define -class BaseOperationModel(BaseClass): - """ - Base class for turbine operation models. All turbine operation models must implement static - power(), thrust_coefficient(), and axial_induction() methods, which are called by power() and - thrust_coefficient() through the interface in the turbine.py module. - - Args: - BaseClass (_type_): _description_ - - Raises: - NotImplementedError: _description_ - NotImplementedError: _description_ - """ - @staticmethod - @abstractmethod - def power() -> None: - raise NotImplementedError("BaseOperationModel.power") - - @staticmethod - @abstractmethod - def thrust_coefficient() -> None: - raise NotImplementedError("BaseOperationModel.thrust_coefficient") - - @staticmethod - @abstractmethod - def axial_induction() -> None: - # TODO: Consider whether we can make a generic axial_induction method - # based purely on thrust_coefficient so that we don't need to implement - # axial_induciton() in individual operation models. - raise NotImplementedError("BaseOperationModel.axial_induction") - @define class SimpleTurbine(BaseOperationModel): """ @@ -72,6 +40,7 @@ class SimpleTurbine(BaseOperationModel): not intended to be instantiated; it simply defines a library of static methods. """ + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -106,6 +75,7 @@ def power( return power + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -135,6 +105,7 @@ def thrust_coefficient( return thrust_coefficient + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -166,6 +137,7 @@ class CosineLossTurbine(BaseOperationModel): not intended to be instantiated; it simply defines a library of static methods. """ + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -219,6 +191,7 @@ def power( return power + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -269,6 +242,7 @@ def thrust_coefficient( return thrust_coefficient + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -307,6 +281,7 @@ class SimpleDeratingTurbine(BaseOperationModel): added to the kwargs dictionaries in the respective functions on turbine.py. They won't affect the other operation models. """ + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -331,6 +306,7 @@ def power( # TODO: would we like special handling of zero power setpoints # (mixed with non-zero values) to speed up computation in that case? + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -359,6 +335,7 @@ def thrust_coefficient( thrust_coefficients = power_fractions * base_thrust_coefficients return np.minimum(base_thrust_coefficients, thrust_coefficients) + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -519,9 +496,11 @@ class AWCTurbine(BaseOperationModel): the other operation models. """ + @staticmethod def AWC_model(a, b, c, base_values, awc_amplitudes): return base_values * (1 - (b + c*base_values)*awc_amplitudes**a) + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -569,7 +548,7 @@ def power( return powers - + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -602,6 +581,7 @@ def thrust_coefficient( return thrust_coefficients + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, @@ -625,6 +605,7 @@ def axial_induction( @define class PeakShavingTurbine(): + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -672,6 +653,7 @@ def power( return powers + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -720,6 +702,7 @@ def thrust_coefficient( return thrust_coefficient + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, diff --git a/floris/core/turbine/turbine.py b/floris/core/turbine/turbine.py index d9900037ea..bb98ea1d5a 100644 --- a/floris/core/turbine/turbine.py +++ b/floris/core/turbine/turbine.py @@ -1,4 +1,5 @@ import copy +import inspect import logging import os from collections.abc import Callable, Iterable @@ -6,13 +7,13 @@ import attrs import numpy as np -import pandas as pd from attrs import define, field from scipy.interpolate import interp1d -from floris.core import BaseClass +from floris.core import BaseClass, BaseLibrary from floris.core.turbine import ( AWCTurbine, + BaseOperationModel, ControllerDependentTurbine, CosineLossTurbine, MixedOperationTurbine, @@ -47,250 +48,445 @@ }, } +def _op_model_converter(operation_model): + # If operation_model is an instantiated class, return it + if isinstance(operation_model, BaseOperationModel): + return operation_model + + # If operation_model is an uninstantiated class with only static methods, instantiate it + elif isinstance(operation_model, type) and issubclass(operation_model, BaseOperationModel): + # Check if all methods are static + if all( + isinstance(inspect.getattr_static(operation_model, method), staticmethod) + for method in ["power", "thrust_coefficient", "axial_induction"] + ): + return operation_model() + else: + raise TypeError( + "operation_model must be an instantiated BaseOperationModel or a subclass " + "with only static methods." + ) -def _select_multidim_condition( - condition: dict, - specified_conditions: Iterable[tuple], - condition_keys: list[str], - n_findex: int, -) -> tuple: - """ - Convert condition to the type expected by power_thrust_table and select - nearest specified condition - """ - if type(condition) is dict: - # Check valid keys - if set(condition.keys()) != set(condition_keys): + # If operation_model is a string, instantiate from TURBINE_MODEL_MAP + elif isinstance(operation_model, str): + if operation_model not in TURBINE_MODEL_MAP["operation_model"]: + valid_models = list(TURBINE_MODEL_MAP["operation_model"].keys()) raise ValueError( - f"The provided condition keys {list(condition.keys())} do not match the " - f"expected keys {condition_keys}. A single value should be provided for " - "each dimension of the multidimensional power/thrust_coefficient table." + f"Unknown operation model '{operation_model}'. " + f"Expected one of {valid_models}." ) - # Create a tuple of the condition values in the correct order - if isinstance(condition[condition_keys[0]], list) or isinstance( - condition[condition_keys[0]], np.ndarray - ): - # Assume multiple specified conditions - n_conds = len(condition[condition_keys[0]]) - if n_conds != n_findex: - raise ValueError( - "When providing multiple specified conditions, the number of conditions " - "must match the number of findices." - ) - for k in condition_keys: - if len(condition[k]) != n_conds: - raise ValueError( - "All condition values must have the same length when providing " - "multiple specified conditions." - ) - condition = [tuple(condition[k][i] for k in condition_keys) for i in range(n_conds)] - else: - n_conds = 1 - condition = [tuple(condition[k] for k in condition_keys)] - elif condition is None: - raise ValueError( - "multidim_condition must be provided if using multidimensional " - "power/thrust_coefficient." - ) + return TURBINE_MODEL_MAP["operation_model"][operation_model]() + + # Handle dict representation of an operation model + elif isinstance(operation_model, dict): + return BaseLibrary.from_dict(operation_model) + + # Otherwise, raise an error else: - raise TypeError("condition should be of type dict.") + raise TypeError( + "operation_model must be a BaseOperationModel instance, " + "a BaseOperationModel subclass, or a valid operation-model string." + ) - # Find the nearest key to the specified conditions. - specified_conditions = np.array(specified_conditions) - if specified_conditions.ndim == 1: # Single specified condition - specified_conditions = specified_conditions.reshape(-1, 1) +@define +class Turbine(BaseClass): + """ + A class containing the parameters and infrastructure to model a wind turbine's performance + for a particular atmospheric condition. - # Find the nearest key to the specified conditions. - nearest_conditions = np.zeros((n_conds, specified_conditions.shape[1])) - for f, cond in enumerate(condition): # Loop over findices - for i, c in enumerate(cond): - nearest_conditions[f, i] = ( - specified_conditions[:, i][np.absolute(specified_conditions[:, i] - c).argmin()] - ) + Args: + turbine_type (str): An identifier for this type of turbine such as "NREL_5MW". + rotor_diameter (float): The rotor diameter in meters. + hub_height (float): The hub height in meters. + TSR (float): The Tip Speed Ratio of the turbine. + power_thrust_table (dict[str, float]): Contains power coefficient and thrust coefficient + values at a series of wind speeds to define the turbine performance. + The dictionary must have the following three keys with equal length values: + { + "wind_speeds": List[float], + "power": List[float], + "thrust": List[float], + } + or, contain a key "power_thrust_data_file" pointing to the power/thrust data. + Optionally, power_thrust_table may include parameters for use in the turbine submodel, + for example: + cosine_loss_exponent_yaw (float): The cosine exponent relating the yaw misalignment + angle to turbine power. + cosine_loss_exponent_tilt (float): The cosine exponent relating the rotor tilt angle + to turbine power. + ref_air_density (float): The density at which the provided Cp and Ct curves are + defined. + ref_tilt (float): The implicit tilt of the turbine for which the Cp and Ct + curves are defined. This is typically the nacelle tilt. + operation_model (str | BaseOperationModel): The turbine operation model to use for this + turbine. This can be given as a string corresponding to one of the provided operation + models, or a custom operation model defined as a subclass of BaseOperationModel. + correct_cp_ct_for_tilt (bool): A flag to indicate whether to correct Cp and Ct for tilt + usually for a floating turbine. + Optional, defaults to False. + floating_tilt_table (dict[str, float]): Look up table of tilt angles at a series of + wind speeds. The dictionary must have the following keys with equal length values: + { + "wind_speeds": List[float], + "tilt": List[float], + } + Required if `correct_cp_ct_for_tilt = True`. Defaults to None. + multi_dimensional_cp_ct (bool): Use a multidimensional power_thrust_table. Defaults to + False. + """ + turbine_type: str = field() + rotor_diameter: float = field() + hub_height: float = field() + TSR: float = field() + power_thrust_table: dict = field(default={}) # conversion to numpy in __post_init__ + operation_model: BaseOperationModel = field( + default=CosineLossTurbine(), + converter=_op_model_converter + ) - nearest_conditions, md_map = np.unique(nearest_conditions, axis=0, return_inverse=True) + correct_cp_ct_for_tilt: bool = field(default=False) + floating_tilt_table: dict[str, NDArrayFloat] | None = field(default=None) - # Update map if only a single condition was provided - if n_conds == 1: - md_map = np.repeat(md_map, n_findex, axis=0) + multi_dimensional_cp_ct: bool = field(default=False) - return nearest_conditions, md_map + # Initialized in the post_init function + rotor_radius: float = field(init=False) + rotor_area: float = field(init=False) + thrust_coefficient_function: Callable = field(init=False) + axial_induction_function: Callable = field(init=False) + power_function: Callable = field(init=False) + tilt_interp: interp1d = field(init=False, default=None) + power_thrust_data_file: str = field(default=None) + ref_tilt: float = field(default=0.0, init=False) + # Only used by mutlidimensional turbines + turbine_library_path: Path = field( + default=Path(__file__).parents[2] / "turbine_library", + converter=convert_to_path, + validator=attrs.validators.instance_of(Path) + ) -def power( - velocities: NDArrayFloat, - turbulence_intensities: NDArrayFloat, - air_density: float, - power_functions: dict[str, Callable], - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, - awc_modes: NDArrayStr, - awc_amplitudes: NDArrayFloat, - tilt_interps: dict[str, interp1d], - turbine_type_map: NDArrayObject, - turbine_power_thrust_tables: dict, - ix_filter: NDArrayInt | Iterable[int] | None = None, - average_method: str = "cubic-mean", - cubature_weights: NDArrayFloat | None = None, - correct_cp_ct_for_tilt: bool = False, - multidim_condition: dict | None = None, -) -> NDArrayFloat: - """Power produced by a turbine adjusted for yaw and tilt. Value - given in Watts. + # Not to be provided by the user + condition_keys: list[str] = field(init=False, factory=list) - Args: - velocities (NDArrayFloat[n_findex, n_turbines, n_grid, n_grid]): The velocities at a - turbine. - turbulence_intensities (NDArrayFloat[findex, turbines]): The turbulence intensity at - each turbine. - air_density (float): air density for simulation [kg/m^3] - power_functions (dict[str, Callable]): A dictionary of power functions for - each turbine type. Keys are the turbine type and values are the callable functions. - yaw_angles (NDArrayFloat[findex, turbines]): The yaw angle for each turbine. - tilt_angles (NDArrayFloat[findex, turbines]): The tilt angle for each turbine. - power_setpoints: (NDArrayFloat[findex, turbines]): Maximum power setpoint for each - turbine [W]. - awc_modes: (NDArrayStr[findex, turbines]): awc excitation mode (currently, only "baseline" - and "helix" are implemented). - awc_modes: (NDArrayStr[findex, turbines]): awc excitation mode (currently, only "baseline" - and "helix" are implemented). - awc_amplitudes: (NDArrayFloat[findex, turbines]): awc excitation amplitude for each - turbine [deg]. - tilt_interps (Iterable[tuple]): The tilt interpolation functions for each - turbine. - turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition for - each turbine. - turbine_power_thrust_tables: Reference data for the power and thrust representation - ix_filter (NDArrayInt, optional): The boolean array, or - integer indices to filter out before calculation. Defaults to None. - average_method (str, optional): The method for averaging over turbine rotor points - to determine a rotor-average wind speed. Defaults to "cubic-mean". - cubature_weights (NDArrayFloat | None): Weights for cubature averaging methods. Defaults to - None. - multidim_condition (dict | None): The condition dictionary used to select the appropriate - thrust coefficient relationship for multidimensional power/thrust tables. Defaults to - None. + def __attrs_post_init__(self) -> None: + self._initialize_power_thrust_functions() + self.__post_init__() - Returns: - NDArrayFloat: The power, in Watts, for each turbine after adjusting for yaw and tilt. - """ + def __post_init__(self) -> None: + self._initialize_tilt_interpolation() - # Down-select inputs if ix_filter is given - if ix_filter is not None: - velocities = velocities[:, ix_filter] - turbulence_intensities = turbulence_intensities[:, ix_filter] - yaw_angles = yaw_angles[:, ix_filter] - tilt_angles = tilt_angles[:, ix_filter] - power_setpoints = power_setpoints[:, ix_filter] - awc_modes = awc_modes[:, ix_filter] - awc_amplitudes = awc_amplitudes[:, ix_filter] - turbine_type_map = turbine_type_map[:, ix_filter] - if type(correct_cp_ct_for_tilt) is bool: - pass + bypass_numeric_converter = False + if self.multi_dimensional_cp_ct: + self._initialize_multidim_power_thrust_table() + bypass_numeric_converter = True else: - correct_cp_ct_for_tilt = correct_cp_ct_for_tilt[:, ix_filter] + self.ref_tilt = self.power_thrust_table["ref_tilt"] - # Establish the main set of keyword arguments for power() - power_model_kwargs = { - "power_thrust_table": None, # Will be filled below - "velocities": velocities, - "turbulence_intensities": turbulence_intensities, - "air_density": air_density, - "yaw_angles": yaw_angles, - "tilt_angles": tilt_angles, - "power_setpoints": power_setpoints, - "awc_modes": awc_modes, - "awc_amplitudes": awc_amplitudes, - "tilt_interp": None, # Will be filled below - "average_method": average_method, - "cubature_weights": cubature_weights, - "correct_cp_ct_for_tilt": correct_cp_ct_for_tilt, - } + # Check for whether a cp_ct_data_file is specified, and load it if so. + if "controller_dependent_turbine_parameters" in self.power_thrust_table: + floris_root = Path(__file__).resolve().parents[2] + file_path = ( + floris_root / "turbine_library" / + self.power_thrust_table["controller_dependent_turbine_parameters"] + ["cp_ct_data_file"] + ) + npz_data = dict(np.load(file_path)) + self.power_thrust_table["controller_dependent_turbine_parameters"]["cp_ct_data"] = { + k: v.tolist() for k, v in npz_data.items() + } + bypass_numeric_converter = True - # Loop over each turbine type given to get power for all turbines - p = np.zeros(np.shape(velocities)[0:2]) - turb_types = np.unique(turbine_type_map) - for turb_type in turb_types: - if "power" in turbine_power_thrust_tables[turb_type]: # Not multidimensional - power_thrust_table = turbine_power_thrust_tables[turb_type] + if not bypass_numeric_converter: + self.power_thrust_table = floris_numeric_dict_converter(self.power_thrust_table) - power_model_kwargs["power_thrust_table"] = power_thrust_table - power_model_kwargs["tilt_interp"] = tilt_interps[turb_type] + def _initialize_power_thrust_functions(self) -> None: + self.thrust_coefficient_function = self.operation_model.thrust_coefficient + self.axial_induction_function = self.operation_model.axial_induction + self.power_function = self.operation_model.power - p += ( - power_functions[turb_type](**power_model_kwargs) - * (turbine_type_map == turb_type) - ) - else: # Multidimensional - md_conditions, md_conditions_map = _select_multidim_condition( - multidim_condition, - [k for k in turbine_power_thrust_tables[turb_type].keys() if k != "condition_keys"], - turbine_power_thrust_tables[turb_type]["condition_keys"], - velocities.shape[0], + + def _initialize_tilt_interpolation(self) -> None: + # TODO: + # Remove any duplicate wind speed entries + # _, duplicate_filter = np.unique(self.wind_speeds, return_index=True) + # self.tilt = self.tilt[duplicate_filter] + # self.wind_speeds = self.wind_speeds[duplicate_filter] + + if self.floating_tilt_table is not None: + self.floating_tilt_table = floris_numeric_dict_converter(self.floating_tilt_table) + + # If defined, create a tilt interpolation function for floating turbines. + # fill_value currently set to apply the min or max tilt angles if outside + # of the interpolation range. + if self.correct_cp_ct_for_tilt: + self.tilt_interp = interp1d( + self.floating_tilt_table["wind_speed"], + self.floating_tilt_table["tilt"], + fill_value=(0.0, self.floating_tilt_table["tilt"][-1]), + bounds_error=False, ) - # Loop over conditions and mask onto power - for i, md_cond in enumerate(md_conditions): - power_thrust_table = turbine_power_thrust_tables[turb_type][tuple(md_cond)] + def _initialize_multidim_power_thrust_table(self): + # Collect reference information + power_thrust_table_ref = copy.deepcopy(self.power_thrust_table) + self.ref_tilt = power_thrust_table_ref["ref_tilt"] + self.power_thrust_data_file = power_thrust_table_ref.pop("power_thrust_data_file") - power_model_kwargs["power_thrust_table"] = power_thrust_table - power_model_kwargs["tilt_interp"] = tilt_interps[turb_type] + # Solidify the data file path and name + self.power_thrust_data_file = self.turbine_library_path / self.power_thrust_data_file - p += ( - power_functions[turb_type](**power_model_kwargs) - * (turbine_type_map == turb_type) - * (md_conditions_map[:, None] == i) - ) + # Read in the multi-dimensional data supplied by the user. + data = np.genfromtxt(self.power_thrust_data_file, delimiter=',', names=True) - return p + # The CSV columns are: [condition_keys..., ws, power, thrust_coefficient] + # Condition keys are the leading columns (e.g. Tp, Hs) + # ws/power/thrust_coefficient are always the last 3 and are excluded. + self.condition_keys = list(data.dtype.names[:-3]) + # Find unique combinations of condition key values. + # np.column_stack promotes 1D arrays to (N, 1), so this works for any number of keys. + cond_data = np.column_stack([data[c] for c in self.condition_keys]) + unique_keys = [tuple(row) for row in np.unique(cond_data, axis=0)] -def thrust_coefficient( - velocities: NDArrayFloat, - turbulence_intensities: NDArrayFloat, - air_density: float, - yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, - power_setpoints: NDArrayFloat, + # Loop over the multi-dimensional keys to get the correct ws/Cp/Ct data to make + # the thrust_coefficient and power interpolants. + power_thrust_table_ = {} # Reset + for key in unique_keys: + # Build a boolean mask selecting rows that match this condition combination + mask = np.ones(len(data), dtype=bool) + for col, val in zip(self.condition_keys, key): + mask &= data[col] == val + + rows = data[mask] + + # Build the interpolants + power_thrust_table_.update( + { + key: { + "wind_speed": rows['ws'], + "power": rows['power'], + "thrust_coefficient": rows['thrust_coefficient'], + **power_thrust_table_ref + }, + } + ) + + # Save names of dimensions and set on-object version + power_thrust_table_.update({"condition_keys": self.condition_keys}) + self.power_thrust_table = power_thrust_table_ + + @power_thrust_table.validator + def _check_power_thrust_table(self, instance: attrs.Attribute, value: dict) -> None: + """ + Verify that the power and thrust tables are given with arrays of equal length + to the wind speed array. + """ + + if self.multi_dimensional_cp_ct: + if "power_thrust_data_file" in value.keys(): + return None + else: + key_types = [type(k) for k in value.keys()] + if key_types[0] in (tuple, float, int): + value = list(value.values())[0] # Check the first entry of multidim + else: + raise ValueError( + "power_thrust_data_file must be defined if multi_dimensional_cp_ct is True." + ) + + if not {"wind_speed", "power", "thrust_coefficient"} <= set(value.keys()): + raise ValueError( + """ + power_thrust_table dictionary must contain: + { + "wind_speed": List[float], + "power": List[float], + "thrust_coefficient": List[float], + } + """ + ) + + @rotor_diameter.validator + def _reset_rotor_diameter_dependencies(self, instance: attrs.Attribute, value: float) -> None: + """Resets the `rotor_radius` and `rotor_area` attributes.""" + # Temporarily turn off validators to avoid infinite recursion + with attrs.validators.disabled(): + # Reset the values + self.rotor_radius = value / 2.0 + self.rotor_area = np.pi * self.rotor_radius ** 2.0 + + @rotor_radius.validator + def _reset_rotor_radius(self, instance: attrs.Attribute, value: float) -> None: + """ + Resets the `rotor_diameter` value to trigger the recalculation of + `rotor_diameter`, `rotor_radius` and `rotor_area`. + """ + self.rotor_diameter = value * 2.0 + + @rotor_area.validator + def _reset_rotor_area(self, instance: attrs.Attribute, value: float) -> None: + """ + Resets the `rotor_radius` value to trigger the recalculation of + `rotor_diameter`, `rotor_radius` and `rotor_area`. + """ + self.rotor_radius = (value / np.pi) ** 0.5 + + @floating_tilt_table.validator + def _check_floating_tilt_table(self, instance: attrs.Attribute, value: dict | None) -> None: + """ + If the tilt / wind_speed table is defined, verify that the tilt and + wind_speed arrays are the same length. + """ + if value is None: + return + + if len(value.keys()) != 2 or set(value.keys()) != {"wind_speed", "tilt"}: + raise ValueError( + """ + floating_tilt_table dictionary must have the form: + { + "wind_speed": List[float], + "tilt": List[float], + } + """ + ) + + if any(len(np.shape(e)) > 1 for e in (value["tilt"], value["wind_speed"])): + raise ValueError("tilt and wind_speed inputs must be 1-D.") + + if len( {len(value["tilt"]), len(value["wind_speed"])} ) > 1: + raise ValueError("tilt and wind_speed inputs must be the same size.") + + @correct_cp_ct_for_tilt.validator + def _check_for_cp_ct_correct_flag_if_floating( + self, + instance: attrs.Attribute, + value: bool + ) -> None: + """ + Check that the boolean flag exists for correcting Cp/Ct for tilt + if a tile/wind_speed table is also defined. + """ + if self.correct_cp_ct_for_tilt and self.floating_tilt_table is None: + raise ValueError( + "To enable the Cp and Ct tilt correction, a tilt table must be given." + ) + + @operation_model.validator + def _op_model_validator(self, instance: attrs.Attribute, value): + if (isinstance(value, ControllerDependentTurbine) + and "demo" in + self.power_thrust_table["controller_dependent_turbine_parameters"]["cp_ct_data_file"] + ): + self.logger.warning( + "Cp/Ct data provided with FLORIS is for demonstration purposes only," + " and may not accurately reflect the actual Cp/Ct surfaces of reference wind" + " turbines." + ) + +def select_multidim_condition( + condition: dict, + specified_conditions: Iterable[tuple], + condition_keys: list[str], + n_findex: int, +) -> tuple: + """ + Convert condition to the type expected by power_thrust_table and select + nearest specified condition + """ + if type(condition) is dict: + # Check valid keys + if set(condition.keys()) != set(condition_keys): + raise ValueError( + f"The provided condition keys {list(condition.keys())} do not match the " + f"expected keys {condition_keys}. A single value should be provided for " + "each dimension of the multidimensional power/thrust_coefficient table." + ) + # Create a tuple of the condition values in the correct order + if isinstance(condition[condition_keys[0]], list) or isinstance( + condition[condition_keys[0]], np.ndarray + ): + # Assume multiple specified conditions + n_conds = len(condition[condition_keys[0]]) + if n_conds != n_findex: + raise ValueError( + "When providing multiple specified conditions, the number of conditions " + "must match the number of findices." + ) + for k in condition_keys: + if len(condition[k]) != n_conds: + raise ValueError( + "All condition values must have the same length when providing " + "multiple specified conditions." + ) + condition = [tuple(condition[k][i] for k in condition_keys) for i in range(n_conds)] + else: + n_conds = 1 + condition = [tuple(condition[k] for k in condition_keys)] + elif condition is None: + raise ValueError( + "multidim_condition must be provided if using multidimensional " + "power/thrust_coefficient." + ) + else: + raise TypeError("condition should be of type dict.") + + # Find the nearest key to the specified conditions. + specified_conditions = np.array(specified_conditions) + if specified_conditions.ndim == 1: # Single specified condition + specified_conditions = specified_conditions.reshape(-1, 1) + + # Find the nearest key to the specified conditions. + nearest_conditions = np.zeros((n_conds, specified_conditions.shape[1])) + for f, cond in enumerate(condition): # Loop over findices + for i, c in enumerate(cond): + nearest_conditions[f, i] = ( + specified_conditions[:, i][np.absolute(specified_conditions[:, i] - c).argmin()] + ) + + nearest_conditions, md_map = np.unique(nearest_conditions, axis=0, return_inverse=True) + + # Update map if only a single condition was provided + if n_conds == 1: + md_map = np.repeat(md_map, n_findex, axis=0) + + return nearest_conditions, md_map + + +def power( + turbines: list[Turbine], + velocities: NDArrayFloat, + turbulence_intensities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, awc_modes: NDArrayStr, awc_amplitudes: NDArrayFloat, - thrust_coefficient_functions: dict[str, Callable], - tilt_interps: dict[str, interp1d], - correct_cp_ct_for_tilt: NDArrayBool, turbine_type_map: NDArrayObject, - turbine_power_thrust_tables: dict, - ix_filter: NDArrayFilter | Iterable[int] | None = None, + ix_filter: NDArrayInt | Iterable[int] | None = None, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, multidim_condition: dict | None = None, ) -> NDArrayFloat: - - """Thrust coefficient of a turbine. - The value is obtained from the coefficient of thrust specified by the callables specified - in the thrust_coefficient_functions. + """ + Convenience function for computing the power of multiple turbines at once. Args: - velocities (NDArrayFloat[findex, turbines, grid1, grid2]): The velocity field at - a turbine. - turbulence_intensities (NDArrayFloat[findex, turbines]): The turbulence intensity at + turbines (list[Turbine]): List of all turbines in the farm + velocities (NDArrayFloat[n_findex, n_turbines, n_grid1, n_grid2]): The velocity field at the + turbines. + turbulence_intensities (NDArrayFloat[n_findex, n_turbines]): The turbulence intensity at each turbine. air_density (float): air density for simulation [kg/m^3] - yaw_angles (NDArrayFloat[findex, turbines]): The yaw angle for each turbine. - tilt_angles (NDArrayFloat[findex, turbines]): The tilt angle for each turbine. - power_setpoints: (NDArrayFloat[findex, turbines]): Maximum power setpoint for each + yaw_angles (NDArrayFloat[n_findex, n_turbines]): The yaw angle for each turbine. + power_setpoints: (NDArrayFloat[n_findex, n_turbines]): Maximum power setpoint for each turbine [W]. - awc_modes: (NDArrayStr[findex, turbines]): awc excitation mode (currently, only "baseline" - and "helix" are implemented). - awc_amplitudes: (NDArrayFloat[findex, turbines]): awc excitation amplitude for each + awc_modes: (NDArrayStr[n_findex, n_turbines]): awc excitation mode (currently, only + "baseline" and "helix" are implemented). + awc_amplitudes: (NDArrayFloat[n_findex, n_turbines]): awc excitation amplitude for each turbine [deg]. - thrust_coefficient_functions (dict): The thrust coefficient functions for each turbine. Keys - are the turbine type string and values are the callable functions. - tilt_interps (Iterable[tuple]): The tilt interpolation functions for each - turbine. - correct_cp_ct_for_tilt (NDArrayBool[findex, turbines]): Boolean for determining if the - turbines Cp and Ct should be corrected for tilt. - turbine_type_map: (NDArrayObject[findex, turbines]): The Turbine type definition + turbine_type_map: (NDArrayObject[n_findex, n_turbines]): The turbine_type definition for each turbine. ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or integer indices as an iterable of array to filter out before calculation. @@ -304,123 +500,119 @@ def thrust_coefficient( None. Returns: - NDArrayFloat: Coefficient of thrust for each requested turbine. + NDArrayFloat[n_findex, n_turbines]: The power, in Watts, for each turbine. """ + # Note: will only have one, if turbine_type is the same for all turbines. + turbine_dict = {t.turbine_type: t for t in turbines} + # Down-select inputs if ix_filter is given if ix_filter is not None: velocities = velocities[:, ix_filter] turbulence_intensities = turbulence_intensities[:, ix_filter] yaw_angles = yaw_angles[:, ix_filter] - tilt_angles = tilt_angles[:, ix_filter] power_setpoints = power_setpoints[:, ix_filter] awc_modes = awc_modes[:, ix_filter] awc_amplitudes = awc_amplitudes[:, ix_filter] turbine_type_map = turbine_type_map[:, ix_filter] - if type(correct_cp_ct_for_tilt) is bool: - pass - else: - correct_cp_ct_for_tilt = correct_cp_ct_for_tilt[:, ix_filter] - # Establish the main set of keyword arguments for thrust_coefficient() - thrust_model_kwargs = { - "power_thrust_table": None, # Will be filled below + # Establish the main set of keyword arguments for power() + # power_thrust_table and tilt arguments set below. + power_model_kwargs = { + "power_thrust_table": None, "velocities": velocities, "turbulence_intensities": turbulence_intensities, "air_density": air_density, "yaw_angles": yaw_angles, - "tilt_angles": tilt_angles, + "tilt_angles": None, "power_setpoints": power_setpoints, "awc_modes": awc_modes, "awc_amplitudes": awc_amplitudes, - "tilt_interp": None, # Will be filled below + "tilt_interp": None, "average_method": average_method, "cubature_weights": cubature_weights, - "correct_cp_ct_for_tilt": correct_cp_ct_for_tilt, + "correct_cp_ct_for_tilt": None, } - # Loop over each turbine type given to get thrust coefficient for all turbines - thrust_coefficient = np.zeros(np.shape(velocities)[0:2]) + # Loop over each turbine type given to get power for all turbines + p = np.zeros(np.shape(velocities)[0:2]) turb_types = np.unique(turbine_type_map) for turb_type in turb_types: - if "thrust_coefficient" in turbine_power_thrust_tables[turb_type]: # Not multidimensional - power_thrust_table = turbine_power_thrust_tables[turb_type] - - thrust_model_kwargs["power_thrust_table"] = power_thrust_table - thrust_model_kwargs["tilt_interp"] = tilt_interps[turb_type] - - thrust_coefficient += ( - thrust_coefficient_functions[turb_type](**thrust_model_kwargs) + # Tilt arguments + power_model_kwargs["tilt_angles"] = ( + turbine_dict[turb_type].ref_tilt * np.ones_like(yaw_angles) + ) + power_model_kwargs["tilt_interp"] = turbine_dict[turb_type].tilt_interp + power_model_kwargs["correct_cp_ct_for_tilt"] = ( + turbine_dict[turb_type].correct_cp_ct_for_tilt + ) + if "power" in turbine_dict[turb_type].power_thrust_table: # Not multidimensional + power_model_kwargs["power_thrust_table"] = turbine_dict[turb_type].power_thrust_table + p += ( + turbine_dict[turb_type].operation_model.power(**power_model_kwargs) * (turbine_type_map == turb_type) ) else: # Multidimensional - md_conditions, md_conditions_map = _select_multidim_condition( + md_conditions, md_conditions_map = select_multidim_condition( multidim_condition, - [k for k in turbine_power_thrust_tables[turb_type].keys() if k != "condition_keys"], - turbine_power_thrust_tables[turb_type]["condition_keys"], + [k for k in turbine_dict[turb_type].power_thrust_table.keys() + if k != "condition_keys"], + turbine_dict[turb_type].power_thrust_table["condition_keys"], velocities.shape[0], ) - # Loop over conditions and mask onto thrust_coefficient + # Loop over conditions and mask onto power for i, md_cond in enumerate(md_conditions): - power_thrust_table = turbine_power_thrust_tables[turb_type][tuple(md_cond)] - - thrust_model_kwargs["power_thrust_table"] = power_thrust_table - thrust_model_kwargs["tilt_interp"] = tilt_interps[turb_type] + power_model_kwargs["power_thrust_table"] = ( + turbine_dict[turb_type].power_thrust_table[tuple(md_cond)] + ) - thrust_coefficient += ( - thrust_coefficient_functions[turb_type](**thrust_model_kwargs) + p += ( + turbine_dict[turb_type].operation_model.power(**power_model_kwargs) * (turbine_type_map == turb_type) * (md_conditions_map[:, None] == i) ) - return thrust_coefficient + return p -def axial_induction( +def thrust_coefficient( + turbines: list[Turbine], velocities: NDArrayFloat, turbulence_intensities: NDArrayFloat, air_density: float, yaw_angles: NDArrayFloat, - tilt_angles: NDArrayFloat, power_setpoints: NDArrayFloat, awc_modes: NDArrayStr, awc_amplitudes: NDArrayFloat, - axial_induction_functions: dict, - tilt_interps: NDArrayObject, - correct_cp_ct_for_tilt: NDArrayBool, turbine_type_map: NDArrayObject, - turbine_power_thrust_tables: dict, - ix_filter: NDArrayFilter | Iterable[int] | None = None, + ix_filter: NDArrayInt | Iterable[int] | None = None, average_method: str = "cubic-mean", cubature_weights: NDArrayFloat | None = None, multidim_condition: dict | None = None, ) -> NDArrayFloat: - """Axial induction factor of the turbine incorporating - the thrust coefficient and yaw angle. + + """ + Convenience function for calling thrust_coefficient on multiple turbines at once. Args: - velocities (NDArrayFloat): The velocity field at each turbine; should be shape: - (number of turbines, ngrid, ngrid), or (ngrid, ngrid) for a single turbine. - turbulence_intensities (NDArrayFloat[findex, turbines]): The turbulence intensity at + turbines (list[Turbine]): List of all turbines in the farm + velocities (NDArrayFloat[n_findex, n_turbines, n_grid1, n_grid2]): The velocity field at the + turbines. + turbulence_intensities (NDArrayFloat[n_findex, n_turbines]): The turbulence intensity at each turbine. air_density (float): air density for simulation [kg/m^3] - yaw_angles (NDArrayFloat[findex, turbines]): The yaw angle for each turbine. - tilt_angles (NDArrayFloat[findex, turbines]): The tilt angle for each turbine. - power_setpoints: (NDArrayFloat[findex, turbines]): Maximum power setpoint for each + yaw_angles (NDArrayFloat[n_findex, n_turbines]): The yaw angle for each turbine. + power_setpoints: (NDArrayFloat[n_findex, n_turbines]): Maximum power setpoint for each turbine [W]. - awc_amplitudes: (NDArrayFloat[findex, turbines]): awc excitation amplitude for each + awc_modes: (NDArrayStr[n_findex, n_turbines]): awc excitation mode (currently, only + "baseline" and "helix" are implemented). + awc_amplitudes: (NDArrayFloat[n_findex, n_turbines]): awc excitation amplitude for each turbine [deg]. - axial_induction_functions (dict): The axial induction functions for each turbine. Keys are - the turbine type string and values are the callable functions. - tilt_interps (Iterable[tuple]): The tilt interpolation functions for each - turbine. - correct_cp_ct_for_tilt (NDArrayBool[findex, turbines]): Boolean for determining if the - turbines Cp and Ct should be corrected for tilt. - turbine_type_map: (NDArrayObject[findex, turbines]): The Turbine type definition + turbine_type_map: (NDArrayObject[n_findex, n_turbines]): The turbine_type definition for each turbine. ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or - integer indices (as an array or iterable) to filter out before calculation. + integer indices as an iterable of array to filter out before calculation. Defaults to None. average_method (str, optional): The method for averaging over turbine rotor points to determine a rotor-average wind speed. Defaults to "cubic-mean". @@ -431,357 +623,207 @@ def axial_induction( None. Returns: - Union[float, NDArrayFloat]: [description] + NDArrayFloat[n_findex, n_turbines]: Coefficient of thrust for each requested turbine. """ + # Note: will only have one, if turbine_type is the same for all turbines. + turbine_dict = {t.turbine_type: t for t in turbines} + # Down-select inputs if ix_filter is given if ix_filter is not None: velocities = velocities[:, ix_filter] turbulence_intensities = turbulence_intensities[:, ix_filter] yaw_angles = yaw_angles[:, ix_filter] - tilt_angles = tilt_angles[:, ix_filter] power_setpoints = power_setpoints[:, ix_filter] awc_modes = awc_modes[:, ix_filter] awc_amplitudes = awc_amplitudes[:, ix_filter] turbine_type_map = turbine_type_map[:, ix_filter] - if type(correct_cp_ct_for_tilt) is bool: - pass - else: - correct_cp_ct_for_tilt = correct_cp_ct_for_tilt[:, ix_filter] - # Establish the main set of keyword arguments for axial_induction() - axial_induction_model_kwargs = { - "power_thrust_table": None, # Will be filled below + # Establish the main set of keyword arguments for thrust_coefficient() + # power_thrust_table and tilt arguments set below. + thrust_model_kwargs = { + "power_thrust_table": None, "velocities": velocities, "turbulence_intensities": turbulence_intensities, "air_density": air_density, "yaw_angles": yaw_angles, - "tilt_angles": tilt_angles, + "tilt_angles": None, "power_setpoints": power_setpoints, "awc_modes": awc_modes, "awc_amplitudes": awc_amplitudes, - "tilt_interp": None, # Will be filled below + "tilt_interp": None, "average_method": average_method, "cubature_weights": cubature_weights, - "correct_cp_ct_for_tilt": correct_cp_ct_for_tilt, + "correct_cp_ct_for_tilt": None, } - # Loop over each turbine type given to get axial induction for all turbines - axial_induction = np.zeros(np.shape(velocities)[0:2]) + # Loop over each turbine type given to get thrust coefficient for all turbines + thrust_coefficient = np.zeros(np.shape(velocities)[0:2]) turb_types = np.unique(turbine_type_map) for turb_type in turb_types: - if "thrust_coefficient" in turbine_power_thrust_tables[turb_type]: # Not multidimensional - power_thrust_table = turbine_power_thrust_tables[turb_type] - - axial_induction_model_kwargs["power_thrust_table"] = power_thrust_table - axial_induction_model_kwargs["tilt_interp"] = tilt_interps[turb_type] - - axial_induction += ( - axial_induction_functions[turb_type](**axial_induction_model_kwargs) + # Tilt arguments + thrust_model_kwargs["tilt_angles"] = ( + turbine_dict[turb_type].ref_tilt * np.ones_like(yaw_angles) + ) + thrust_model_kwargs["tilt_interp"] = turbine_dict[turb_type].tilt_interp + thrust_model_kwargs["correct_cp_ct_for_tilt"] = ( + turbine_dict[turb_type].correct_cp_ct_for_tilt + ) + if "thrust_coefficient" in turbine_dict[turb_type].power_thrust_table: # Not multidim + thrust_model_kwargs["power_thrust_table"] = turbine_dict[turb_type].power_thrust_table + thrust_coefficient += ( + turbine_dict[turb_type].operation_model.thrust_coefficient(**thrust_model_kwargs) * (turbine_type_map == turb_type) ) else: # Multidimensional - md_conditions, md_conditions_map = _select_multidim_condition( + md_conditions, md_conditions_map = select_multidim_condition( multidim_condition, - [k for k in turbine_power_thrust_tables[turb_type].keys() if k != "condition_keys"], - turbine_power_thrust_tables[turb_type]["condition_keys"], + [k for k in turbine_dict[turb_type].power_thrust_table.keys() + if k != "condition_keys"], + turbine_dict[turb_type].power_thrust_table["condition_keys"], velocities.shape[0], ) - # Loop over conditions and mask onto axial_induction + # Loop over conditions and mask onto thrust_coefficient for i, md_cond in enumerate(md_conditions): - power_thrust_table = turbine_power_thrust_tables[turb_type][tuple(md_cond)] - - axial_induction_model_kwargs["power_thrust_table"] = power_thrust_table - axial_induction_model_kwargs["tilt_interp"] = tilt_interps[turb_type] + thrust_model_kwargs["power_thrust_table"] = ( + turbine_dict[turb_type].power_thrust_table[tuple(md_cond)] + ) - axial_induction += ( - axial_induction_functions[turb_type](**axial_induction_model_kwargs) + thrust_coefficient += ( + turbine_dict[turb_type].operation_model.thrust_coefficient( + **thrust_model_kwargs + ) * (turbine_type_map == turb_type) * (md_conditions_map[:, None] == i) ) - return axial_induction - + return thrust_coefficient -@define -class Turbine(BaseClass): - """ - A class containing the parameters and infrastructure to model a wind turbine's performance - for a particular atmospheric condition. - Args: - turbine_type (str): An identifier for this type of turbine such as "NREL_5MW". - rotor_diameter (float): The rotor diameter in meters. - hub_height (float): The hub height in meters. - TSR (float): The Tip Speed Ratio of the turbine. - power_thrust_table (dict[str, float]): Contains power coefficient and thrust coefficient - values at a series of wind speeds to define the turbine performance. - The dictionary must have the following three keys with equal length values: - { - "wind_speeds": List[float], - "power": List[float], - "thrust": List[float], - } - or, contain a key "power_thrust_data_file" pointing to the power/thrust data. - Optionally, power_thrust_table may include parameters for use in the turbine submodel, - for example: - cosine_loss_exponent_yaw (float): The cosine exponent relating the yaw misalignment - angle to turbine power. - cosine_loss_exponent_tilt (float): The cosine exponent relating the rotor tilt angle - to turbine power. - ref_air_density (float): The density at which the provided Cp and Ct curves are - defined. - ref_tilt (float): The implicit tilt of the turbine for which the Cp and Ct - curves are defined. This is typically the nacelle tilt. - correct_cp_ct_for_tilt (bool): A flag to indicate whether to correct Cp and Ct for tilt - usually for a floating turbine. - Optional, defaults to False. - floating_tilt_table (dict[str, float]): Look up table of tilt angles at a series of - wind speeds. The dictionary must have the following keys with equal length values: - { - "wind_speeds": List[float], - "tilt": List[float], - } - Required if `correct_cp_ct_for_tilt = True`. Defaults to None. - multi_dimensional_cp_ct (bool): Use a multidimensional power_thrust_table. Defaults to - False. +def axial_induction( + turbines: list[Turbine], + velocities: NDArrayFloat, + turbulence_intensities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + power_setpoints: NDArrayFloat, + awc_modes: NDArrayStr, + awc_amplitudes: NDArrayFloat, + turbine_type_map: NDArrayObject, + ix_filter: NDArrayInt | Iterable[int] | None = None, + average_method: str = "cubic-mean", + cubature_weights: NDArrayFloat | None = None, + multidim_condition: dict | None = None, +) -> NDArrayFloat: """ - turbine_type: str = field() - rotor_diameter: float = field() - hub_height: float = field() - TSR: float = field() - power_thrust_table: dict = field(default={}) # conversion to numpy in __post_init__ - operation_model: str = field(default="cosine-loss") - - correct_cp_ct_for_tilt: bool = field(default=False) - floating_tilt_table: dict[str, NDArrayFloat] | None = field(default=None) - - multi_dimensional_cp_ct: bool = field(default=False) - - # Initialized in the post_init function - rotor_radius: float = field(init=False) - rotor_area: float = field(init=False) - thrust_coefficient_function: Callable = field(init=False) - axial_induction_function: Callable = field(init=False) - power_function: Callable = field(init=False) - tilt_interp: interp1d = field(init=False, default=None) - power_thrust_data_file: str = field(default=None) + Convenience function for computing the axial induction factor of multiple turbines at once. - # Only used by mutlidimensional turbines - turbine_library_path: Path = field( - default=Path(__file__).parents[2] / "turbine_library", - converter=convert_to_path, - validator=attrs.validators.instance_of(Path) - ) - - # Not to be provided by the user - condition_keys: list[str] = field(init=False, factory=list) + Args: + turbines (list[Turbine]): List of all turbines in the farm + velocities (NDArrayFloat[n_findex, n_turbines, n_grid1, n_grid2]): The velocity field at the + turbines. + turbulence_intensities (NDArrayFloat[n_findex, n_turbines]): The turbulence intensity at + each turbine. + air_density (float): air density for simulation [kg/m^3] + yaw_angles (NDArrayFloat[n_findex, n_turbines]): The yaw angle for each turbine. + power_setpoints: (NDArrayFloat[n_findex, n_turbines]): Maximum power setpoint for each + turbine [W]. + awc_modes: (NDArrayStr[n_findex, n_turbines]): awc excitation mode (currently, only + "baseline" and "helix" are implemented). + awc_amplitudes: (NDArrayFloat[n_findex, n_turbines]): awc excitation amplitude for each + turbine [deg]. + turbine_type_map: (NDArrayObject[n_findex, n_turbines]): The turbine_type definition + for each turbine. + ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or + integer indices as an iterable of array to filter out before calculation. + Defaults to None. + average_method (str, optional): The method for averaging over turbine rotor points + to determine a rotor-average wind speed. Defaults to "cubic-mean". + cubature_weights (NDArrayFloat | None): Weights for cubature averaging methods. Defaults to + None. + multidim_condition (dict | None): The condition dictionary used to select the appropriate + thrust coefficient relationship for multidimensional power/thrust tables. Defaults to + None. - def __attrs_post_init__(self) -> None: - self._initialize_power_thrust_functions() - self.__post_init__() + Returns: + NDArrayFloat[n_findex, n_turbines]: Axial induction factor for each requested turbine. + """ - def __post_init__(self) -> None: - self._initialize_tilt_interpolation() + # Note: will only have one, if turbine_type is the same for all turbines. + turbine_dict = {t.turbine_type: t for t in turbines} - bypass_numeric_converter = False - if self.multi_dimensional_cp_ct: - self._initialize_multidim_power_thrust_table() - bypass_numeric_converter = True + # Down-select inputs if ix_filter is given + if ix_filter is not None: + velocities = velocities[:, ix_filter] + turbulence_intensities = turbulence_intensities[:, ix_filter] + yaw_angles = yaw_angles[:, ix_filter] + power_setpoints = power_setpoints[:, ix_filter] + awc_modes = awc_modes[:, ix_filter] + awc_amplitudes = awc_amplitudes[:, ix_filter] + turbine_type_map = turbine_type_map[:, ix_filter] - # Check for whether a cp_ct_data_file is specified, and load it if so. - if "controller_dependent_turbine_parameters" in self.power_thrust_table: - floris_root = Path(__file__).resolve().parents[2] - file_path = ( - floris_root / "turbine_library" / - self.power_thrust_table["controller_dependent_turbine_parameters"] - ["cp_ct_data_file"] - ) - npz_data = dict(np.load(file_path)) - self.power_thrust_table["controller_dependent_turbine_parameters"]["cp_ct_data"] = { - k: v.tolist() for k, v in npz_data.items() - } - bypass_numeric_converter = True + # Establish the main set of keyword arguments for axial_induction() + # power_thrust_table and tilt arguments set below. + axial_induction_model_kwargs = { + "power_thrust_table": None, + "velocities": velocities, + "turbulence_intensities": turbulence_intensities, + "air_density": air_density, + "yaw_angles": yaw_angles, + "tilt_angles": None, + "power_setpoints": power_setpoints, + "awc_modes": awc_modes, + "awc_amplitudes": awc_amplitudes, + "tilt_interp": None, + "average_method": average_method, + "cubature_weights": cubature_weights, + "correct_cp_ct_for_tilt": None, + } - # Raise warning if "demo" in the cp_ct data file name - if ( - self.operation_model in ["controller-dependent"] - and "demo" in self.power_thrust_table["controller_dependent_turbine_parameters"] - ["cp_ct_data_file"] - ): - self.logger.warning( - "Cp/Ct data provided with FLORIS is for demonstration purposes only," - " and may not accurately reflect the actual Cp/Ct surfaces of reference wind" - " turbines." + # Loop over each turbine type given to get axial induction for all turbines + axial_induction = np.zeros(np.shape(velocities)[0:2]) + turb_types = np.unique(turbine_type_map) + for turb_type in turb_types: + # Tilt arguments + axial_induction_model_kwargs["tilt_angles"] = ( + turbine_dict[turb_type].ref_tilt * np.ones_like(yaw_angles) + ) + axial_induction_model_kwargs["tilt_interp"] = turbine_dict[turb_type].tilt_interp + axial_induction_model_kwargs["correct_cp_ct_for_tilt"] = ( + turbine_dict[turb_type].correct_cp_ct_for_tilt + ) + if "thrust_coefficient" in turbine_dict[turb_type].power_thrust_table: # Not multidim + axial_induction_model_kwargs["power_thrust_table"] = ( + turbine_dict[turb_type].power_thrust_table ) - - if not bypass_numeric_converter: - self.power_thrust_table = floris_numeric_dict_converter(self.power_thrust_table) - - def _initialize_power_thrust_functions(self) -> None: - turbine_function_model = TURBINE_MODEL_MAP["operation_model"][self.operation_model] - self.thrust_coefficient_function = turbine_function_model.thrust_coefficient - self.axial_induction_function = turbine_function_model.axial_induction - self.power_function = turbine_function_model.power - - - def _initialize_tilt_interpolation(self) -> None: - # TODO: - # Remove any duplicate wind speed entries - # _, duplicate_filter = np.unique(self.wind_speeds, return_index=True) - # self.tilt = self.tilt[duplicate_filter] - # self.wind_speeds = self.wind_speeds[duplicate_filter] - - if self.floating_tilt_table is not None: - self.floating_tilt_table = floris_numeric_dict_converter(self.floating_tilt_table) - - # If defined, create a tilt interpolation function for floating turbines. - # fill_value currently set to apply the min or max tilt angles if outside - # of the interpolation range. - if self.correct_cp_ct_for_tilt: - self.tilt_interp = interp1d( - self.floating_tilt_table["wind_speed"], - self.floating_tilt_table["tilt"], - fill_value=(0.0, self.floating_tilt_table["tilt"][-1]), - bounds_error=False, + axial_induction += ( + turbine_dict[turb_type].operation_model.axial_induction( + **axial_induction_model_kwargs + ) + * (turbine_type_map == turb_type) ) - - def _initialize_multidim_power_thrust_table(self): - # Collect reference information - power_thrust_table_ref = copy.deepcopy(self.power_thrust_table) - self.power_thrust_data_file = power_thrust_table_ref.pop("power_thrust_data_file") - - # Solidify the data file path and name - self.power_thrust_data_file = self.turbine_library_path / self.power_thrust_data_file - - # Read in the multi-dimensional data supplied by the user. - df = pd.read_csv(self.power_thrust_data_file) - - # Down-select the DataFrame to have just the ws, Cp, and Ct values - index_col = df.columns.values[:-3] - self.condition_keys = index_col.tolist() - df2 = df.set_index(index_col.tolist()) - - # Loop over the multi-dimensional keys to get the correct ws/Cp/Ct data to make - # the thrust_coefficient and power interpolants. - power_thrust_table_ = {} # Reset - for key in df2.index.unique(): - # Select the correct ws/Cp/Ct data - data = df2.loc[key] - if type(key) is not tuple: - key = (key,) - - # Build the interpolants - power_thrust_table_.update( - { - key: { - "wind_speed": data['ws'].values, - "power": data['power'].values, - "thrust_coefficient": data['thrust_coefficient'].values, - **power_thrust_table_ref - }, - } + else: # Multidimensional + md_conditions, md_conditions_map = select_multidim_condition( + multidim_condition, + [k for k in turbine_dict[turb_type].power_thrust_table.keys() + if k != "condition_keys"], + turbine_dict[turb_type].power_thrust_table["condition_keys"], + velocities.shape[0], ) - # Add reference information at the lower level - # Save names of dimensions and set on-object version - power_thrust_table_.update({"condition_keys": self.condition_keys}) - self.power_thrust_table = power_thrust_table_ - - @power_thrust_table.validator - def check_power_thrust_table(self, instance: attrs.Attribute, value: dict) -> None: - """ - Verify that the power and thrust tables are given with arrays of equal length - to the wind speed array. - """ + # Loop over conditions and mask onto axial_induction + for i, md_cond in enumerate(md_conditions): + axial_induction_model_kwargs["power_thrust_table"] = ( + turbine_dict[turb_type].power_thrust_table[tuple(md_cond)] + ) - if self.multi_dimensional_cp_ct: - if "power_thrust_data_file" in value.keys(): - return None - else: - key_types = [type(k) for k in value.keys()] - if key_types[0] in (tuple, float, int): - value = list(value.values())[0] # Check the first entry of multidim - else: - raise ValueError( - "power_thrust_data_file must be defined if multi_dimensional_cp_ct is True." + axial_induction += ( + turbine_dict[turb_type].operation_model.axial_induction( + **axial_induction_model_kwargs ) + * (turbine_type_map == turb_type) + * (md_conditions_map[:, None] == i) + ) - if not {"wind_speed", "power", "thrust_coefficient"} <= set(value.keys()): - raise ValueError( - """ - power_thrust_table dictionary must contain: - { - "wind_speed": List[float], - "power": List[float], - "thrust_coefficient": List[float], - } - """ - ) - - @rotor_diameter.validator - def reset_rotor_diameter_dependencies(self, instance: attrs.Attribute, value: float) -> None: - """Resets the `rotor_radius` and `rotor_area` attributes.""" - # Temporarily turn off validators to avoid infinite recursion - with attrs.validators.disabled(): - # Reset the values - self.rotor_radius = value / 2.0 - self.rotor_area = np.pi * self.rotor_radius ** 2.0 - - @rotor_radius.validator - def reset_rotor_radius(self, instance: attrs.Attribute, value: float) -> None: - """ - Resets the `rotor_diameter` value to trigger the recalculation of - `rotor_diameter`, `rotor_radius` and `rotor_area`. - """ - self.rotor_diameter = value * 2.0 - - @rotor_area.validator - def reset_rotor_area(self, instance: attrs.Attribute, value: float) -> None: - """ - Resets the `rotor_radius` value to trigger the recalculation of - `rotor_diameter`, `rotor_radius` and `rotor_area`. - """ - self.rotor_radius = (value / np.pi) ** 0.5 - - @floating_tilt_table.validator - def check_floating_tilt_table(self, instance: attrs.Attribute, value: dict | None) -> None: - """ - If the tilt / wind_speed table is defined, verify that the tilt and - wind_speed arrays are the same length. - """ - if value is None: - return - - if len(value.keys()) != 2 or set(value.keys()) != {"wind_speed", "tilt"}: - raise ValueError( - """ - floating_tilt_table dictionary must have the form: - { - "wind_speed": List[float], - "tilt": List[float], - } - """ - ) - - if any(len(np.shape(e)) > 1 for e in (value["tilt"], value["wind_speed"])): - raise ValueError("tilt and wind_speed inputs must be 1-D.") - - if len( {len(value["tilt"]), len(value["wind_speed"])} ) > 1: - raise ValueError("tilt and wind_speed inputs must be the same size.") - - @correct_cp_ct_for_tilt.validator - def check_for_cp_ct_correct_flag_if_floating( - self, - instance: attrs.Attribute, - value: bool - ) -> None: - """ - Check that the boolean flag exists for correcting Cp/Ct for tilt - if a tile/wind_speed table is also defined. - """ - if self.correct_cp_ct_for_tilt and self.floating_tilt_table is None: - raise ValueError( - "To enable the Cp and Ct tilt correction, a tilt table must be given." - ) + return axial_induction diff --git a/floris/core/turbine/unified_momentum_model.py b/floris/core/turbine/unified_momentum_model.py index 2517481788..2ecd4d3605 100644 --- a/floris/core/turbine/unified_momentum_model.py +++ b/floris/core/turbine/unified_momentum_model.py @@ -17,7 +17,7 @@ average_velocity, rotor_velocity_air_density_correction, ) -from floris.core.turbine.operation_models import BaseOperationModel +from floris.core.turbine import BaseOperationModel from floris.type_dec import NDArrayFloat @@ -248,6 +248,7 @@ class UnifiedMomentumModelTurbine(BaseOperationModel): Turbine operation model as described by Heck et al. (2023). """ + @staticmethod def power( power_thrust_table: dict, velocities: NDArrayFloat, @@ -305,6 +306,7 @@ def power( return power + @staticmethod def thrust_coefficient( power_thrust_table: dict, velocities: NDArrayFloat, @@ -356,6 +358,7 @@ def thrust_coefficient( return yawed_thrust_coefficients + @staticmethod def axial_induction( power_thrust_table: dict, velocities: NDArrayFloat, diff --git a/floris/core/wake.py b/floris/core/wake.py index e58a85cb46..d80067aea2 100644 --- a/floris/core/wake.py +++ b/floris/core/wake.py @@ -1,63 +1,70 @@ -import attrs +from typing import Callable + from attrs import define, field -from floris.core import BaseClass, BaseModel -from floris.core.wake_combination import ( - FLS, - MAX, - SOSFS, -) -from floris.core.wake_deflection import ( - EmpiricalGaussVelocityDeflection, - GaussVelocityDeflection, - JimenezVelocityDeflection, - NoneVelocityDeflection, +from floris.core import ( + BaseClass, + BaseLibrary, + BaseModel, ) -from floris.core.wake_turbulence import ( - CrespoHernandez, - NoneWakeTurbulence, - WakeInducedMixing, +from floris.core.wake_model import ( + BaseWakeModel, + CumulativeCurl, + EmpiricalGauss, + Gauss, + JensenJimenez, + NoneWake, + TurbOParkGauss, ) -from floris.core.wake_velocity import ( - CumulativeGaussCurlVelocityDeficit, - EmpiricalGaussVelocityDeficit, - GaussVelocityDeficit, - JensenVelocityDeficit, - NoneVelocityDeficit, - TurboparkgaussVelocityDeficit, - TurbOParkVelocityDeficit, +from floris.core.wake_model.wake_combination import ( + fls, + maximum, + none_combination, + sosfs, ) MODEL_MAP = { - "combination_model": { - "fls": FLS, - "max": MAX, - "sosfs": SOSFS - }, - "deflection_model": { - "jimenez": JimenezVelocityDeflection, - "gauss": GaussVelocityDeflection, - "none": NoneVelocityDeflection, - "empirical_gauss": EmpiricalGaussVelocityDeflection - }, - "turbulence_model": { - "none": NoneWakeTurbulence, - "crespo_hernandez": CrespoHernandez, - "wake_induced_mixing": WakeInducedMixing - }, - "velocity_model": { - "none": NoneVelocityDeficit, - "cc": CumulativeGaussCurlVelocityDeficit, - "gauss": GaussVelocityDeficit, - "jensen": JensenVelocityDeficit, - "turbopark": TurbOParkVelocityDeficit, - "empirical_gauss": EmpiricalGaussVelocityDeficit, - "turboparkgauss": TurboparkgaussVelocityDeficit, - }, + "none": NoneWake, + "cc": CumulativeCurl, + "gauss": Gauss, + "jensen": JensenJimenez, + "empirical_gauss": EmpiricalGauss, + "turboparkgauss": TurbOParkGauss, +} + +COMBINATION_MAP = { + "none": none_combination, + "fls": fls, + "max": maximum, + "sosfs": sosfs, } +def _wake_model_converter(model, model_parameters): + # If model is a string, instantiate from MODEL_MAP using model_parameters + if isinstance(model, str): + if model == "none": + return NoneWake() + elif model not in MODEL_MAP: + valid_models = list(MODEL_MAP.keys()) + raise ValueError( + f"Unknown velocity model '{model}'. " + f"Expected one of {valid_models}." + ) + else: + return MODEL_MAP[model](**model_parameters) + + # Handle dict representation of a wake model (use existing parameters on model) + elif isinstance(model, dict): + return BaseLibrary.from_dict(model) + + # Otherwise, raise an error + else: + raise TypeError( + "model must be a BaseWakeModel subclass (in dict representation), " + "or a valid velocity-model string." + ) @define class WakeModelManager(BaseClass): @@ -68,97 +75,19 @@ class WakeModelManager(BaseClass): Args: wake (:obj:`dict`): The wake's properties input dictionary - velocity_model (str): The name of the velocity model to be instantiated. - - turbulence_model (str): The name of the turbulence model to be instantiated. - - deflection_model (str): The name of the deflection model to be instantiated. - combination_model (str): The name of the combination model to be instantiated. """ - model_strings: dict = field(converter=dict) - enable_secondary_steering: bool = field(converter=bool) - enable_yaw_added_recovery: bool = field(converter=bool) - enable_active_wake_mixing: bool = field(converter=bool) - enable_transverse_velocities: bool = field(converter=bool) - - wake_deflection_parameters: dict = field(converter=dict) - wake_turbulence_parameters: dict = field(converter=dict) - wake_velocity_parameters: dict = field(converter=dict, factory=dict) - - combination_model: BaseModel = field(init=False) - deflection_model: BaseModel = field(init=False) - turbulence_model: BaseModel = field(init=False) - velocity_model: BaseModel = field(init=False) + model: str | BaseWakeModel = field() + parameters: dict = field(converter=dict) + combination_model: str | Callable = field(default="sosfs") def __attrs_post_init__(self) -> None: - velocity_model_string = self.model_strings["velocity_model"].lower() - model: BaseModel = MODEL_MAP["velocity_model"][velocity_model_string] - if velocity_model_string == "none": - model_parameters = None - else: - model_parameters = self.wake_velocity_parameters[velocity_model_string] - if model_parameters is None: - # Use model defaults - self.velocity_model = model() - else: - self.velocity_model = model.from_dict(model_parameters) - - deflection_model_string = self.model_strings["deflection_model"].lower() - model: BaseModel = MODEL_MAP["deflection_model"][deflection_model_string] - if deflection_model_string == "none": - model_parameters = None - else: - model_parameters = self.wake_deflection_parameters[deflection_model_string] - if model_parameters is None: - self.deflection_model = model() - else: - self.deflection_model = model.from_dict(model_parameters) - - turbulence_model_string = self.model_strings["turbulence_model"].lower() - model: BaseModel = MODEL_MAP["turbulence_model"][turbulence_model_string] - if turbulence_model_string == "none": - model_parameters = None - else: - model_parameters = self.wake_turbulence_parameters[turbulence_model_string] - if model_parameters is None: - self.turbulence_model = model() - else: - self.turbulence_model = model.from_dict(model_parameters) - - combination_model_string = self.model_strings["combination_model"].lower() - model: BaseModel = MODEL_MAP["combination_model"][combination_model_string] - self.combination_model = model() - - @model_strings.validator - def validate_model_strings(self, instance: attrs.Attribute, value: dict) -> None: - required_strings = [ - "velocity_model", - "deflection_model", - "combination_model", - "turbulence_model" - ] - # Check that all required strings are given - for s in required_strings: - if s not in value.keys(): - raise KeyError(f"Wake: '{s}' not provided in the input but it is required.") - - # Check that no other strings are given - for k in value.keys(): - if k not in required_strings: - raise KeyError(( - f"Wake: '{k}' was given as input but it is not a valid option." - f"Required inputs are: {', '.join(required_strings)}" - )) - - @property - def deflection_function(self): - return self.deflection_model.function - @property - def velocity_function(self): - return self.velocity_model.function + self.model = _wake_model_converter(self.model, self.parameters) - @property - def turbulence_function(self): - return self.turbulence_model.function + if isinstance(self.combination_model, str): + self.combination_model = COMBINATION_MAP[self.combination_model] + self.model.assign_combination_function(self.combination_model) - @property - def combination_function(self): - return self.combination_model.function + def assign_user_defined_wake_model(self, wake_model: BaseWakeModel): + self.model = wake_model diff --git a/floris/core/wake_combination/__init__.py b/floris/core/wake_combination/__init__.py deleted file mode 100644 index 246aab65c2..0000000000 --- a/floris/core/wake_combination/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from floris.core.wake_combination.fls import FLS -from floris.core.wake_combination.max import MAX -from floris.core.wake_combination.sosfs import SOSFS diff --git a/floris/core/wake_combination/fls.py b/floris/core/wake_combination/fls.py deleted file mode 100644 index 42e68045f6..0000000000 --- a/floris/core/wake_combination/fls.py +++ /dev/null @@ -1,32 +0,0 @@ - -import numpy as np -from attrs import define - -from floris.core import BaseModel - - -@define -class FLS(BaseModel): - """ - FLS uses freestream linear superposition to apply the wake velocity - deficits to the freestream flow field. - """ - - def prepare_function(self) -> dict: - pass - - def function(self, wake_field: np.ndarray, velocity_field: np.ndarray): - """ - Combines the base flow field with the velocity deficits - using freestream linear superposition. In other words, the wake - field and base fields are simply added together. - - Args: - u_field (np.array): The base flow field. - u_wake (np.array): The wake to apply to the base flow field. - - Returns: - np.array: The resulting flow field after applying the wake to the - base. - """ - return wake_field + velocity_field diff --git a/floris/core/wake_combination/max.py b/floris/core/wake_combination/max.py deleted file mode 100644 index 0898cc842f..0000000000 --- a/floris/core/wake_combination/max.py +++ /dev/null @@ -1,38 +0,0 @@ - -import numpy as np -from attrs import define - -from floris.core import BaseModel - - -@define -class MAX(BaseModel): - """ - MAX uses the maximum wake velocity deficit to add to the - base flow field. For more information, refer to - :cite:`max-gunn2016limitations`. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: max- - """ - - def prepare_function(self) -> dict: - pass - - def function(self, wake_field: np.ndarray, velocity_field: np.ndarray): - """ - Incorporates the velocity deficits into the base flow field by - selecting the maximum of the two for each point. - - Args: - u_field (np.array): The base flow field. - u_wake (np.array): The wake to apply to the base flow field. - - Returns: - np.array: The resulting flow field after applying the wake to the - base. - """ - return np.maximum(wake_field, velocity_field) diff --git a/floris/core/wake_combination/sosfs.py b/floris/core/wake_combination/sosfs.py deleted file mode 100644 index 305ff68035..0000000000 --- a/floris/core/wake_combination/sosfs.py +++ /dev/null @@ -1,33 +0,0 @@ - -import numpy as np -from attrs import define - -from floris.core import BaseModel - - -@define -class SOSFS(BaseModel): - """ - SOSFS uses sum of squares freestream superposition to combine the - wake velocity deficits to the base flow field. - - For more information, refer to :cite:`katic_sos_1986`. - """ - - def prepare_function(self) -> dict: - pass - - def function(self, wake_field: np.ndarray, velocity_field: np.ndarray): - """ - Combines the base flow field with the velocity deficits - using sum of squares. - - Args: - u_field (np.array): The base flow field. - u_wake (np.array): The wake to apply to the base flow field. - - Returns: - np.array: The resulting flow field after applying the wake to the - base. - """ - return np.hypot(wake_field, velocity_field) diff --git a/floris/core/wake_deflection/__init__.py b/floris/core/wake_deflection/__init__.py deleted file mode 100644 index ba5e637886..0000000000 --- a/floris/core/wake_deflection/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ - -from floris.core.wake_deflection.empirical_gauss import EmpiricalGaussVelocityDeflection -from floris.core.wake_deflection.gauss import GaussVelocityDeflection -from floris.core.wake_deflection.jimenez import JimenezVelocityDeflection -from floris.core.wake_deflection.none import NoneVelocityDeflection diff --git a/floris/core/wake_deflection/empirical_gauss.py b/floris/core/wake_deflection/empirical_gauss.py deleted file mode 100644 index 185588f528..0000000000 --- a/floris/core/wake_deflection/empirical_gauss.py +++ /dev/null @@ -1,141 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import cosd, sind - - -@define -class EmpiricalGaussVelocityDeflection(BaseModel): - """ - The Empirical Gauss deflection model is based on the form of previous the - Gauss deflection model (see :cite:`bastankhah2016experimental` and - :cite:`King2019Controls`) but simplifies the formulation for simpler - tuning and more independence from the velocity deficit model. - - parameter_dictionary (dict): Model-specific parameters. - Default values are used when a parameter is not included - in `parameter_dictionary`. Possible key-value pairs include: - - - **horizontal_deflection_gain_D** (*float*): Gain for the - maximum (y-direction) deflection achieved far downstream - of a yawed turbine. - - **vertical_deflection_gain_D** (*float*): Gain for the - maximum vertical (z-direction) deflection achieved at a - far downstream location due to rotor tilt. Specifying as - -1 will mean that vertical deflections due to tilt match - horizontal deflections due to yaw. - - **deflection_rate** (*float*): Rate at which the - deflected wake center approaches its maximum deflection. - - **mixing_gain_deflection** (*float*): Gain to set the - reduction in deflection due to wake-induced mixing. - - **yaw_added_mixing_gain** (*float*): Sets the - contribution of turbine yaw misalignment to the mixing - in that turbine's wake (similar to yaw-added recovery). - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - """ - horizontal_deflection_gain_D: float = field(default=3.0) - vertical_deflection_gain_D: float = field(default=-1) - deflection_rate: float = field(default=22) - mixing_gain_deflection: float = field(default=0.0) - yaw_added_mixing_gain: float = field(default=0.0) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - yaw_i: np.ndarray, - tilt_i: np.ndarray, - mixing_i: np.ndarray, - ct_i: np.ndarray, - rotor_diameter_i: float, - *, - x: np.ndarray, - ): - """ - Calculates the deflection field of the wake. - - Args: - x_i (np.array): Streamwise direction grid coordinates of - the ith turbine (m). - y_i (np.array): Cross stream direction grid coordinates of - the ith turbine (m) [not used]. - yaw_i (np.array): Yaw angle of the ith turbine (deg). - tilt_i (np.array): Tilt angle of the ith turbine (deg). - mixing_i (np.array): The wake-induced mixing term for the - ith turbine. - ct_i (np.array): Thrust coefficient for the ith turbine (-). - rotor_diameter_i (np.array): Rotor diameter for the ith - turbine (m). - - x (np.array): Streamwise direction grid coordinates of the - flow field domain (m). - - Returns: - np.array: Deflection field for the wake. - """ - # ============================================================== - - deflection_gain_y = self.horizontal_deflection_gain_D * rotor_diameter_i - if self.vertical_deflection_gain_D == -1: - deflection_gain_z = deflection_gain_y - else: - deflection_gain_z = self.vertical_deflection_gain_D * rotor_diameter_i - - # Convert to radians, CW yaw for consistency with other models - yaw_r = np.pi/180 * -yaw_i - tilt_r = np.pi/180 * tilt_i - - A_y = (deflection_gain_y * ct_i * yaw_r) / (1 + self.mixing_gain_deflection * mixing_i) - A_z = (deflection_gain_z * ct_i * tilt_r) / (1 + self.mixing_gain_deflection * mixing_i) - - # Apply downstream mask in the process - x_normalized = (x - x_i) * (x > x_i + 0.1) / rotor_diameter_i - - log_term = np.log( - (x_normalized - self.deflection_rate) / (x_normalized + self.deflection_rate) - + 2 - ) - - deflection_y = A_y * log_term - deflection_z = A_z * log_term - - return deflection_y, deflection_z - -def yaw_added_wake_mixing( - axial_induction_i, - yaw_angle_i, - downstream_distance_D_i, - yaw_added_mixing_gain -): - return ( - axial_induction_i[:,:,0,0] - * yaw_added_mixing_gain - * (1 - cosd(yaw_angle_i[:,:,0,0])) - / downstream_distance_D_i**2 - ) diff --git a/floris/core/wake_deflection/jimenez.py b/floris/core/wake_deflection/jimenez.py deleted file mode 100644 index daca6e9c55..0000000000 --- a/floris/core/wake_deflection/jimenez.py +++ /dev/null @@ -1,130 +0,0 @@ - -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import cosd, sind - - -@define -class JimenezVelocityDeflection(BaseModel): - """ - Jiménez wake deflection model, derived from - :cite:`jdm-jimenez2010application`. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: jdm- - """ - - kd: float = field(default=0.05) - ad: float = field(default=0.0) - bd: float = field(default=0.0) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - yaw_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - rotor_diameter_i: np.ndarray, - *, - x: np.ndarray, - ): - """ - Calculates the deflection field of the wake in relation to the yaw of - the turbine. This is coded as defined in [1]. - - Args: - x_locations (np.array): streamwise locations in wake - y_locations (np.array): spanwise locations in wake - z_locations (np.array): vertical locations in wake - (not used in Jiménez) - turbine (:py:class:`floris.core.turbine.Turbine`): - Turbine object - coord - (:py:meth:`floris.core.turbine_map.TurbineMap.coords`): - Spatial coordinates of wind turbine. - flow_field - (:py:class:`floris.core.flow_field.FlowField`): - Flow field object. - - Returns: - deflection (np.array): Deflected wake centerline. - - - This function calculates the deflection of the entire flow field - given the yaw angle and Ct of the current turbine - """ - - # NOTE: Its important to remember the rules of broadcasting here. - # An operation between two np.arrays of different sizes involves - # broadcasting. First, the rank and then the dimensions are compared. - # If the ranks are different, new dimensions of size 1 are added to - # the missing dimensions. Then, arrays can be combined (arithmetic) - # if corresponding dimensions are either the same size or 1. - # https://numpy.org/doc/stable/user/basics.broadcasting.html - # Here, many dimensions are 1, but these are essentially treated - # as a scalar value for that dimension. - - # angle of deflection - xi_init = cosd(yaw_i) * sind(yaw_i) * ct_i / 2.0 - - """ - delta_x = x - x_i - - # yaw displacement - A = 15 * (2 * self.kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0 - B = (30 * self.kd / rotor_diameter_i) - B *= ( 2 * self.kd * delta_x / rotor_diameter_i + 1 ) ** 5.0 - C = xi_init * rotor_diameter_i * (15 + xi_init ** 2.0) - D = 30 * self.kd - - yYaw_init = (xi_init * A / B) - (C / D) - - # corrected yaw displacement with lateral offset - # This has the same shape as the grid - - deflection = yYaw_init + self.ad + self.bd * delta_x - """ - - # Numexpr - do not change below without corresponding changes above. - kd = self.kd - ad = self.ad - bd = self.bd - - delta_x = ne.evaluate("x - x_i") - A = ne.evaluate("15 * (2 * kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0") - B = ne.evaluate("(30 * kd / rotor_diameter_i)") - B = ne.evaluate("B * ( 2 * kd * delta_x / rotor_diameter_i + 1 ) ** 5.0") - C = ne.evaluate("xi_init * rotor_diameter_i * (15 + xi_init ** 2.0)") - D = ne.evaluate("30 * kd") - - yYaw_init = ne.evaluate("(xi_init * A / B) - (C / D)") - deflection = ne.evaluate("yYaw_init + ad + bd * delta_x") - - return deflection diff --git a/floris/core/wake_deflection/none.py b/floris/core/wake_deflection/none.py deleted file mode 100644 index b428c8af9e..0000000000 --- a/floris/core/wake_deflection/none.py +++ /dev/null @@ -1,54 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define - -from floris.core import ( - BaseModel, - FlowField, - Grid, -) - - -@define -class NoneVelocityDeflection(BaseModel): - """ - The None deflection model is a placeholder code that simple ignores any - deflection and returns an array of zeroes. - """ - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "freestream_velocity": flow_field.u_initial_sorted, - } - return kwargs - - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - yaw_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - rotor_diameter_i: float, - *, - freestream_velocity: np.ndarray, - ): - """Skip all deflection calculations and returns zeros array.""" - self.logger.info( - "The wake deflection model is set to 'none'. Deflection modeling disabled." - ) - if np.any(np.abs(yaw_i) > 0.001): - raise ValueError( - "The deflection model is disabled yet not all effective yaw angles are zero. " + - "To resolve this error, please ensure secondary steering is disabled in your " + - "input file and ensure no nonzero yaw angles are passed to the floris object." - ) - - return np.zeros_like(freestream_velocity) diff --git a/floris/core/wake_model/__init__.py b/floris/core/wake_model/__init__.py new file mode 100644 index 0000000000..a44f519fae --- /dev/null +++ b/floris/core/wake_model/__init__.py @@ -0,0 +1,7 @@ +from floris.core.wake_model.base_wake_model import BaseWakeModel +from floris.core.wake_model.cumulative_curl import CumulativeCurl +from floris.core.wake_model.empirical_gauss import EmpiricalGauss +from floris.core.wake_model.gauss import Gauss +from floris.core.wake_model.jensen import JensenJimenez +from floris.core.wake_model.none_model import NoneWake +from floris.core.wake_model.turboparkgauss import TurbOParkGauss diff --git a/floris/core/wake_model/base_wake_model.py b/floris/core/wake_model/base_wake_model.py new file mode 100644 index 0000000000..1856f2fe2b --- /dev/null +++ b/floris/core/wake_model/base_wake_model.py @@ -0,0 +1,178 @@ +import copy +from abc import abstractmethod +from typing import Callable + +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + axial_induction, + BaseLibrary, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + power, + thrust_coefficient, + TurbineGrid, +) + + +@define +class BaseWakeModel(BaseLibrary): # Inherit instead from BaseLibrary + + # Storage + x_i: np.ndarray = field(init=False, default=None) + y_i: np.ndarray = field(init=False, default=None) + z_i: np.ndarray = field(init=False, default=None) + + yaw_angle_i: np.ndarray = field(init=False, default=None) + hub_height_i: np.ndarray = field(init=False, default=None) + rotor_diameter_i: np.ndarray = field(init=False, default=None) + TSR_i: np.ndarray = field(init=False, default=None) + + # Combination model + combination_function: Callable = field(init=False, default=None) + + def set_turbine_i(self, grid, farm, i): + + # Get the current turbine quantities + self.x_i = np.mean(grid.x_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.y_i = np.mean(grid.y_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + self.z_i = np.mean(grid.z_sorted[:, i:i+1], axis=(2, 3), keepdims=True) + + self.yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + self.hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + self.rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + self.TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] + + def assign_combination_function(self, combination_function): + self.combination_function = combination_function + + @abstractmethod + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + raise NotImplementedError( + "The turbine_solve method has not yet been implemented for "+self.__class__.__name__ + ) + + @abstractmethod + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ): + raise NotImplementedError( + "points_solve is not implemented for "+self.__class__.__name__ + ) + + @staticmethod + def evaluate_turbine_axial_induction(grid, farm, flow_field, i: int | None=None): + axial_induction_ = axial_induction( + turbines=farm.turbines, + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i] if i is not None else None, + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + + # Save output onto farm for later post-analysis + if i is None: + farm.turbine_axial_inductions_sorted = axial_induction_ + else: + farm.turbine_axial_inductions_sorted[:, i:i+1] = axial_induction_ + + return axial_induction_[:, :, None, None] + + @staticmethod + def evaluate_turbine_thrust_coefficient(grid, farm, flow_field, i: int | None=None): + thrust_coefficient_ = thrust_coefficient( + turbines=farm.turbines, + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i] if i is not None else None, + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions + ) + + # Save output onto farm for later post-analysis + if i is None: + farm.turbine_thrust_coefficients_sorted = thrust_coefficient_ + else: + farm.turbine_thrust_coefficients_sorted[:, i:i+1] = thrust_coefficient_ + + return thrust_coefficient_[:, :, None, None] + + @staticmethod + def evaluate_turbine_power(grid, farm, flow_field, i: int | None=None): + power_ = power( + turbines=farm.turbines, + velocities=flow_field.u_sorted, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i] if i is not None else None, + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions, + ) + + # Save output onto farm for later post-analysis + if i is None: + farm.turbine_powers_sorted = power_ + else: + farm.turbine_powers_sorted[:, i:i+1] = power_ + + return power_[:, :, None, None] + + @staticmethod + def generate_turbine_grid_objects( + farm: Farm, + flow_field: FlowField, + ): + """Generate turbine grid objects from points grid objects. + Intermediate step of point_solve. + """ + turbine_grid_farm = copy.deepcopy(farm) + turbine_grid_flow_field = copy.deepcopy(flow_field) + + turbine_grid = TurbineGrid( + turbine_coordinates=turbine_grid_farm.coordinates, + turbine_diameters=turbine_grid_farm.rotor_diameters, + wind_directions=turbine_grid_flow_field.wind_directions, + grid_resolution=3, + ) + turbine_grid_farm.set_sorted_indices(turbine_grid.sorted_coord_indices) + turbine_grid_farm.construct_turbine_type_map() + turbine_grid_flow_field.initialize_velocity_field(turbine_grid) + turbine_grid_farm.initialize() + + return turbine_grid_farm, turbine_grid_flow_field, turbine_grid diff --git a/floris/core/wake_model/cumulative_curl.py b/floris/core/wake_model/cumulative_curl.py new file mode 100644 index 0000000000..7711733109 --- /dev/null +++ b/floris/core/wake_model/cumulative_curl.py @@ -0,0 +1,661 @@ +import copy + +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) +from scipy.special import gamma + +from floris.core import ( + axial_induction, + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + thrust_coefficient, + TurbineGrid, +) +from floris.core.rotor_velocity import ( + average_velocity, +) +from floris.core.wake_model import BaseWakeModel +from floris.core.wake_model.gauss import Gauss +from floris.core.wake_model.gch_components import ( + calculate_transverse_velocity, + wake_added_yaw, + yaw_added_turbulence_mixing, +) +from floris.utilities import ( + cosd, + tand, +) + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + + +def wake_expansion( + delta_x, + ct_i, + turbulence_intensity_i, + rotor_diameter, + a_s, + b_s, + c_s1, + c_s2, +): + # Calculate Beta (Eq 10, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) + beta = 0.5 * (1.0 + np.sqrt(1.0 - ct_i)) / np.sqrt(1.0 - ct_i) + k = a_s * turbulence_intensity_i + b_s + eps = (c_s1 * ct_i + c_s2) * np.sqrt(beta) + + # Calculate sigma_tilde (Eq 9, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) + x_tilde = np.abs(delta_x) / rotor_diameter + sigma_y = k * x_tilde + eps + + return sigma_y + + +@define +class CumulativeCurl(BaseWakeModel): + """ + The cumulative curl model is an implementation of the model described in + :cite:`cc-bay_2022`, which itself is based on the cumulative model of + :cite:`cc-bastankhah_2021`. + + References: + .. bibliography:: /references.bib + :style: unsrt + :filter: docname in docnames + :keyprefix: cc- + """ + + # Cumulative Gauss Curl velocity deficit parameters + a_s: float = field(default=0.179367259) + b_s: float = field(default=0.0118889215) + c_s1: float = field(default=0.0563691592) + c_s2: float = field(default=0.13290157) + a_f: float = field(default=3.11) + b_f: float = field(default=-0.68) + c_f: float = field(default=2.41) + alpha_mod: float = field(default=1.0) + + # Gauss deflection model parameters + ad: float = field(converter=float, default=0.0) + bd: float = field(converter=float, default=0.0) + alpha: float = field(converter=float, default=0.58) + beta: float = field(converter=float, default=0.077) + ka: float = field(converter=float, default=0.38) + kb: float = field(converter=float, default=0.004) + dm: float = field(converter=float, default=1.0) + eps_gain: float = field(converter=float, default=0.2) + use_secondary_steering: bool = field(converter=bool, default=True) + + # Borrow deflection model from Gauss class + deflection = Gauss.deflection + + # Crespo-Hernandez turbulence model parameters + initial: float = field(converter=float, default=0.1) + constant: float = field(converter=float, default=0.9) + ai: float = field(converter=float, default=0.8) + downstream: float = field(converter=float, default=-0.32) + + # Secondary effects parameters + enable_secondary_steering: bool = field(converter=bool, default=True) + enable_transverse_velocities: bool = field(converter=bool, default=True) + enable_yaw_added_recovery: bool = field(converter=bool, default=True) + + # Instance variables (not initialized) + effective_yaw_i: np.ndarray = field(init=False, default=None) + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + wind_veer: float = field(init=False, default=None) + freestream_velocity: np.ndarray = field(init=False, default=None) + turb_u_wake: np.ndarray = field(init=False, default=None) + Ctmp: np.ndarray = field(init=False, default=None) + turb_inflow_field: np.ndarray = field(init=False, default=None) + turb_Cts: np.ndarray = field(init=False, default=None) + + def velocity_deficit( + self, + ii: int, + u_i: np.ndarray, + deflection_field: np.ndarray, + turbulence_intensity: np.ndarray, + ct: np.ndarray, + turbine_diameter: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + u_initial: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + """ + Cumulative velocity deficit calculation. Updates and returns turb_u_wake and Ctmp. + """ + turbine_Ct = ct + turbine_ti = turbulence_intensity + turbine_yaw = self.yaw_angle_i + + # Cubic mean velocity at current turbine + turb_avg_vels = np.cbrt(np.mean(u_i ** 3, axis=(2, 3), keepdims=True)) + + delta_x = x - self.x_i + + sigma_n = wake_expansion( + delta_x, + turbine_Ct[:, ii:ii+1], + turbine_ti[:, ii:ii+1], + turbine_diameter[:, ii:ii+1], + self.a_s, + self.b_s, + self.c_s1, + self.c_s2, + ) + + y_i_loc = np.mean(self.y_i, axis=(2, 3), keepdims=True) + z_i_loc = np.mean(self.z_i, axis=(2, 3), keepdims=True) + + x_coord = np.mean(x, axis=(2, 3), keepdims=True) + y_coord = np.mean(y, axis=(2, 3), keepdims=True) + z_coord = np.mean(z, axis=(2, 3), keepdims=True) + + sum_lbda = np.zeros_like(u_initial) + + # Cumulative effects from all upstream turbines + for m in range(0, ii - 1): + x_coord_m = x_coord[:, m:m+1] + y_coord_m = y_coord[:, m:m+1] + z_coord_m = z_coord[:, m:m+1] + + if x_coord[:, m:m+1].size == 0: + break + + delta_x_m = x - x_coord_m + + sigma_i = wake_expansion( + delta_x_m, + turbine_Ct[:, m:m+1], + turbine_ti[:, m:m+1], + turbine_diameter[:, m:m+1], + self.a_s, + self.b_s, + self.c_s1, + self.c_s2, + ) + + S_i = sigma_n ** 2 + sigma_i ** 2 + + Y_i = (y_i_loc - y_coord_m - deflection_field) ** 2 / (2 * S_i) + Z_i = (z_i_loc - z_coord_m) ** 2 / (2 * S_i) + + lbda = 1.0 * sigma_i ** 2 / S_i * np.exp(-Y_i) * np.exp(-Z_i) + + sum_lbda = sum_lbda + lbda * (self.Ctmp[m] / u_initial) + + # Super-Gaussian velocity deficit (Blondel model with cumulative effects) + x_tilde = np.abs(delta_x) / turbine_diameter[:, ii:ii+1] + r_tilde = np.sqrt( + (y - y_i_loc - deflection_field) ** 2 + (z - z_i_loc) ** 2 + ) + r_tilde /= turbine_diameter[:, ii:ii+1] + + n = self.a_f * np.exp(self.b_f * x_tilde) + self.c_f + a1 = 2 ** (2 / n - 1) + a2 = 2 ** (4 / n - 2) + + # Blondel model with cumulative effects + tmp = a2 - ( + (n * turbine_Ct[:, ii:ii+1]) + * cosd(turbine_yaw) + / ( + 16.0 + * gamma(2 / n) + * np.sign(sigma_n) + * (np.abs(sigma_n) ** (4 / n)) + * (1 - sum_lbda) ** 2 + ) + ) + + # Replace negative values with zeros to prevent NaNs + tmp = tmp * (tmp >= 0) + + C = a1 - np.sqrt(tmp) + C = C * (1 - sum_lbda) + + self.Ctmp[ii] = C + + yR = y - y_i_loc + xR = yR * tand(turbine_yaw) + self.x_i + + # Velocity deficit + velDef = C * np.exp((-1 * r_tilde ** n) / (2 * sigma_n ** 2)) + velDef = velDef * (x - xR >= 0.1) + + self.turb_u_wake = self.turb_u_wake + turb_avg_vels * velDef + return (self.turb_u_wake, self.Ctmp) + + def turbulence( + self, + turbulence_intensity: np.ndarray, + x: np.ndarray, + y: np.ndarray, + axial_induction: np.ndarray, + area_overlap: np.ndarray, + ) -> np.ndarray: + """ + Crespo-Hernandez turbulence model. + """ + x_i = self.x_i + rotor_diameter_i = self.rotor_diameter_i + delta_x = x - x_i + ambient_TI = self.ambient_turbulence_intensities + + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 + + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask + + # Crespo et al. turbulence intensity calculation + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter_i) ** downstream" + ) + wake_added_turbulence_intensity = ti * downstream_mask + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (x > self.x_i) + * (np.abs(self.y_i - y) < 2 * self.rotor_diameter_i) + * (x <= downstream_influence_length + self.x_i) + ) + + # Combine turbine TIs with WAT + turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + ambient_TI**2), turbulence_intensity + ) + + return turbulence_intensity + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + """ + Solve for turbines using the cumulative curl model. + """ + # Check not assigned a working combination_function + if self.combination_function(0,0) is not None: + self.logger.warning( + "CumulativeCurl model does not use a combination model. " + "Suggest setting combination_model to `none`." + ) + + # Initialize wake state + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + self.turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) + self.turb_inflow_field = copy.deepcopy(flow_field.u_initial_sorted) + + # Set up turbulence arrays + turbine_turbulence_intensity = flow_field.turbulence_intensities[:, None, None, None] + turbine_turbulence_intensity = np.repeat( + turbine_turbulence_intensity, farm.n_turbines, axis=1 + ) + + # Ambient turbulent intensity + self.ambient_turbulence_intensities = ( + flow_field.turbulence_intensities.copy() + [:, None, None, None] + ) + + # Initialize state arrays for cumulative calculation + shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) + self.Ctmp = np.zeros((shape)) + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + self.wind_veer = flow_field.wind_veer + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(grid, farm, i) + + # Compute rotor-vicinity mask for inflow field update + rotor_diameter_i = farm.rotor_diameters_sorted[:, i:i+1, None, None] + mask2 = ( + (grid.x_sorted < self.x_i + 0.01) + * (grid.x_sorted > self.x_i - 0.01) + * (grid.y_sorted < self.y_i + 0.51 * rotor_diameter_i) + * (grid.y_sorted > self.y_i - 0.51 * rotor_diameter_i) + ) + self.turb_inflow_field = ( + self.turb_inflow_field * ~mask2 + + (flow_field.u_initial_sorted - self.turb_u_wake) * mask2 + ) + + # Compute thrust coefficients for all turbines using turbine inflow field + turb_avg_vels = average_velocity(self.turb_inflow_field)[:, :, None, None] + self.turb_Cts = thrust_coefficient( + turbines=farm.turbines, + velocities=turb_avg_vels, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions, + ) + self.turb_Cts = self.turb_Cts[:, :, None, None] + + # Compute axial induction for current turbine (uses turb_avg_vels) + # TODO: different velocities from call below? + aIs_i_avgvel = axial_induction( + turbines=farm.turbines, + velocities=turb_avg_vels, + turbulence_intensities=flow_field.turbulence_intensity_field_sorted, + air_density=flow_field.air_density, + yaw_angles=farm.yaw_angles_sorted, + power_setpoints=farm.power_setpoints_sorted, + awc_modes=farm.awc_modes_sorted, + awc_amplitudes=farm.awc_amplitudes_sorted, + turbine_type_map=farm.turbine_type_map_sorted, + ix_filter=[i], + average_method=grid.average_method, + cubature_weights=grid.cubature_weights, + multidim_condition=flow_field.multidim_conditions, + ) + aIs_i_avgvel = aIs_i_avgvel[:, :, None, None] + + u_i = self.turb_inflow_field[:, i:i+1] + v_i = flow_field.v_sorted[:, i:i+1] + + # Axial induction for current turbine (uses flow_field.u_sorted) + axial_induction_i = self.evaluate_turbine_axial_induction(grid, farm, flow_field, i) + + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + yaw_angle_i = farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = farm.hub_heights_sorted[:, i:i+1, None, None] + TSR_i = farm.TSRs_sorted[:, i:i+1, None, None] + + # Initialize effective yaw angle + self.effective_yaw_i = yaw_angle_i.copy() + + if self.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + flow_field.u_initial_sorted, + grid.y_sorted[:, i:i+1] - self.y_i, + grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + hub_height_i, + self.turb_Cts[:, i:i+1], + TSR_i, + axial_induction_i, + flow_field.wind_shear, + scale=2.0, + ) + self.effective_yaw_i += added_yaw + + # Compute deflection + deflection_field = self.deflection( + turbulence_intensity_i, + self.turb_Cts[:, i:i+1], + grid.x_sorted, + ) + + if self.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, + self.rotor_diameter_i, + hub_height_i, + yaw_angle_i, + self.turb_Cts[:, i:i+1], + TSR_i, + axial_induction_i, + flow_field.wind_shear, + scale=2.0, + ) + + if self.enable_yaw_added_recovery: + I_mixing = yaw_added_turbulence_mixing( + u_i, + turbulence_intensity_i, + v_i, + flow_field.w_sorted[:, i:i+1], + v_wake[:, i:i+1], + w_wake[:, i:i+1], + ) + gch_gain = 1.0 + turbine_turbulence_intensity[:, i:i+1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) + + # Compute velocity deficit (cumulative) + self.turb_u_wake, self.Ctmp = self.velocity_deficit( + i, + u_i, + deflection_field, + turbine_turbulence_intensity, + self.turb_Cts, + farm.rotor_diameters_sorted[:, :, None, None], + grid.x_sorted, + grid.y_sorted, + grid.z_sorted, + flow_field.u_initial_sorted, + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = 1 - ( + np.sum(self.turb_u_wake <= 0.05, axis=(2, 3), keepdims=True) + / (grid.grid_resolution * grid.grid_resolution) + ) + + # Compute wake-added turbulence with area overlap + wake_added_turbulence_intensity = self.turbulence( + self.ambient_turbulence_intensities, + grid.x_sorted, + grid.y_sorted, + aIs_i_avgvel, + area_overlap, + ) + + # Combine turbine TIs with WAT + turbine_turbulence_intensity = np.maximum( + wake_added_turbulence_intensity, + turbine_turbulence_intensity + ) + + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + flow_field.u_sorted = self.turb_inflow_field + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(2, 3), + keepdims=True + ) + + # Compute turbine powers based on final flow field + self.evaluate_turbine_power(grid, farm, flow_field) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + """ + Solve for a general point grid using the cumulative curl model. + Mimics full_flow_cc_solver from solver.py. + """ + # Get the flow quantities and turbine performance on turbine grid + turbine_grid_farm = copy.deepcopy(farm) + turbine_grid_flow_field = copy.deepcopy(flow_field) + + turbine_grid = TurbineGrid( + turbine_coordinates=turbine_grid_farm.coordinates, + turbine_diameters=turbine_grid_farm.rotor_diameters, + wind_directions=turbine_grid_flow_field.wind_directions, + grid_resolution=3, + ) + turbine_grid_farm.set_sorted_indices(turbine_grid.sorted_coord_indices) + turbine_grid_farm.construct_turbine_type_map() + turbine_grid_flow_field.initialize_velocity_field(turbine_grid) + turbine_grid_farm.initialize() + + # Run turbine solve to populate state + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + # Now compute wake field on the full grid + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + turb_u_wake = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Extract freestream velocity for deficit, deflection calculations + self.freestream_velocity = flow_field.u_initial_sorted + + shape = (farm.n_turbines,) + np.shape(flow_field.u_initial_sorted) + Ctmp = np.zeros((shape)) + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Set self.Ctmp and self.turb_u_wake for this iteration + # (point_solve uses local versions for full grid) + self.Ctmp = Ctmp + self.turb_u_wake = turb_u_wake + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + + u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] + v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] + + # Use saved turb_Cts from turbine_solve + turb_Cts_i = self.turb_Cts + + # Axial induction + axial_induction_i = self.evaluate_turbine_axial_induction( + turbine_grid, turbine_grid_farm, turbine_grid_flow_field, i + ) + + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + yaw_angle_i = turbine_grid_farm.yaw_angles_sorted[:, i:i+1, None, None] + hub_height_i = turbine_grid_farm.hub_heights_sorted[:, i:i+1, None, None] + TSR_i = turbine_grid_farm.TSRs_sorted[:, i:i+1, None, None] + + self.effective_yaw_i = yaw_angle_i.copy() + + if self.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + turbine_grid_flow_field.u_initial_sorted, + turbine_grid.y_sorted[:, i:i+1] - self.y_i, + turbine_grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + hub_height_i, + turb_Cts_i[:, i:i+1], + TSR_i, + axial_induction_i, + flow_field.wind_shear, + scale=2.0, + ) + self.effective_yaw_i += added_yaw + + # Model calculations + deflection_field = self.deflection( + turbulence_intensity_i, + turb_Cts_i[:, i:i+1], + grid.x_sorted, + ) + + if self.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, + self.rotor_diameter_i, + hub_height_i, + yaw_angle_i, + turb_Cts_i[:, i:i+1], + TSR_i, + axial_induction_i, + flow_field.wind_shear, + scale=2.0, + ) + + # Velocity deficit (cumulative) + turb_u_wake, Ctmp = self.velocity_deficit( + i, + u_i, + deflection_field, + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg, + turb_Cts_i, + turbine_grid_farm.rotor_diameters_sorted[:, :, None, None], + grid.x_sorted, + grid.y_sorted, + grid.z_sorted, + flow_field.u_initial_sorted, + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = np.where(turb_u_wake > 0.05, 1, 0) + + # Compute wake-added turbulence with area overlap + wake_added_turbulence_intensity = self.turbulence( + ambient_turbulence_intensities, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + area_overlap, + ) + + # Combine turbine TIs with WAT + turbulence_intensity_field = np.maximum( + wake_added_turbulence_intensity, + turbulence_intensity_field + ) + + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + flow_field.u_sorted = flow_field.u_initial_sorted - turb_u_wake + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field diff --git a/floris/core/wake_model/empirical_gauss.py b/floris/core/wake_model/empirical_gauss.py new file mode 100644 index 0000000000..efc45bfa8e --- /dev/null +++ b/floris/core/wake_model/empirical_gauss.py @@ -0,0 +1,519 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.rotor_velocity import ( + average_velocity, + calculate_tilt_for_rotor_effective_velocities, +) +from floris.core.wake_model import BaseWakeModel +from floris.core.wake_model.gauss import gaussian_function +from floris.type_dec import floris_float_type +from floris.utilities import cosd + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class EmpiricalGauss(BaseWakeModel): + + # Deficit model parameters + wake_expansion_rates: list = field(factory=lambda: [0.023, 0.008]) + breakpoints_D: list = field(factory=lambda: [10]) + sigma_0_D: float = field(default=0.28) + smoothing_length_D: float = field(default=2.0) + mixing_gain_velocity: float = field(default=2.0) + awc_mode: str = field(default="baseline") + awc_wake_exp: float = field(default=1.2) + awc_wake_denominator: float = field(default=400) + include_mirror_wake: bool = field(default=True) + + # Deflection model parameters + horizontal_deflection_gain_D: float = field(default=3.0) + vertical_deflection_gain_D: float = field(default=-1) + deflection_rate: float = field(default=22) + mixing_gain_deflection: float = field(default=0.0) + yaw_added_mixing_gain: float = field(default=0.0) + + # Mixing model parameters + atmospheric_ti_gain: float = field(converter=float, default=0.0) + enable_yaw_added_recovery: bool = field(default=True) + enable_active_wake_mixing: bool = field(default=True) + + tilt_angle_i: np.ndarray = field(init=False, default=None) + + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + wind_veer: float = field(init=False, default=None) + freestream_velocity: np.ndarray = field(init=False, default=None) + mixing_factor: np.ndarray = field(init=False, default=None) + + def velocity_deficit( + self, + deflection_field_y_i: np.ndarray, + deflection_field_z_i: np.ndarray, + mixing_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # Only symmetric terms using yaw, but keep for consistency + yaw_angle = -1 * self.yaw_angle_i + + # Initial wake widths + sigma_y0 = self.sigma_0_D * self.rotor_diameter_i * cosd(yaw_angle) + sigma_z0 = self.sigma_0_D * self.rotor_diameter_i * cosd(self.tilt_angle_i) + + # No specific near, far wakes in this model + downstream_mask = (x > self.x_i + 0.1) + upstream_mask = (x < self.x_i - 0.1) + + # Wake expansion in the lateral (y) and the vertical (z) + # TODO: could compute shared components in sigma_z, sigma_y + # with one function call. + sigma_y = empirical_gauss_model_wake_width( + x - self.x_i, + self.wake_expansion_rates, + [b * self.rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] + sigma_y0, + self.smoothing_length_D * self.rotor_diameter_i, + self.mixing_gain_velocity * mixing_i, + ) + sigma_y[upstream_mask] = \ + np.tile(sigma_y0, np.shape(sigma_y)[1:])[upstream_mask] + + sigma_z = empirical_gauss_model_wake_width( + x - self.x_i, + self.wake_expansion_rates, + [b * self.rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] + sigma_z0, + self.smoothing_length_D * self.rotor_diameter_i, + self.mixing_gain_velocity * mixing_i, + ) + sigma_z[upstream_mask] = \ + np.tile(sigma_z0, np.shape(sigma_z)[1:])[upstream_mask] + + # 'Standard' wake component + r, C = rCalt( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_y_i, + deflection_field_z_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.tilt_angle_i, + self.rotor_diameter_i, + sigma_y0, + sigma_z0 + ) + # Normalize to match end of actuator disk model tube + C = C / (8 * self.sigma_0_D**2 ) + + wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) + + if self.include_mirror_wake: + # TODO: speed up this option by calculating various elements in + # rCalt only once. + # Mirror component + r_mirr, C_mirr = rCalt( + self.wind_veer, # TODO: Is veer OK with mirror wakes? + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_y_i, + deflection_field_z_i, + z, + -self.hub_height_i, # Turbine at negative hub height location + ct_i, + yaw_angle, + self.tilt_angle_i, + self.rotor_diameter_i, + sigma_y0, + sigma_z0 + ) + # Normalize to match end of actuator disk model tube + C_mirr = C_mirr / (8 * self.sigma_0_D**2) + + # ASSUME sum-of-squares superposition for the real and mirror wakes + wake_deficit = np.sqrt( + wake_deficit**2 + + gaussian_function(C_mirr, r_mirr, 1, np.sqrt(0.5))**2 + ) + + velocity_deficit = wake_deficit * downstream_mask + + return velocity_deficit + + def deflection( + self, + mixing_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + + deflection_gain_y = self.horizontal_deflection_gain_D * self.rotor_diameter_i + if self.vertical_deflection_gain_D == -1: + deflection_gain_z = deflection_gain_y + else: + deflection_gain_z = self.vertical_deflection_gain_D * self.rotor_diameter_i + + # Convert to radians, CW yaw for consistency with other models + yaw_r = np.pi/180 * -self.yaw_angle_i + tilt_r = np.pi/180 * self.tilt_angle_i + + A_y = (deflection_gain_y * ct_i * yaw_r) / (1 + self.mixing_gain_deflection * mixing_i) + A_z = (deflection_gain_z * ct_i * tilt_r) / (1 + self.mixing_gain_deflection * mixing_i) + + # Apply downstream mask in the process + x_normalized = (x - self.x_i) * (x > self.x_i + 0.1) / self.rotor_diameter_i + + log_term = np.log( + (x_normalized - self.deflection_rate) / (x_normalized + self.deflection_rate) + + 2 + ) + + deflection_y = A_y * log_term + deflection_z = A_z * log_term + + return deflection_y, deflection_z + + def mixing( + self, + axial_induction_i: np.ndarray, + downstream_distance_D_i: np.ndarray, + ) -> np.ndarray: + """ + Calculates the contribution of turbine i to all other turbines' + mixing terms. + + Args: + axial_induction_i (np.array): Axial induction factor of + the ith turbine (-). + downstream_distance_D_i (np.array): The distance downstream + from turbine i to all other turbines (specified in terms + of multiples of turbine i's rotor diameter) (D). + + Returns: + np.array: Components of the wake-induced mixing term due to + the ith turbine. + """ + + wake_induced_mixing = axial_induction_i[:,:,0,0] / downstream_distance_D_i**2 + + return wake_induced_mixing + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize mixing factor information + x_locs = np.mean(grid.x_sorted, axis=(2, 3))[:,:,None] + downstream_distance_D = x_locs - np.transpose(x_locs, axes=(0,2,1)) + downstream_distance_D = downstream_distance_D / \ + np.repeat(farm.rotor_diameters_sorted[:,:,None], grid.n_turbines, axis=-1) + downstream_distance_D = np.maximum(downstream_distance_D, 0.1) # For ease + # Initialize the mixing factor model using TI if specified + initial_mixing_factor = self.atmospheric_ti_gain * np.eye(grid.n_turbines) + mixing_factor = np.repeat( + initial_mixing_factor[None, :, :], + flow_field.n_findex, + axis=0 + ) + mixing_factor = mixing_factor * flow_field.turbulence_intensities[:, None, None] + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + self.wind_veer = flow_field.wind_veer + + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + grid, farm, flow_field, i + ) + axial_induction_i = self.evaluate_turbine_axial_induction(grid, farm, flow_field, i) + + # Compute the tilt angle of the ith turbine + average_velocities = average_velocity( + flow_field.u_sorted, + method=grid.average_method, + cubature_weights=grid.cubature_weights + ) + self.tilt_angle_i = calculate_tilt_for_rotor_effective_velocities( + farm, average_velocities + )[:, i:i+1, None, None] + + if self.enable_yaw_added_recovery: + # Influence of yawing on turbine's own wake + mixing_factor[:, i:i+1, i] += \ + yaw_added_wake_mixing( + axial_induction_i, self.yaw_angle_i, 1, self.yaw_added_mixing_gain + ) + if self.enable_active_wake_mixing: + # Influence of awc on turbine's own wake + mixing_factor[:, i:i+1, i] += \ + awc_added_wake_mixing( + farm.awc_modes_sorted[:, i:i+1, None, None], + farm.awc_amplitudes_sorted[:, i:i+1, None, None], + farm.awc_frequencies_sorted[:, i:i+1, None, None], + self.awc_wake_exp, + self.awc_wake_denominator + ) + + # Extract total wake induced mixing for turbine i + mixing_i = np.linalg.norm( + mixing_factor[:, i:i+1, :, None], + ord=2, axis=2, keepdims=True + ) + + # Primary model calculations + deflection_field_y, deflection_field_z = self.deflection( + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + deflection_field_y, + deflection_field_z, + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = np.sum( + velocity_deficit * flow_field.u_initial_sorted > 0.05, + axis=(2, 3) + ) / (grid.grid_resolution * grid.grid_resolution) + + # Compute wake induced mixing factor + mixing_factor[:,:,i] += area_overlap * self.mixing( + axial_induction_i, downstream_distance_D[:,:,i] + ) + + if self.enable_yaw_added_recovery: + mixing_factor[:,:,i] += \ + area_overlap * yaw_added_wake_mixing( + axial_induction_i, + self.yaw_angle_i, + downstream_distance_D[:,:,i], + self.yaw_added_mixing_gain + ) + + # Remove wakes from flow field + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + # Store for use in point_solve + self.mixing_factor = mixing_factor + + # Compute turbine powers based on final flow field + self.evaluate_turbine_power(grid, farm, flow_field) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Extract freestream velocity for deficit, deflection calculations + self.freestream_velocity = flow_field.u_initial_sorted + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + + # Get mixing_i based on turbine_solve results + mixing_i = self.mixing_factor[:, i:i+1, :, None].sum(axis=2, keepdims=1) + + average_velocities = average_velocity( + turbine_grid_flow_field.u_sorted, + method=turbine_grid.average_method, + cubature_weights=turbine_grid.cubature_weights + ) + # Check: should self.tilt_angle_i be updated? Could it just be saved? + self.tilt_angle_i = calculate_tilt_for_rotor_effective_velocities( + turbine_grid_farm, + average_velocities + )[:, i:i+1, None, None] + + # Model calculations + deflection_field_y, deflection_field_z = self.deflection( + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + deflection_field_y, + deflection_field_z, + mixing_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field + + +# @profile +def rCalt(wind_veer, sigma_y, sigma_z, y, y_i, delta_y, delta_z, z, HH, Ct, + yaw, tilt, D, sigma_y0, sigma_z0): + + ## Numexpr + wind_veer = np.deg2rad(wind_veer) + a = ne.evaluate( + "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + b = ne.evaluate( + "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" + ) + c = ne.evaluate( + "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + r = ne.evaluate( + "a * ( (y - y_i - delta_y) ** 2) - "+\ + "2 * b * (y - y_i - delta_y) * (z - HH - delta_z) + "+\ + "c * ((z - HH - delta_z) ** 2)" + ) + d = 1 - Ct * (sigma_y0 * sigma_z0)/(sigma_y * sigma_z) * cosd(yaw) * cosd(tilt) + C = ne.evaluate("1 - sqrt(d)") + return r, C + +def sigmoid_integral(x, center=0, width=1): + y = np.zeros_like(x) + # TODO: Can this be made faster? + above_smoothing_zone = (x-center) > width/2 + y[above_smoothing_zone] = (x-center)[above_smoothing_zone] + in_smoothing_zone = ((x-center) >= -width/2) & ((x-center) <= width/2) + z = ((x-center)/width + 0.5)[in_smoothing_zone] + if width.shape[0] > 1: # multiple turbine sizes + width = np.broadcast_to(width, x.shape)[in_smoothing_zone] + y[in_smoothing_zone] = (width*(z**6 - 3*z**5 + 5/2*z**4)).flatten() + return y + +def empirical_gauss_model_wake_width( + x, + wake_expansion_rates, + breakpoints, + sigma_0, + smoothing_length, + mixing_final, + ): + assert len(wake_expansion_rates) == len(breakpoints) + 1, \ + "Invalid combination of wake_expansion_rates and breakpoints." + + sigma = (wake_expansion_rates[0] + mixing_final) * x + sigma_0 + for ib, b in enumerate(breakpoints): + sigma += (wake_expansion_rates[ib+1] - wake_expansion_rates[ib]) * \ + sigmoid_integral(x, center=b, width=smoothing_length) + + return sigma + +def awc_added_wake_mixing( + awc_mode_i, + awc_amplitude_i, + awc_frequency_i, + awc_wake_exp, + awc_wake_denominator +): + # Drop surplus (grid) dimensions + awc_amplitude_i = awc_amplitude_i[:,:,0,0] + awc_mode_i = awc_mode_i[:,:,0,0] + + # TODO: Add TI in the mix, finetune amplitude/freq effect + awc_mixing_factor = np.zeros_like(awc_amplitude_i, dtype=floris_float_type) + helix_mask = awc_mode_i == 'helix' + + awc_mixing_factor[helix_mask] = ( + awc_amplitude_i[helix_mask]**awc_wake_exp/awc_wake_denominator + ) + + return awc_mixing_factor + +def yaw_added_wake_mixing( + axial_induction_i, + yaw_angle_i, + downstream_distance_D_i, + yaw_added_mixing_gain +): + return ( + axial_induction_i[:,:,0,0] + * yaw_added_mixing_gain + * (1 - cosd(yaw_angle_i[:,:,0,0])) + / downstream_distance_D_i**2 + ) diff --git a/floris/core/wake_model/gauss.py b/floris/core/wake_model/gauss.py new file mode 100644 index 0000000000..663c3f8c3c --- /dev/null +++ b/floris/core/wake_model/gauss.py @@ -0,0 +1,657 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.core.wake_model.gch_components import ( + calculate_transverse_velocity, + wake_added_yaw, + yaw_added_turbulence_mixing, +) +from floris.utilities import cosd + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class Gauss(BaseWakeModel): + + # Gauss deficit model parameters + alpha: float = field(default=0.58) + beta: float = field(default=0.077) + ka: float = field(default=0.38) + kb: float = field(default=0.004) + + # Gauss deflection model parameters + ad: float = field(converter=float, default=0.0) + bd: float = field(converter=float, default=0.0) + dm: float = field(converter=float, default=1.0) + eps_gain: float = field(converter=float, default=0.2) + use_secondary_steering: bool = field(converter=bool, default=True) + + # Crespo-Hernandez turbulence model parameters + initial: float = field(converter=float, default=0.1) + constant: float = field(converter=float, default=0.9) + ai: float = field(converter=float, default=0.8) + downstream: float = field(converter=float, default=-0.32) + + # Secondary effects parameters (GCH) + enable_transverse_velocities: bool = field(converter=bool, default=True) + enable_yaw_added_recovery: bool = field(converter=bool, default=True) + enable_secondary_steering: bool = field(converter=bool, default=True) + + effective_yaw_i: np.ndarray = field(init=False, default=None) + + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + wind_veer: float = field(init=False, default=None) + freestream_velocity: np.ndarray = field(init=False, default=None) + + def velocity_deficit( + self, + axial_induction_i: np.ndarray, + deflection_field_i: np.ndarray, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # yaw_angle is all turbine yaw angles for each wind speed + # Extract and broadcast only the current turbine yaw setting + # for all wind speeds + + # Opposite sign convention in this model + yaw_angle = -1 * self.yaw_angle_i + + # Initialize the velocity deficit + uR = self.freestream_velocity * ct_i / (2.0 * (1 - np.sqrt(1 - ct_i))) + u0 = self.freestream_velocity * np.sqrt(1 - ct_i) + + # Initial lateral bounds + sigma_z0 = self.rotor_diameter_i * 0.5 * np.sqrt(uR / (self.freestream_velocity + u0)) + sigma_y0 = sigma_z0 * cosd(yaw_angle) * cosd(self.wind_veer) + + # Compute the bounds of the near and far wake regions and a mask + + # Start of the near wake + xR = self.x_i + + # Start of the far wake + x0 = np.ones_like(self.freestream_velocity) + x0 *= self.rotor_diameter_i * cosd(yaw_angle) * (1 + np.sqrt(1 - ct_i) ) + x0 /= np.sqrt(2) * ( + 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i) ) + ) + x0 += self.x_i + + # Initialize the velocity deficit array + velocity_deficit = np.zeros_like(self.freestream_velocity) + + # Masks + # When we have only an inequality, the current turbine may be applied its own + # wake in cases where numerical precision cause in incorrect comparison. We've + # applied a small bump to avoid this. "0.1" is arbitrary but it is a small, non + # zero value. + + # This mask defines the near wake; keeps the areas downstream of xR and upstream of x0 + near_wake_mask = (x > xR + 0.1) * (x < x0) + far_wake_mask = (x >= x0) + + # Compute the velocity deficit in the NEAR WAKE region + # ONLY If there are points within the near wake boundary + # TODO: for the TurbineGrid, do we need to do this near wake calculation at all? + # same question for any grid with a resolution larger than the near wake region + if np.sum(near_wake_mask): + + # Calculate the wake expansion + + # This is a linear ramp from 0 to 1 from the start of the near wake to the start + # of the far wake. + near_wake_ramp_up = (x - xR) / (x0 - xR) + # Another linear ramp, but positive upstream of the far wake and negative in the + # far wake; 0 at the start of the far wake + near_wake_ramp_down = (x0 - x) / (x0 - xR) + # near_wake_ramp_down = -1 * (near_wake_ramp_up - 1) # : this is equivalent, right? + + sigma_y = near_wake_ramp_down * 0.501 * self.rotor_diameter_i * np.sqrt(ct_i / 2.0) + sigma_y += near_wake_ramp_up * sigma_y0 + sigma_y *= (x >= xR) + sigma_y += np.ones_like(sigma_y) * (x < xR) * 0.5 * self.rotor_diameter_i + + sigma_z = near_wake_ramp_down * 0.501 * self.rotor_diameter_i * np.sqrt(ct_i / 2.0) + sigma_z += near_wake_ramp_up * sigma_z0 + sigma_z *= (x >= xR) + sigma_z += np.ones_like(sigma_z) * (x < xR) * 0.5 * self.rotor_diameter_i + + r_squared, C = rC( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.rotor_diameter_i, + ) + + near_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) + near_wake_deficit *= near_wake_mask + + velocity_deficit += near_wake_deficit + + # Compute the velocity deficit in the FAR WAKE region + if np.sum(far_wake_mask): + + # Wake expansion in the lateral (y) and the vertical (z) + ky = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters + kz = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters + sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * (x < x0) + sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * (x < x0) + + r_squared, C = rC( + self.wind_veer, + sigma_y, + sigma_z, + y, + self.y_i, + deflection_field_i, + z, + self.hub_height_i, + ct_i, + yaw_angle, + self.rotor_diameter_i, + ) + + far_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) + far_wake_deficit *= far_wake_mask + + velocity_deficit += far_wake_deficit + + return velocity_deficit + + def deflection( + self, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> np.ndarray: + """ + Calculates the deflection field of the wake. See + :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` + for details on the methods used. + + Args: + x_i (np.array): x-coordinates of turbine i. + y_i (np.array): y-coordinates of turbine i. + yaw_i (np.array): Yaw angle of turbine i. + turbulence_intensity_i (np.array): Turbulence intensity at turbine i. + ct_i (np.array): Thrust coefficient of turbine i. + rotor_diameter_i (float): Rotor diameter of turbine i. + + Returns: + np.array: Deflection field for the wake. + """ + # ============================================================== + + # Opposite sign convention in this model + yaw_i = -1 * self.effective_yaw_i + + # TODO: connect support for tilt + tilt = 0.0 # turbine.tilt_angle + + # initial velocity deficits + uR = ( + self.freestream_velocity + * ct_i + * cosd(tilt) + * cosd(yaw_i) + / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) + ) + u0 = self.freestream_velocity * np.sqrt(1 - ct_i) + + # length of near wake + x0 = ( + self.rotor_diameter_i + * (cosd(yaw_i) * (1 + np.sqrt(1 - ct_i * cosd(yaw_i)))) + / (np.sqrt(2) * ( + 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i)) + )) + self.x_i + ) + + # wake expansion parameters + ky = self.ka * turbulence_intensity_i + self.kb + kz = self.ka * turbulence_intensity_i + self.kb + + C0 = 1 - u0 / self.freestream_velocity + M0 = C0 * (2 - C0) + E0 = ne.evaluate("C0 ** 2 - 3 * exp(1.0 / 12.0) * C0 + 3 * exp(1.0 / 3.0)") + + # initial Gaussian wake expansion + freestream_velocity = self.freestream_velocity # Extract for numexpr + rotor_diameter_i = self.rotor_diameter_i # Extract for numexpr + sigma_z0 = ne.evaluate("rotor_diameter_i * 0.5 * sqrt(uR / (freestream_velocity + u0))") + sigma_y0 = sigma_z0 * cosd(yaw_i) * cosd(self.wind_veer) + + # yR = y - y_i + xR = self.x_i # yR * tand(yaw) + x_i + + # yaw parameters (skew angle and distance from centerline) + # skew angle in radians + theta_c0 = self.dm * (0.3 * np.radians(yaw_i) / cosd(yaw_i)) + theta_c0 *= (1 - np.sqrt(1 - ct_i * cosd(yaw_i))) + delta0 = np.tan(theta_c0) * (x0 - self.x_i) # initial wake deflection; + # NOTE: use np.tan here since theta_c0 is radians + + # deflection in the near wake + delta_near_wake = ((x - xR) / (x0 - xR)) * delta0 + (self.ad + self.bd * (x - self.x_i)) + delta_near_wake *= (x >= xR) & (x <= x0) + + # deflection in the far wake + sigma_y = ky * (x - x0) + sigma_y0 + sigma_z = kz * (x - x0) + sigma_z0 + sigma_y = sigma_y * (x >= x0) + sigma_y0 * (x < x0) + sigma_z = sigma_z * (x >= x0) + sigma_z0 * (x < x0) + + M0_sqrt = np.sqrt(M0) + middle_term = np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) + ln_deltaNum = (1.6 + M0_sqrt) * (1.6 * middle_term - M0_sqrt) + ln_deltaDen = (1.6 - M0_sqrt) * (1.6 * middle_term + M0_sqrt) + + middle_term = ne.evaluate( + "theta_c0" + " * E0" + " / 5.2" + " * sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0))" + " * log(ln_deltaNum / ln_deltaDen)" + ) + delta_far_wake = delta0 + middle_term + (self.ad + self.bd * (x - self.x_i)) + + delta_far_wake = delta_far_wake * (x > x0) + deflection = delta_near_wake + delta_far_wake + + return deflection + + def turbulence( + self, + turbulence_intensity: np.ndarray, + x: np.ndarray, + y: np.ndarray, + axial_induction: np.ndarray, + area_overlap: np.ndarray, + ) -> np.ndarray: + # Replace zeros and negatives with 1 to prevent nans/infs + x_i = self.x_i + rotor_diameter_i = self.rotor_diameter_i + delta_x = x - x_i + ambient_TI = self.ambient_turbulence_intensities + + # TODO: ensure that these fudge factors are needed for different rotations + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 + + # Keep downstream components Set upstream to 1.0 + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask + + # turbulence intensity calculation based on Crespo et. al. + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter_i) ** downstream" + ) + # Mask the 1 values from above with zeros + wake_added_turbulence_intensity = ti * downstream_mask + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (x > self.x_i) + * (np.abs(self.y_i - y) < 2 * self.rotor_diameter_i) + * (x <= downstream_influence_length + self.x_i) + ) + # Combine turbine TIs with WAT + turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + ambient_TI**2), turbulence_intensity + ) + + return turbulence_intensity + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + turbine_turbulence_intensity = np.repeat( + flow_field.turbulence_intensities[:, None, None, None], + farm.n_turbines, + axis=1 + ) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + self.wind_veer = flow_field.wind_veer + + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + grid, farm, flow_field, i + ) + axial_induction_i = self.evaluate_turbine_axial_induction(grid, farm, flow_field, i) + u_i = flow_field.u_sorted[:, i:i+1] + v_i = flow_field.v_sorted[:, i:i+1] + turbulence_intensity_i = turbine_turbulence_intensity[:, i:i+1] + + # Initialize the effective yaw angle + self.effective_yaw_i = self.yaw_angle_i.copy() + + # Model calculations + if self.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + flow_field.u_initial_sorted, + grid.y_sorted[:, i:i+1] - self.y_i, + grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + self.hub_height_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + self.effective_yaw_i += added_yaw + + deflection_field = self.deflection( + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + ) + + if self.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, + self.rotor_diameter_i, + self.hub_height_i, + self.yaw_angle_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + else: + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + + if self.enable_yaw_added_recovery: + I_mixing = yaw_added_turbulence_mixing( + u_i, + turbulence_intensity_i, + v_i, + flow_field.w_sorted[:, i:i+1], + v_wake[:, i:i+1], + w_wake[:, i:i+1], + ) + gch_gain = 2 + turbine_turbulence_intensity[:, i:i+1] = ( + turbulence_intensity_i + gch_gain * I_mixing + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) + area_overlap = area_overlap[:, :, None, None] + + turbine_turbulence_intensity = self.turbulence( + turbine_turbulence_intensity, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + area_overlap, + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + # Add the final turbine turbulence intensity field to the flow field object + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(2,3), + keepdims=True + ) + + # Compute turbine powers based on final flow field + self.evaluate_turbine_power(grid, farm, flow_field) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Extract freestream velocity for deficit, deflection calculations + self.freestream_velocity = flow_field.u_initial_sorted + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + axial_induction_i = self.evaluate_turbine_axial_induction( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + u_i = turbine_grid_flow_field.u_sorted[:, i:i+1] + v_i = turbine_grid_flow_field.v_sorted[:, i:i+1] + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + + # Initialize the effective yaw angle + self.effective_yaw_i = self.yaw_angle_i.copy() + + # Model calculations + if self.enable_secondary_steering: + added_yaw = wake_added_yaw( + u_i, + v_i, + turbine_grid_flow_field.u_initial_sorted, + turbine_grid.y_sorted[:, i:i+1] - self.y_i, + turbine_grid.z_sorted[:, i:i+1], + self.rotor_diameter_i, + self.hub_height_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + self.effective_yaw_i += added_yaw + + deflection_field = self.deflection( + turbulence_intensity_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + if self.enable_transverse_velocities: + v_wake, w_wake = calculate_transverse_velocity( + u_i, + flow_field.u_initial_sorted, + flow_field.dudz_initial_sorted, + grid.x_sorted - self.x_i, + grid.y_sorted - self.y_i, + grid.z_sorted, + self.rotor_diameter_i, + self.hub_height_i, + self.yaw_angle_i, + thrust_coefficient_i, + self.TSR_i, + axial_induction_i, + flow_field.wind_shear, + ) + else: + v_wake = np.zeros_like(flow_field.v_initial_sorted) + w_wake = np.zeros_like(flow_field.w_initial_sorted) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbulence_intensity_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + turbulence_intensity_field = self.turbulence( + turbulence_intensity_field, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + flow_field.v_sorted += v_wake + flow_field.w_sorted += w_wake + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field + + +# @profile +def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): + + ## original + # a = cosd(wind_veer) ** 2 / (2 * sigma_y ** 2) + sind(wind_veer) ** 2 / (2 * sigma_z ** 2) + # b = -sind(2 * wind_veer) / (4 * sigma_y ** 2) + sind(2 * wind_veer) / (4 * sigma_z ** 2) + # c = sind(wind_veer) ** 2 / (2 * sigma_y ** 2) + cosd(wind_veer) ** 2 / (2 * sigma_z ** 2) + # r_squared = ( + # a * (y - y_i - delta) ** 2 + # - 2 * b * (y - y_i - delta) * (z - HH) + # + c * (z - HH) ** 2 + # ) + # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / D ** 2)), 0.0, 1.0)) + + ## Precalculate some parts + # twox_sigmay_2 = 2 * sigma_y ** 2 + # twox_sigmaz_2 = 2 * sigma_z ** 2 + # a = cosd(wind_veer) ** 2 / (twox_sigmay_2) + sind(wind_veer) ** 2 / (twox_sigmaz_2) + # b = -sind(2 * wind_veer) / (2 * twox_sigmay_2) + sind(2 * wind_veer) / (2 * twox_sigmaz_2) + # c = sind(wind_veer) ** 2 / (twox_sigmay_2) + cosd(wind_veer) ** 2 / (twox_sigmaz_2) + # delta_y = y - y_i - delta + # delta_z = z - HH + # r_squared = (a * (delta_y ** 2) - 2 * b * (delta_y) * (delta_z) + c * (delta_z ** 2)) + # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / (D * D))), 0.0, 1.0)) + + ## Numexpr + wind_veer = np.deg2rad(wind_veer) + a = ne.evaluate( + "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + b = ne.evaluate( + "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" + ) + c = ne.evaluate( + "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" + ) + r_squared = ne.evaluate( + "a * ((y - y_i - delta) ** 2) - 2 * b * (y - y_i - delta) * (z - HH) + c * ((z - HH) ** 2)" + ) + d = np.clip(1 - (Ct * cosd(yaw) / ( 8.0 * sigma_y * sigma_z / (D * D) )), 0.0, 1.0) + C = ne.evaluate("1 - sqrt(d)") + return r_squared, C + + +def gaussian_function(C, r_squared, n, sigma): + result = ne.evaluate("C * exp(-1 * r_squared ** n / (2 * sigma ** 2))") + return result diff --git a/floris/core/wake_deflection/gauss.py b/floris/core/wake_model/gch_components.py similarity index 58% rename from floris/core/wake_deflection/gauss.py rename to floris/core/wake_model/gch_components.py index a210ef95e7..7fdfd934cb 100644 --- a/floris/core/wake_deflection/gauss.py +++ b/floris/core/wake_model/gch_components.py @@ -1,211 +1,16 @@ -from typing import Any - import numexpr as ne import numpy as np from attrs import ( - define, - field, fields, ) from numpy import pi -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) +from floris.core import BaseModel from floris.utilities import cosd, sind NUM_EPS = fields(BaseModel).NUM_EPS.default -@define -class GaussVelocityDeflection(BaseModel): - """ - The Gauss deflection model is a blend of the models described in - :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` for - calculating the deflection field in turbine wakes. - - parameter_dictionary (dict): Model-specific parameters. - Default values are used when a parameter is not included - in `parameter_dictionary`. Possible key-value pairs include: - - - **ka** (*float*): Parameter used to determine the linear - relationship between the turbulence intensity and the - width of the Gaussian wake shape. - - **kb** (*float*): Parameter used to determine the linear - relationship between the turbulence intensity and the - width of the Gaussian wake shape. - - **alpha** (*float*): Parameter that determines the - dependence of the downstream boundary between the near - wake and far wake region on the turbulence intensity. - - **beta** (*float*): Parameter that determines the - dependence of the downstream boundary between the near - wake and far wake region on the turbine's induction - factor. - - **ad** (*float*): Additional tuning parameter to modify - the wake deflection with a lateral offset. - Defaults to 0. - - **bd** (*float*): Additional tuning parameter to modify - the wake deflection with a lateral offset. - Defaults to 0. - - **dm** (*float*): Additional tuning parameter to scale - the amount of wake deflection. Defaults to 1.0 - - **use_secondary_steering** (*bool*): Flag to use - secondary steering on the wake velocity using methods - developed in [2]. - - **eps_gain** (*float*): Tuning value for calculating - the V- and W-component velocities using methods - developed in [7]. - TODO: Believe this should be removed, need to verify. - See property on super-class for more details. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: gdm- - """ - - ad: float = field(converter=float, default=0.0) - bd: float = field(converter=float, default=0.0) - alpha: float = field(converter=float, default=0.58) - beta: float = field(converter=float, default=0.077) - ka: float = field(converter=float, default=0.38) - kb: float = field(converter=float, default=0.004) - dm: float = field(converter=float, default=1.0) - eps_gain: float = field(converter=float, default=0.2) - use_secondary_steering: bool = field(converter=bool, default=True) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "freestream_velocity": flow_field.u_initial_sorted, - "wind_veer": flow_field.wind_veer, - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - yaw_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - rotor_diameter_i: float, - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - freestream_velocity: np.ndarray, - wind_veer: float, - ): - """ - Calculates the deflection field of the wake. See - :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` - for details on the methods used. - - Args: - x_i (np.array): x-coordinates of turbine i. - y_i (np.array): y-coordinates of turbine i. - yaw_i (np.array): Yaw angle of turbine i. - turbulence_intensity_i (np.array): Turbulence intensity at turbine i. - ct_i (np.array): Thrust coefficient of turbine i. - rotor_diameter_i (float): Rotor diameter of turbine i. - - Returns: - np.array: Deflection field for the wake. - """ - # ============================================================== - - # Opposite sign convention in this model - yaw_i *= -1 - - # TODO: connect support for tilt - tilt = 0.0 # turbine.tilt_angle - - # initial velocity deficits - uR = ( - freestream_velocity - * ct_i - * cosd(tilt) - * cosd(yaw_i) - / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) - ) - u0 = freestream_velocity * np.sqrt(1 - ct_i) - - # length of near wake - x0 = ( - rotor_diameter_i - * (cosd(yaw_i) * (1 + np.sqrt(1 - ct_i * cosd(yaw_i)))) - / (np.sqrt(2) * ( - 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i)) - )) + x_i - ) - - # wake expansion parameters - ky = self.ka * turbulence_intensity_i + self.kb - kz = self.ka * turbulence_intensity_i + self.kb - - C0 = 1 - u0 / freestream_velocity - M0 = C0 * (2 - C0) - E0 = ne.evaluate("C0 ** 2 - 3 * exp(1.0 / 12.0) * C0 + 3 * exp(1.0 / 3.0)") - - # initial Gaussian wake expansion - sigma_z0 = ne.evaluate("rotor_diameter_i * 0.5 * sqrt(uR / (freestream_velocity + u0))") - sigma_y0 = sigma_z0 * cosd(yaw_i) * cosd(wind_veer) - - # yR = y - y_i - xR = x_i # yR * tand(yaw) + x_i - - # yaw parameters (skew angle and distance from centerline) - # skew angle in radians - theta_c0 = self.dm * (0.3 * np.radians(yaw_i) / cosd(yaw_i)) - theta_c0 *= (1 - np.sqrt(1 - ct_i * cosd(yaw_i))) - delta0 = np.tan(theta_c0) * (x0 - x_i) # initial wake deflection; - # NOTE: use np.tan here since theta_c0 is radians - - # deflection in the near wake - delta_near_wake = ((x - xR) / (x0 - xR)) * delta0 + (self.ad + self.bd * (x - x_i)) - delta_near_wake *= (x >= xR) & (x <= x0) - - # deflection in the far wake - sigma_y = ky * (x - x0) + sigma_y0 - sigma_z = kz * (x - x0) + sigma_z0 - sigma_y = sigma_y * (x >= x0) + sigma_y0 * (x < x0) - sigma_z = sigma_z * (x >= x0) + sigma_z0 * (x < x0) - - M0_sqrt = np.sqrt(M0) - middle_term = np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) - ln_deltaNum = (1.6 + M0_sqrt) * (1.6 * middle_term - M0_sqrt) - ln_deltaDen = (1.6 - M0_sqrt) * (1.6 * middle_term + M0_sqrt) - - middle_term = ne.evaluate( - "theta_c0" - " * E0" - " / 5.2" - " * sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0))" - " * log(ln_deltaNum / ln_deltaDen)" - ) - delta_far_wake = delta0 + middle_term + (self.ad + self.bd * (x - x_i)) - - delta_far_wake = delta_far_wake * (x > x0) - deflection = delta_near_wake + delta_far_wake - - return deflection - -## GCH components - def gamma( D, velocity, diff --git a/floris/core/wake_model/jensen.py b/floris/core/wake_model/jensen.py new file mode 100644 index 0000000000..1c411892da --- /dev/null +++ b/floris/core/wake_model/jensen.py @@ -0,0 +1,365 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd, sind + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class JensenJimenez(BaseWakeModel): + + # Jensen deficit model parameters + we: float = field(default=0.05) + + # Jimenez deflection model parameters + kd: float = field(default=0.05) # TODO: is this the same as we? + ad: float = field(default=0.0) + bd: float = field(default=0.0) + + # Crespo-Hernandez turbulence model parameters + initial: float = field(converter=float, default=0.1) + constant: float = field(converter=float, default=0.9) + ai: float = field(converter=float, default=0.8) + downstream: float = field(converter=float, default=-0.32) + + # Uninitialized attributes set in turbine_solve + ambient_turbulence_intensities: np.ndarray = field(init=False, default=None) + + def velocity_deficit( + self, + axial_induction_i: np.ndarray, + deflection_field_i: np.ndarray, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + + # u is 4-dimensional (n wind speeds, n turbines, grid res 1, grid res 2) + # velocities is 3-dimensional (n turbines, grid res 1, grid res 2) + + # TODO: How much faster is numexpr? Is it worth it still worth it? + + x_i = self.x_i + y_i = self.y_i + z_i = self.z_i + yaw_angle_i = self.yaw_angle_i + hub_height_i = self.hub_height_i + rotor_diameter_i = self.rotor_diameter_i # Must be unpacked for numexpr? + + rotor_radius = rotor_diameter_i / 2.0 + + dx = ne.evaluate("x - x_i") + dy = ne.evaluate("y - y_i - deflection_field_i") + dz = ne.evaluate("z - z_i") + + we = self.we + + # Construct a boolean mask to include all points downstream of the turbine + downstream_mask = ne.evaluate("dx > 0 + NUM_EPS") + + # Construct a boolean mask to include all points within the wake boundary + # as defined by the Jensen model. This is a linear wake expansion that makes + # a shape like a cone and starts at the turbine disc. + # The left side of the inequality below evaluates the distance from the wake centerline + # for all points including positive and negative values. The inequality compares distance + # from the centerline and it must be below the line defined by the wake + # expansion parameter, "we". + boundary_mask = ne.evaluate("sqrt(dy ** 2 + dz ** 2) < we * dx + rotor_radius") + + # Calculate C for points within the mask and fill points outside with 0 + c = np.where( + np.logical_and(downstream_mask, boundary_mask), + ne.evaluate("(rotor_radius / (rotor_radius + we * dx + NUM_EPS)) ** 2"), # This is "C" + 0.0, + ) + + velocity_deficit = ne.evaluate("2 * axial_induction_i * c") + + return velocity_deficit + + def deflection( + self, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + ) -> np.ndarray: + # TODO: Does it make more sense for x to simply be on the class? Seems to. + # What should be passed in vs live on the class? + """ + Calculates the deflection field of the wake in relation to the yaw of + the turbine. This is coded as defined in [1]. + + Args: + x_locations (np.array): streamwise locations in wake + y_locations (np.array): spanwise locations in wake + z_locations (np.array): vertical locations in wake + (not used in Jiménez) + turbine (:py:class:`floris.core.turbine.Turbine`): + Turbine object + coord + (:py:meth:`floris.core.turbine_map.TurbineMap.coords`): + Spatial coordinates of wind turbine. + flow_field + (:py:class:`floris.core.flow_field.FlowField`): + Flow field object. + + Returns: + deflection (np.array): Deflected wake centerline. + + + This function calculates the deflection of the entire flow field + given the yaw angle and Ct of the current turbine + """ + + # Unpack for numexpr + kd = self.kd + ad = self.ad + bd = self.bd + + x_i = self.x_i + y_i = self.y_i + yaw_i = self.yaw_angle_i + rotor_diameter_i = self.rotor_diameter_i + + # angle of deflection + xi_init = cosd(yaw_i) * sind(yaw_i) * ct_i / 2.0 + + delta_x = ne.evaluate("x - x_i") + A = ne.evaluate("15 * (2 * kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0") + B = ne.evaluate("(30 * kd / rotor_diameter_i)") + B = ne.evaluate("B * ( 2 * kd * delta_x / rotor_diameter_i + 1 ) ** 5.0") + C = ne.evaluate("xi_init * rotor_diameter_i * (15 + xi_init ** 2.0)") + D = ne.evaluate("30 * kd") + + yYaw_init = ne.evaluate("(xi_init * A / B) - (C / D)") + deflection = ne.evaluate("yYaw_init + ad + bd * delta_x") + + return deflection + + def turbulence( + self, + turbulence_intensity: np.ndarray, + x: np.ndarray, + y: np.ndarray, + axial_induction: np.ndarray, + area_overlap: np.ndarray, + ) -> np.ndarray: + # Replace zeros and negatives with 1 to prevent nans/infs + x_i = self.x_i + rotor_diameter_i = self.rotor_diameter_i + delta_x = x - x_i + ambient_TI = self.ambient_turbulence_intensities + + # TODO: ensure that these fudge factors are needed for different rotations + upstream_mask = delta_x <= 0.1 + downstream_mask = delta_x > -0.1 + + # Keep downstream components Set upstream to 1.0 + delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask + + # turbulence intensity calculation based on Crespo et. al. + constant = self.constant + ai = self.ai + initial = self.initial + downstream = self.downstream + ti = ne.evaluate( + "constant" + " * axial_induction ** ai" + " * ambient_TI ** initial" + " * (delta_x / rotor_diameter_i) ** downstream" + ) + # Mask the 1 values from above with zeros + wake_added_turbulence_intensity = ti * downstream_mask + + # Modify wake added turbulence by wake area overlap + downstream_influence_length = 15 * self.rotor_diameter_i + ti_added = ( + area_overlap + * np.nan_to_num(wake_added_turbulence_intensity, posinf=0.0) + * (x > self.x_i) + * (np.abs(self.y_i - y) < 2 * self.rotor_diameter_i) + * (x <= downstream_influence_length + self.x_i) + ) + # Combine turbine TIs with WAT + turbulence_intensity = np.maximum( + np.sqrt(ti_added**2 + ambient_TI**2), turbulence_intensity + ) + + return turbulence_intensity + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + turbine_turbulence_intensity = np.repeat( + flow_field.turbulence_intensities[:, None, None, None], + farm.n_turbines, + axis=1 + ) + + # Ambient turbulent intensity should be a copy of n_findex-long turbulence_intensity + # with dimensions expanded for (n_turbines, grid, grid) + self.ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + grid, farm, flow_field, i + ) + axial_induction_i = self.evaluate_turbine_axial_induction(grid, farm, flow_field, i) + + # Model calculations + deflection_field = self.deflection( + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbine_turbulence_intensity[:, i:i+1], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + # Calculate wake overlap for wake-added turbulence (WAT) + area_overlap = ( + np.sum(velocity_deficit * flow_field.u_initial_sorted > 0.05, axis=(2, 3)) + / (grid.grid_resolution * grid.grid_resolution) + ) + area_overlap = area_overlap[:, :, None, None] + + turbine_turbulence_intensity = self.turbulence( + turbine_turbulence_intensity, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + area_overlap, + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + # Add the final turbine turbulence intensity field to the flow field object + flow_field.turbulence_intensity_field_sorted = turbine_turbulence_intensity + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + turbine_turbulence_intensity, + axis=(2,3), + keepdims=True + ) + + # Compute turbine powers based on final flow field + self.evaluate_turbine_power(grid, farm, flow_field) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + turbulence_intensity_field = ambient_turbulence_intensities.copy() + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + axial_induction_i = self.evaluate_turbine_axial_induction( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + turbulence_intensity_i = \ + turbine_grid_flow_field.turbulence_intensity_field_sorted_avg[:, i:i+1] + + # Model calculations + deflection_field = self.deflection( + turbulence_intensity_i, + thrust_coefficient_i, + grid.x_sorted, + ) + + velocity_deficit = self.velocity_deficit( + axial_induction_i, + deflection_field, + turbulence_intensity_i, + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + turbulence_intensity_field = self.turbulence( + turbulence_intensity_field, + grid.x_sorted, + grid.y_sorted, + axial_induction_i, + np.where(velocity_deficit * flow_field.u_initial_sorted > 0.05, 1, 0), + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = turbulence_intensity_field diff --git a/floris/core/wake_model/none_model.py b/floris/core/wake_model/none_model.py new file mode 100644 index 0000000000..cf61aad8f1 --- /dev/null +++ b/floris/core/wake_model/none_model.py @@ -0,0 +1,55 @@ +import numexpr as ne +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.utilities import cosd, sind + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class NoneWake(BaseWakeModel): + + def __attrs_post_init__(self): + self.logger.warning("The wake model is set to 'none'. Wake modeling disabled.") + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + # None wake model does not calculate any velocity deficits, so simply set the flow field + flow_field.u_sorted = flow_field.u_initial_sorted.copy() + print("I WAS HERE") + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + + # None wake model; set to final values. + flow_field.u_sorted = flow_field.u_initial_sorted + flow_field.turbulence_intensity_field_sorted = ambient_turbulence_intensities.copy() diff --git a/floris/core/wake_model/turboparkgauss.py b/floris/core/wake_model/turboparkgauss.py new file mode 100644 index 0000000000..6692b61522 --- /dev/null +++ b/floris/core/wake_model/turboparkgauss.py @@ -0,0 +1,219 @@ +import numpy as np +from attrs import ( + define, + field, + fields, +) + +from floris.core import ( + BaseModel, + Farm, + FlowField, + FlowFieldPlanarGrid, + PointsGrid, + TurbineGrid, +) +from floris.core.wake_model import BaseWakeModel +from floris.core.wake_model.gauss import gaussian_function + + +NUM_EPS = fields(BaseModel).NUM_EPS.default + +@define +class TurbOParkGauss(BaseWakeModel): + """ + Model based on TurbOPark with Gaussian wake profile (Pedersen et al. 2020). + + Does not use a deflection model (yaw not supported) or turbulence model + (built into Frandsen-based wake width calculation) + + References: + Pedersen J G, Svensen E, Poulsen L, and Nygaard N G. "Turbulence Optimized + Park model with Gaussian wake profile." Journal of Physics: Conference + Series. Vol. 2265. No. 022063. IOP Publishing, 2020. + doi:10.1088/1742-6596/2265/2/022063 + """ + + # TurboparkGauss-specific parameters + A: float = field(converter=float, default=0.04) + include_mirror_wake: bool = field(converter=bool, default=True) + + # Set during solve routines + freestream_velocity: np.ndarray = field(init=False, default=None) + + + def velocity_deficit( + self, + turbulence_intensity_i: np.ndarray, + ct_i: np.ndarray, + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + ) -> np.ndarray: + # Initialize the velocity deficit array + velocity_deficit = np.zeros_like(self.freestream_velocity) + + downstream_mask = (x - self.x_i >= NUM_EPS) + x_dist = (x - self.x_i) * downstream_mask / self.rotor_diameter_i + + # Characteristic wake widths from all turbines relative to turbine i + sigma = characteristic_wake_width( + x_dist, turbulence_intensity_i, ct_i, self.A + ) * self.rotor_diameter_i + + # Peak wake deficits + C = 1 - np.sqrt(np.clip(1 - ct_i / (8 * (sigma / self.rotor_diameter_i) ** 2), 0.0, 1.0)) + + r_dist = np.sqrt((y - self.y_i) ** 2 + (z - self.z_i) ** 2) + + # Compute deficits for real turbines and for mirrored (image) turbines + delta_real = (x_dist > 0) * gaussian_function(C, r_dist, 2, sigma) + if self.include_mirror_wake: + r_dist_image = np.sqrt((y - self.y_i) ** 2 + (z - 3*self.z_i) ** 2) + delta_image = (x_dist > 0) * gaussian_function(C, r_dist_image, 2, sigma) + delta = np.hypot(delta_real, delta_image) + else: # No mirror wakes + delta = delta_real + + velocity_deficit = np.nan_to_num(delta) + + return velocity_deficit + + def turbine_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: TurbineGrid, + ) -> None: + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Expand input turbulence intensity to 4d for (n_turbines, grid, grid) + ambient_turbulence_intensities = np.repeat( + flow_field.turbulence_intensities[:, None, None, None], + farm.n_turbines, + axis=1 + ) + + # Copy uniform flow field parameters + self.freestream_velocity = flow_field.u_initial_sorted + + # Calculate the velocity deficit sequentially from upstream to downstream turbines + for i in range(grid.n_turbines): + + # Turbine quantities + self.set_turbine_i(grid, farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + grid, farm, flow_field, i + ) + + # Model calculations + velocity_deficit = self.velocity_deficit( + ambient_turbulence_intensities[:, i:i+1, :, :], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + # Copy background turbulence intensity to flow field + flow_field.turbulence_intensity_field_sorted = ambient_turbulence_intensities + flow_field.turbulence_intensity_field_sorted_avg = np.mean( + ambient_turbulence_intensities, + axis=(2,3), + keepdims=True + ) + + # Compute turbine powers, axial inductions based on final flow field + self.evaluate_turbine_power(grid, farm, flow_field) + self.evaluate_turbine_axial_induction(grid, farm, flow_field) + + def point_solve( + self, + farm: Farm, + flow_field: FlowField, + grid: FlowFieldPlanarGrid | PointsGrid, + ) -> None: + + # Get the flow quantities and turbine performance + ( + turbine_grid_farm, + turbine_grid_flow_field, + turbine_grid + ) = self.generate_turbine_grid_objects(farm, flow_field) + + self.turbine_solve(turbine_grid_farm, turbine_grid_flow_field, turbine_grid) + + wake_field = np.zeros_like(flow_field.u_initial_sorted) + + # Initialize the turbulence intensity field over the entire flow field grid + n_points = grid.x_sorted.shape[1] + ambient_turbulence_intensities = flow_field.turbulence_intensities[:, None, None, None] + ambient_turbulence_intensities = np.repeat(ambient_turbulence_intensities, n_points, axis=1) + + # Calculate the velocity deficit in the full grid sequentially from upstream to + # downstream turbines + for i in range(grid.n_turbines): + + # Get the current turbine quantities + self.set_turbine_i(turbine_grid, turbine_grid_farm, i) + thrust_coefficient_i = self.evaluate_turbine_thrust_coefficient( + turbine_grid, + turbine_grid_farm, + turbine_grid_flow_field, + i + ) + + # Model calculations + velocity_deficit = self.velocity_deficit( + ambient_turbulence_intensities[:, i:i+1, :, :], + thrust_coefficient_i, + grid.x_sorted, + grid.y_sorted, + grid.z_sorted + ) + + wake_field = self.combination_function( + wake_field, + velocity_deficit * flow_field.u_initial_sorted + ) + + flow_field.u_sorted = flow_field.u_initial_sorted - wake_field + + flow_field.turbulence_intensity_field_sorted = ambient_turbulence_intensities + + +def characteristic_wake_width(x_D, ambient_TI, Cts, A): + # Parameter values taken from S. T. Frandsen, “Risø-R-1188(EN) Turbulence + # and turbulence generated structural loading in wind turbine clusters” + # Risø, Roskilde, Denmark, 2007. + c1 = 1.5 + c2 = 0.8 + + alpha = ambient_TI * c1 + beta = c2 * ambient_TI / np.sqrt(Cts) + + # Term for the initial width at the turbine location (denoted epsilon in Pedersen et al.) + # Saturate term in initial width to 3.0, as is done in Orsted Matlab code. + initial_width = 0.25 * np.sqrt(np.minimum(0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3.0)) + + # Term for the added width downstream of the turbine + added_width = A * ambient_TI / beta * ( + np.sqrt((alpha + beta * x_D) ** 2 + 1) + - np.sqrt(1 + alpha ** 2) + - np.log( + ((np.sqrt((alpha + beta * x_D) ** 2 + 1) + 1) * alpha) + / ((np.sqrt(1 + alpha ** 2) + 1) * (alpha + beta * x_D)) + ) + ) + + sigma_w_D = initial_width + added_width + + return sigma_w_D diff --git a/floris/core/wake_model/wake_combination.py b/floris/core/wake_model/wake_combination.py new file mode 100644 index 0000000000..b2892a51a1 --- /dev/null +++ b/floris/core/wake_model/wake_combination.py @@ -0,0 +1,68 @@ +""" +Library of functions defining wake combination approaches. +""" + +import numpy as np + + +def sosfs(wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Combines the base flow field with the velocity deficits + using sum of squares. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return np.hypot(wake_field, velocity_field) + + +def maximum(wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Incorporates the velocity deficits into the base flow field by + selecting the maximum of the two for each point. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return np.maximum(wake_field, velocity_field) + + +def fls(wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Combines the base flow field with the velocity deficits + using freestream linear superposition. In other words, the wake + field and base fields are simply added together. + + Args: + u_field (np.array): The base flow field. + u_wake (np.array): The wake to apply to the base flow field. + + Returns: + np.array: The resulting flow field after applying the wake to the + base. + """ + return wake_field + velocity_field + +def none_combination(wake_field: np.ndarray, velocity_field: np.ndarray): + """ + Return None, indicating no combination is applied. Likely will not be called + in a functional model. + + Args: + wake_field (np.array): The wake to apply to the base flow field. + velocity_field (np.array): The base flow field. + + Returns: + None + """ + return None diff --git a/floris/core/wake_turbulence/__init__.py b/floris/core/wake_turbulence/__init__.py deleted file mode 100644 index 8bec72939e..0000000000 --- a/floris/core/wake_turbulence/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from floris.core.wake_turbulence.crespo_hernandez import CrespoHernandez -from floris.core.wake_turbulence.none import NoneWakeTurbulence -from floris.core.wake_turbulence.wake_induced_mixing import WakeInducedMixing diff --git a/floris/core/wake_turbulence/crespo_hernandez.py b/floris/core/wake_turbulence/crespo_hernandez.py deleted file mode 100644 index d87987d7ab..0000000000 --- a/floris/core/wake_turbulence/crespo_hernandez.py +++ /dev/null @@ -1,103 +0,0 @@ - -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import cosd, sind - - -@define -class CrespoHernandez(BaseModel): - """ - CrespoHernandez is a wake-turbulence model that is used to compute - additional variability introduced to the flow field by operation of a wind - turbine. Implementation of the model follows the original formulation and - limitations outlined in :cite:`cht-crespo1996turbulence`. - - Note: The values for default parameters provided here differ from those in - :cite:`cht-crespo1996turbulence. Following their recommendations, the - default parameters would instead be: - - initial: -0.0325* - - constant: 0.73 - - ai: 0.8325 - - downstream: -0.32 - * The "initial" parameter is given as -0.0325 in :cite:`cht-crespo1996turbulence`, - but the negative exponent is not clear in the scans of the paper found on the internet, - and several subsequent paper cite the exponent as positive (0.0325). This discrepancy - is noted in :cite:`zehtabiyan_rezaie_CH_2023`. Moreover, :cite:`zehtabiyan_rezaie_CH_2023` - argues that positive values for this exponent are not representative of the physical - phenomena occurring. For more details, see https://github.com/NREL/floris/issues/773. - Nonetheless, the default value here is set to 0.1 for consistency with previous - FLORIS versions. The default value may be updated in a future release. - - Args: - parameter_dictionary (dict): Model-specific parameters. - Default values are used when a parameter is not included - in `parameter_dictionary`. Possible key-value pairs include: - - - **initial** (*float*): The exponent on the initial ambient - turbulence intensity. - - **constant** (*float*): The constant used to scale the - wake-added turbulence intensity. - - **ai** (*float*): The axial induction factor exponent used - in in the calculation of wake-added turbulence. - - **downstream** (*float*): The exponent applied to the - distance downstream of an upstream turbine normalized by - the rotor diameter used in the calculation of wake-added - turbulence. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: cht- - """ - - initial: float = field(converter=float, default=0.1) - constant: float = field(converter=float, default=0.9) - ai: float = field(converter=float, default=0.8) - downstream: float = field(converter=float, default=-0.32) - - def prepare_function(self) -> dict: - pass - - def function( - self, - ambient_TI: float, - x: np.ndarray, - x_i: np.ndarray, - rotor_diameter: float, - axial_induction: np.ndarray, - ) -> None: - # Replace zeros and negatives with 1 to prevent nans/infs - delta_x = x - x_i - - # TODO: ensure that these fudge factors are needed for different rotations - upstream_mask = delta_x <= 0.1 - downstream_mask = delta_x > -0.1 - - # Keep downstream components Set upstream to 1.0 - delta_x = delta_x * downstream_mask + np.ones_like(delta_x) * upstream_mask - - # turbulence intensity calculation based on Crespo et. al. - constant = self.constant - ai = self.ai - initial = self.initial - downstream = self.downstream - ti = ne.evaluate( - "constant" - " * axial_induction ** ai" - " * ambient_TI ** initial" - " * (delta_x / rotor_diameter) ** downstream" - ) - # Mask the 1 values from above with zeros - return ti * downstream_mask diff --git a/floris/core/wake_turbulence/none.py b/floris/core/wake_turbulence/none.py deleted file mode 100644 index 09de7a30bd..0000000000 --- a/floris/core/wake_turbulence/none.py +++ /dev/null @@ -1,32 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define, field - -from floris.core import BaseModel - - -@define -class NoneWakeTurbulence(BaseModel): - """ - The None wake turbulence model is a placeholder code that simple ignores - any wake turbulence and just returns an array of the ambient TIs. - """ - - def prepare_function(self) -> dict: - pass - - def function( - self, - ambient_TI: float, - x: np.ndarray, - x_i: np.ndarray, - rotor_diameter: float, - axial_induction: np.ndarray, - ) -> None: - """Return unchanged field of turbulence intensities""" - self.logger.info( - "The wake-turbulence model is set to 'none'. Turbulence model disabled." - ) - return np.zeros_like(x) diff --git a/floris/core/wake_turbulence/wake_induced_mixing.py b/floris/core/wake_turbulence/wake_induced_mixing.py deleted file mode 100644 index 64306ff754..0000000000 --- a/floris/core/wake_turbulence/wake_induced_mixing.py +++ /dev/null @@ -1,76 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import cosd, sind - - -@define -class WakeInducedMixing(BaseModel): - """ - WakeInducedMixing is a model used to generalize wake-added turbulence - in the Empirical Gaussian wake model. It computes the contribution of each - turbine to a "wake-induced mixing" term that in turn is used in the - velocity deficit and deflection models. - - Args: - parameter_dictionary (dict): Model-specific parameters. - Default values are used when a parameter is not included - in `parameter_dictionary`. Possible key-value pairs include: - - - **atmospheric_ti_gain** (*float*): The contribution of ambient - turbulent intensity to the wake-induced mixing term. Currently - throws a warning if nonzero. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - """ - atmospheric_ti_gain: float = field(converter=float, default=0.0) - - def __attrs_post_init__(self) -> None: - if self.atmospheric_ti_gain != 0.0: - nonzero_err_msg = \ - "Running wake_induced_mixing model with mixing contributions"+\ - " from the atmospheric turbulence intensity has not been"+\ - " vetted. To avoid this warning, set atmospheric_ti_gain=0."+\ - " in the FLORIS input yaml." - self.logger.warning(nonzero_err_msg, stack_info=True) - - def prepare_function(self) -> dict: - pass - - def function( - self, - axial_induction_i: np.ndarray, - downstream_distance_D_i: np.ndarray, - ) -> None: - """ - Calculates the contribution of turbine i to all other turbines' - mixing terms. - - Args: - axial_induction_i (np.array): Axial induction factor of - the ith turbine (-). - downstream_distance_D_i (np.array): The distance downstream - from turbine i to all other turbines (specified in terms - of multiples of turbine i's rotor diameter) (D). - - Returns: - np.array: Components of the wake-induced mixing term due to - the ith turbine. - """ - - wake_induced_mixing = axial_induction_i[:,:,0,0] / downstream_distance_D_i**2 - - return wake_induced_mixing diff --git a/floris/core/wake_velocity/__init__.py b/floris/core/wake_velocity/__init__.py deleted file mode 100644 index 07762a5cd4..0000000000 --- a/floris/core/wake_velocity/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ - -from floris.core.wake_velocity.cumulative_gauss_curl import CumulativeGaussCurlVelocityDeficit -from floris.core.wake_velocity.empirical_gauss import EmpiricalGaussVelocityDeficit -from floris.core.wake_velocity.gauss import GaussVelocityDeficit -from floris.core.wake_velocity.jensen import JensenVelocityDeficit -from floris.core.wake_velocity.none import NoneVelocityDeficit -from floris.core.wake_velocity.turbopark import TurbOParkVelocityDeficit -from floris.core.wake_velocity.turboparkgauss import TurboparkgaussVelocityDeficit diff --git a/floris/core/wake_velocity/cumulative_gauss_curl.py b/floris/core/wake_velocity/cumulative_gauss_curl.py deleted file mode 100644 index ba88c574df..0000000000 --- a/floris/core/wake_velocity/cumulative_gauss_curl.py +++ /dev/null @@ -1,232 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define, field -from scipy.special import gamma - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import ( - cosd, - sind, - tand, -) - - -@define -class CumulativeGaussCurlVelocityDeficit(BaseModel): - """ - The cumulative curl model is an implementation of the model described in - :cite:`cc-bay_2022`, which itself is based on the cumulative model of - :cite:`cc-bastankhah_2021`. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: cc- - """ - - a_s: float = field(default=0.179367259) - b_s: float = field(default=0.0118889215) - c_s1: float = field(default=0.0563691592) - c_s2: float = field(default=0.13290157) - a_f: float = field(default=3.11) - b_f: float = field(default=-0.68) - c_f: float = field(default=2.41) - alpha_mod: float = field(default=1.0) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "u_initial": flow_field.u_initial_sorted, - } - return kwargs - - def function( - self, - ii: int, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - u_i: np.ndarray, - deflection_field: np.ndarray, - yaw_i: np.ndarray, - turbulence_intensity: np.ndarray, - ct: np.ndarray, - turbine_diameter: np.ndarray, - turb_u_wake: np.ndarray, - Ctmp: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - u_initial: np.ndarray, - ) -> None: - - turbine_Ct = ct - turbine_ti = turbulence_intensity - turbine_yaw = yaw_i - - # TODO Should this be cbrt? This is done to match v2 - turb_avg_vels = np.cbrt(np.mean(u_i ** 3, axis=(2, 3), keepdims=True)) - - delta_x = x - x_i - - sigma_n = wake_expansion( - delta_x, - turbine_Ct[:, ii:ii+1], - turbine_ti[:, ii:ii+1], - turbine_diameter[:, ii:ii+1], - self.a_s, - self.b_s, - self.c_s1, - self.c_s2, - ) - - y_i_loc = np.mean(y_i, axis=(2, 3), keepdims=True) - z_i_loc = np.mean(z_i, axis=(2, 3), keepdims=True) - - x_coord = np.mean(x, axis=(2, 3), keepdims=True) - - y_loc = y - y_coord = np.mean(y, axis=(2, 3), keepdims=True) - - z_loc = z # np.mean(z, axis=(3,4)) - z_coord = np.mean(z, axis=(2, 3), keepdims=True) - - sum_lbda = np.zeros_like(u_initial) - - for m in range(0, ii - 1): - x_coord_m = x_coord[:, m:m+1] - y_coord_m = y_coord[:, m:m+1] - z_coord_m = z_coord[:, m:m+1] - - # For computing cross planes, we don't need to compute downstream - # turbines from out cross plane position. - if x_coord[:, m:m+1].size == 0: - break - - delta_x_m = x - x_coord_m - - sigma_i = wake_expansion( - delta_x_m, - turbine_Ct[:, m:m+1], - turbine_ti[:, m:m+1], - turbine_diameter[:, m:m+1], - self.a_s, - self.b_s, - self.c_s1, - self.c_s2, - ) - - S_i = sigma_n ** 2 + sigma_i ** 2 - - Y_i = (y_i_loc - y_coord_m - deflection_field) ** 2 / (2 * S_i) - Z_i = (z_i_loc - z_coord_m) ** 2 / (2 * S_i) - - lbda = 1.0 * sigma_i ** 2 / S_i * np.exp(-Y_i) * np.exp(-Z_i) - - sum_lbda = sum_lbda + lbda * (Ctmp[m] / u_initial) - - # Vectorized version of sum_lbda calc; has issues with y_coord (needs to be - # down-selected appropriately. Prelim. timings show vectorized form takes - # longer than for loop.) - # if ii >= 2: - # S = sigma_n ** 2 + sigma_i[0:ii-1, :, :, :, :, :] ** 2 - # Y = (y_i_loc - y_coord - deflection_field) ** 2 / (2 * S) - # Z = (z_i_loc - z_coord) ** 2 / (2 * S) - - # lbda = self.alpha_mod * sigma_i[0:ii-1, :, :, :, :, :] ** 2 - # lbda /= S * np.exp(-Y) * np.exp(-Z) - # sum_lbda = np.sum(lbda * (Ctmp[0:ii-1, :, :, :, :, :] / u_initial), axis=0) - # else: - # sum_lbda = 0.0 - - # sigma_i[ii] = sigma_n - - # blondel - # super gaussian - # b_f = self.b_f1 * np.exp(self.b_f2 * TI) + self.b_f3 - x_tilde = np.abs(delta_x) / turbine_diameter[:,ii:ii+1] - r_tilde = np.sqrt( (y_loc - y_i_loc - deflection_field) ** 2 + (z_loc - z_i_loc) ** 2 ) - r_tilde /= turbine_diameter[:,ii:ii+1] - - n = self.a_f * np.exp(self.b_f * x_tilde) + self.c_f - a1 = 2 ** (2 / n - 1) - a2 = 2 ** (4 / n - 2) - - # based on Blondel model, modified to include cumulative effects - tmp = a2 - ( - (n * turbine_Ct[:, ii:ii+1]) - * cosd(turbine_yaw) - / ( - 16.0 - * gamma(2 / n) - * np.sign(sigma_n) - * (np.abs(sigma_n) ** (4 / n)) - * (1 - sum_lbda) ** 2 - ) - ) - - # for some low wind speeds, tmp can become slightly negative, which causes NANs, - # so replace the slightly negative values with zeros - tmp = tmp * (tmp >= 0) - - C = a1 - np.sqrt(tmp) - - C = C * (1 - sum_lbda) - - Ctmp[ii] = C - - yR = y_loc - y_i_loc - xR = yR * tand(turbine_yaw) + x_i - - # add turbines together - velDef = C * np.exp((-1 * r_tilde ** n) / (2 * sigma_n ** 2)) - - velDef = velDef * (x - xR >= 0.1) - - turb_u_wake = turb_u_wake + turb_avg_vels * velDef - return (turb_u_wake, Ctmp) - - -def wake_expansion( - delta_x, - ct_i, - turbulence_intensity_i, - rotor_diameter, - a_s, - b_s, - c_s1, - c_s2, -): - # Calculate Beta (Eq 10, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) - beta = 0.5 * (1.0 + np.sqrt(1.0 - ct_i)) / np.sqrt(1.0 - ct_i) - k = a_s * turbulence_intensity_i + b_s - eps = (c_s1 * ct_i + c_s2) * np.sqrt(beta) - - # Calculate sigma_tilde (Eq 9, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) - x_tilde = np.abs(delta_x) / rotor_diameter - sigma_y = k * x_tilde + eps - - # [added dimension to get upstream values, empty, wd, ws, x, y, z ] - # return sigma_y[na, :, :, :, :, :, :] - # Do this ^^ in the main function - - return sigma_y diff --git a/floris/core/wake_velocity/empirical_gauss.py b/floris/core/wake_velocity/empirical_gauss.py deleted file mode 100644 index 0df5a4f06a..0000000000 --- a/floris/core/wake_velocity/empirical_gauss.py +++ /dev/null @@ -1,308 +0,0 @@ - -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.core.wake_velocity.gauss import gaussian_function -from floris.type_dec import floris_float_type -from floris.utilities import ( - cosd, - sind, - tand, -) - - -@define -class EmpiricalGaussVelocityDeficit(BaseModel): - """ - The Empirical Gauss velocity model has a Gaussian profile - (see :cite:`bastankhah2016experimental` and - :cite:`King2019Controls`) throughout and expands in a (smoothed) - piecewise linear fashion. - - parameter_dictionary (dict): Model-specific parameters. - Default values are used when a parameter is not included - in `parameter_dictionary`. Possible key-value pairs include: - - - **wake_expansion_rates** (*list*): List of expansion - rates for the Gaussian wake width. Must be of length 1 - or greater. - - **breakpoints_D** (*list*): List of downstream - locations, specified in terms of rotor diameters, where - the expansion rates go into effect. Must be one element - shorter than wake_expansion_rates. May be empty. - - **sigma_0_D** (*float*): Initial width of the Gaussian - wake at the turbine location, specified as a multiplier - of the rotor diameter. - - **smoothing_length_D** (*float*): Distance over which - the corners in the piece-wise linear wake expansion rate - are smoothed (specified as a multiplier of the rotor - diameter). - - **mixing_gain_deflection** (*float*): Gain to set the - increase in wake expansion due to wake-induced mixing. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - """ - wake_expansion_rates: list = field(factory=lambda: [0.023, 0.008]) - breakpoints_D: list = field(factory=lambda: [10]) - sigma_0_D: float = field(default=0.28) - smoothing_length_D: float = field(default=2.0) - mixing_gain_velocity: float = field(default=2.0) - awc_mode: str = field(default="baseline") - awc_wake_exp: float = field(default=1.2) - awc_wake_denominator: float = field(default=400) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "wind_veer": flow_field.wind_veer - } - return kwargs - - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - axial_induction_i: np.ndarray, - deflection_field_y_i: np.ndarray, - deflection_field_z_i: np.ndarray, - yaw_angle_i: np.ndarray, - tilt_angle_i: np.ndarray, - mixing_i: np.ndarray, - ct_i: np.ndarray, - hub_height_i: float, - rotor_diameter_i: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - wind_veer: float - ) -> None: - """ - Calculates the velocity deficits in the wake. - - Args: - x_i (np.array): Streamwise direction grid coordinates of - the ith turbine (m). - y_i (np.array): Cross stream direction grid coordinates of - the ith turbine (m). - z_i (np.array): Vertical direction grid coordinates of - the ith turbine (m) [not used]. - axial_induction_i (np.array): Axial induction factor of the - ith turbine (-) [not used]. - deflection_field_y_i (np.array): Horizontal wake deflections - due to the ith turbine's yaw misalignment (m). - deflection_field_z_i (np.array): Vertical wake deflections - due to the ith turbine's tilt angle (m). - yaw_angle_i (np.array): Yaw angle of the ith turbine (deg). - tilt_angle_i (np.array): Tilt angle of the ith turbine - (deg). - mixing_i (np.array): The wake-induced mixing term for the - ith turbine. - ct_i (np.array): Thrust coefficient for the ith turbine (-). - hub_height_i (float): Hub height for the ith turbine (m). - rotor_diameter_i (np.array): Rotor diameter for the ith - turbine (m). - - x (np.array): Streamwise direction grid coordinates of the - flow field domain (m). - y (np.array): Cross stream direction grid coordinates of the - flow field domain (m). - z (np.array): Vertical direction grid coordinates of the - flow field domain (m). - wind_veer (np.array): Wind veer (deg). - - Returns: - np.array: Velocity deficits (-). - """ - - include_mirror_wake = True # Could add this as a user preference. - - # Only symmetric terms using yaw, but keep for consistency - yaw_angle = -1 * yaw_angle_i - - # Initial wake widths - sigma_y0 = self.sigma_0_D * rotor_diameter_i * cosd(yaw_angle) - sigma_z0 = self.sigma_0_D * rotor_diameter_i * cosd(tilt_angle_i) - - # No specific near, far wakes in this model - downstream_mask = (x > x_i + 0.1) - upstream_mask = (x < x_i - 0.1) - - # Wake expansion in the lateral (y) and the vertical (z) - # TODO: could compute shared components in sigma_z, sigma_y - # with one function call. - sigma_y = empirical_gauss_model_wake_width( - x - x_i, - self.wake_expansion_rates, - [b * rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] - sigma_y0, - self.smoothing_length_D * rotor_diameter_i, - self.mixing_gain_velocity * mixing_i, - ) - sigma_y[upstream_mask] = \ - np.tile(sigma_y0, np.shape(sigma_y)[1:])[upstream_mask] - - sigma_z = empirical_gauss_model_wake_width( - x - x_i, - self.wake_expansion_rates, - [b * rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] - sigma_z0, - self.smoothing_length_D * rotor_diameter_i, - self.mixing_gain_velocity * mixing_i, - ) - sigma_z[upstream_mask] = \ - np.tile(sigma_z0, np.shape(sigma_z)[1:])[upstream_mask] - - # 'Standard' wake component - r, C = rCalt( - wind_veer, - sigma_y, - sigma_z, - y, - y_i, - deflection_field_y_i, - deflection_field_z_i, - z, - hub_height_i, - ct_i, - yaw_angle, - tilt_angle_i, - rotor_diameter_i, - sigma_y0, - sigma_z0 - ) - # Normalize to match end of actuator disk model tube - C = C / (8 * self.sigma_0_D**2 ) - - wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) - - if include_mirror_wake: - # TODO: speed up this option by calculating various elements in - # rCalt only once. - # Mirror component - r_mirr, C_mirr = rCalt( - wind_veer, # TODO: Is veer OK with mirror wakes? - sigma_y, - sigma_z, - y, - y_i, - deflection_field_y_i, - deflection_field_z_i, - z, - -hub_height_i, # Turbine at negative hub height location - ct_i, - yaw_angle, - tilt_angle_i, - rotor_diameter_i, - sigma_y0, - sigma_z0 - ) - # Normalize to match end of actuator disk model tube - C_mirr = C_mirr / (8 * self.sigma_0_D**2) - - # ASSUME sum-of-squares superposition for the real and mirror wakes - wake_deficit = np.sqrt( - wake_deficit**2 + - gaussian_function(C_mirr, r_mirr, 1, np.sqrt(0.5))**2 - ) - - velocity_deficit = wake_deficit * downstream_mask - - return velocity_deficit - -def rCalt(wind_veer, sigma_y, sigma_z, y, y_i, delta_y, delta_z, z, HH, Ct, - yaw, tilt, D, sigma_y0, sigma_z0): - - ## Numexpr - wind_veer = np.deg2rad(wind_veer) - a = ne.evaluate( - "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" - ) - b = ne.evaluate( - "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" - ) - c = ne.evaluate( - "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" - ) - r = ne.evaluate( - "a * ( (y - y_i - delta_y) ** 2) - "+\ - "2 * b * (y - y_i - delta_y) * (z - HH - delta_z) + "+\ - "c * ((z - HH - delta_z) ** 2)" - ) - d = 1 - Ct * (sigma_y0 * sigma_z0)/(sigma_y * sigma_z) * cosd(yaw) * cosd(tilt) - C = ne.evaluate("1 - sqrt(d)") - return r, C - -def sigmoid_integral(x, center=0, width=1): - y = np.zeros_like(x) - # TODO: Can this be made faster? - above_smoothing_zone = (x-center) > width/2 - y[above_smoothing_zone] = (x-center)[above_smoothing_zone] - in_smoothing_zone = ((x-center) >= -width/2) & ((x-center) <= width/2) - z = ((x-center)/width + 0.5)[in_smoothing_zone] - if width.shape[0] > 1: # multiple turbine sizes - width = np.broadcast_to(width, x.shape)[in_smoothing_zone] - y[in_smoothing_zone] = (width*(z**6 - 3*z**5 + 5/2*z**4)).flatten() - return y - -def empirical_gauss_model_wake_width( - x, - wake_expansion_rates, - breakpoints, - sigma_0, - smoothing_length, - mixing_final, - ): - assert len(wake_expansion_rates) == len(breakpoints) + 1, \ - "Invalid combination of wake_expansion_rates and breakpoints." - - sigma = (wake_expansion_rates[0] + mixing_final) * x + sigma_0 - for ib, b in enumerate(breakpoints): - sigma += (wake_expansion_rates[ib+1] - wake_expansion_rates[ib]) * \ - sigmoid_integral(x, center=b, width=smoothing_length) - - return sigma - -def awc_added_wake_mixing( - awc_mode_i, - awc_amplitude_i, - awc_frequency_i, - awc_wake_exp, - awc_wake_denominator -): - # Drop surplus (grid) dimensions - awc_amplitude_i = awc_amplitude_i[:,:,0,0] - awc_mode_i = awc_mode_i[:,:,0,0] - - # TODO: Add TI in the mix, finetune amplitude/freq effect - awc_mixing_factor = np.zeros_like(awc_amplitude_i, dtype=floris_float_type) - helix_mask = awc_mode_i == 'helix' - - awc_mixing_factor[helix_mask] = ( - awc_amplitude_i[helix_mask]**awc_wake_exp/awc_wake_denominator - ) - - return awc_mixing_factor diff --git a/floris/core/wake_velocity/gauss.py b/floris/core/wake_velocity/gauss.py deleted file mode 100644 index 1aa2e2f957..0000000000 --- a/floris/core/wake_velocity/gauss.py +++ /dev/null @@ -1,237 +0,0 @@ - -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import ( - cosd, - sind, - tand, -) - - -@define -class GaussVelocityDeficit(BaseModel): - - alpha: float = field(default=0.58) - beta: float = field(default=0.077) - ka: float = field(default=0.38) - kb: float = field(default=0.004) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "u_initial": flow_field.u_initial_sorted, - "wind_veer": flow_field.wind_veer - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - axial_induction_i: np.ndarray, - deflection_field_i: np.ndarray, - yaw_angle_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - hub_height_i: float, - rotor_diameter_i: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - u_initial: np.ndarray, - wind_veer: float, - ) -> None: - - # yaw_angle is all turbine yaw angles for each wind speed - # Extract and broadcast only the current turbine yaw setting - # for all wind speeds - - # Opposite sign convention in this model - yaw_angle = -1 * yaw_angle_i - - # Initialize the velocity deficit - uR = u_initial * ct_i / (2.0 * (1 - np.sqrt(1 - ct_i))) - u0 = u_initial * np.sqrt(1 - ct_i) - - # Initial lateral bounds - sigma_z0 = rotor_diameter_i * 0.5 * np.sqrt(uR / (u_initial + u0)) - sigma_y0 = sigma_z0 * cosd(yaw_angle) * cosd(wind_veer) - - # Compute the bounds of the near and far wake regions and a mask - - # Start of the near wake - xR = x_i - - # Start of the far wake - x0 = np.ones_like(u_initial) - x0 *= rotor_diameter_i * cosd(yaw_angle) * (1 + np.sqrt(1 - ct_i) ) - x0 /= np.sqrt(2) * ( - 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i) ) - ) - x0 += x_i - - # Initialize the velocity deficit array - velocity_deficit = np.zeros_like(u_initial) - - # Masks - # When we have only an inequality, the current turbine may be applied its own - # wake in cases where numerical precision cause in incorrect comparison. We've - # applied a small bump to avoid this. "0.1" is arbitrary but it is a small, non - # zero value. - - # This mask defines the near wake; keeps the areas downstream of xR and upstream of x0 - near_wake_mask = (x > xR + 0.1) * (x < x0) - far_wake_mask = (x >= x0) - - # Compute the velocity deficit in the NEAR WAKE region - # ONLY If there are points within the near wake boundary - # TODO: for the TurbineGrid, do we need to do this near wake calculation at all? - # same question for any grid with a resolution larger than the near wake region - if np.sum(near_wake_mask): - - # Calculate the wake expansion - - # This is a linear ramp from 0 to 1 from the start of the near wake to the start - # of the far wake. - near_wake_ramp_up = (x - xR) / (x0 - xR) - # Another linear ramp, but positive upstream of the far wake and negative in the - # far wake; 0 at the start of the far wake - near_wake_ramp_down = (x0 - x) / (x0 - xR) - # near_wake_ramp_down = -1 * (near_wake_ramp_up - 1) # : this is equivalent, right? - - sigma_y = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt(ct_i / 2.0) - sigma_y += near_wake_ramp_up * sigma_y0 - sigma_y *= (x >= xR) - sigma_y += np.ones_like(sigma_y) * (x < xR) * 0.5 * rotor_diameter_i - - sigma_z = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt(ct_i / 2.0) - sigma_z += near_wake_ramp_up * sigma_z0 - sigma_z *= (x >= xR) - sigma_z += np.ones_like(sigma_z) * (x < xR) * 0.5 * rotor_diameter_i - - r_squared, C = rC( - wind_veer, - sigma_y, - sigma_z, - y, - y_i, - deflection_field_i, - z, - hub_height_i, - ct_i, - yaw_angle, - rotor_diameter_i, - ) - - near_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) - near_wake_deficit *= near_wake_mask - - velocity_deficit += near_wake_deficit - - # Compute the velocity deficit in the FAR WAKE region - if np.sum(far_wake_mask): - - # Wake expansion in the lateral (y) and the vertical (z) - ky = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters - kz = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters - sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * (x < x0) - sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * (x < x0) - - r_squared, C = rC( - wind_veer, - sigma_y, - sigma_z, - y, - y_i, - deflection_field_i, - z, - hub_height_i, - ct_i, - yaw_angle, - rotor_diameter_i, - ) - - far_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) - far_wake_deficit *= far_wake_mask - - velocity_deficit += far_wake_deficit - - return velocity_deficit - - -# @profile -def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): - - ## original - # a = cosd(wind_veer) ** 2 / (2 * sigma_y ** 2) + sind(wind_veer) ** 2 / (2 * sigma_z ** 2) - # b = -sind(2 * wind_veer) / (4 * sigma_y ** 2) + sind(2 * wind_veer) / (4 * sigma_z ** 2) - # c = sind(wind_veer) ** 2 / (2 * sigma_y ** 2) + cosd(wind_veer) ** 2 / (2 * sigma_z ** 2) - # r_squared = ( - # a * (y - y_i - delta) ** 2 - # - 2 * b * (y - y_i - delta) * (z - HH) - # + c * (z - HH) ** 2 - # ) - # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / D ** 2)), 0.0, 1.0)) - - ## Precalculate some parts - # twox_sigmay_2 = 2 * sigma_y ** 2 - # twox_sigmaz_2 = 2 * sigma_z ** 2 - # a = cosd(wind_veer) ** 2 / (twox_sigmay_2) + sind(wind_veer) ** 2 / (twox_sigmaz_2) - # b = -sind(2 * wind_veer) / (2 * twox_sigmay_2) + sind(2 * wind_veer) / (2 * twox_sigmaz_2) - # c = sind(wind_veer) ** 2 / (twox_sigmay_2) + cosd(wind_veer) ** 2 / (twox_sigmaz_2) - # delta_y = y - y_i - delta - # delta_z = z - HH - # r_squared = (a * (delta_y ** 2) - 2 * b * (delta_y) * (delta_z) + c * (delta_z ** 2)) - # C = 1 - np.sqrt(np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / (D * D))), 0.0, 1.0)) - - ## Numexpr - wind_veer = np.deg2rad(wind_veer) - a = ne.evaluate( - "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" - ) - b = ne.evaluate( - "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" - ) - c = ne.evaluate( - "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" - ) - r_squared = ne.evaluate( - "a * ((y - y_i - delta) ** 2) - 2 * b * (y - y_i - delta) * (z - HH) + c * ((z - HH) ** 2)" - ) - d = np.clip(1 - (Ct * cosd(yaw) / ( 8.0 * sigma_y * sigma_z / (D * D) )), 0.0, 1.0) - C = ne.evaluate("1 - sqrt(d)") - return r_squared, C - - -def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine_yaw): - yR = mesh_y_rotated - y_coord_rotated - xR = yR * tand(turbine_yaw) + x_coord_rotated - return xR, yR - - -def gaussian_function(C, r_squared, n, sigma): - result = ne.evaluate("C * exp(-1 * r_squared ** n / (2 * sigma ** 2))") - return result diff --git a/floris/core/wake_velocity/jensen.py b/floris/core/wake_velocity/jensen.py deleted file mode 100644 index 7d6b09c31a..0000000000 --- a/floris/core/wake_velocity/jensen.py +++ /dev/null @@ -1,127 +0,0 @@ - -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import ( - define, - field, - fields, -) - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) - - -NUM_EPS = fields(BaseModel).NUM_EPS.default - -@define -class JensenVelocityDeficit(BaseModel): - """ - The Jensen model computes the wake velocity deficit based on the classic - Jensen/Park model :cite:`jvm-jensen1983note`. - - - **we** (*float*): The linear wake decay constant that - defines the cone boundary for the wake as well as the - velocity deficit. D/2 +/- we*x is the cone boundary for the - wake. - - References: - .. bibliography:: /references.bib - :style: unsrt - :filter: docname in docnames - :keyprefix: jvm- - """ - - we: float = field(converter=float, default=0.05) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - """ - This function prepares the inputs from the various FLORIS data structures - for use in the Jensen model. This should only be used to 'initialize' - the inputs. For any data that should be updated successively, - do not use this function and instead pass that data directly to - the model function. - """ - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - axial_induction_i: np.ndarray, - deflection_field_i: np.ndarray, - yaw_angle_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - hub_height_i, - rotor_diameter_i, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - ) -> None: - - # u is 4-dimensional (n wind speeds, n turbines, grid res 1, grid res 2) - # velocities is 3-dimensional (n turbines, grid res 1, grid res 2) - - # TODO: check the rotations with multiple directions or non-0/270 - # grid.rotate_fields(flow_field.wind_directions) - - # Calculate and apply wake mask - # x = grid.x_sorted # mesh_x_rotated - x_coord_rotated - - # This is the velocity deficit seen by the i'th turbine due to wake effects - # from upstream turbines. - # Indeces of velocity_deficit corresponding to unwaked turbines will have 0's - # velocity_deficit = np.zeros(np.shape(flow_field.u_initial)) - - rotor_radius = rotor_diameter_i / 2.0 - - # Numexpr - do not change below without corresponding changes above. - dx = ne.evaluate("x - x_i") - dy = ne.evaluate("y - y_i - deflection_field_i") - dz = ne.evaluate("z - z_i") - - we = self.we - - # Construct a boolean mask to include all points downstream of the turbine - downstream_mask = ne.evaluate("dx > 0 + NUM_EPS") - - # Construct a boolean mask to include all points within the wake boundary - # as defined by the Jensen model. This is a linear wake expansion that makes - # a shape like a cone and starts at the turbine disc. - # The left side of the inequality below evaluates the distance from the wake centerline - # for all points including positive and negative values. The inequality compares distance - # from the centerline and it must be below the line defined by the wake - # expansion parameter, "we". - boundary_mask = ne.evaluate("sqrt(dy ** 2 + dz ** 2) < we * dx + rotor_radius") - - # Calculate C for points within the mask and fill points outside with 0 - c = np.where( - np.logical_and(downstream_mask, boundary_mask), - ne.evaluate("(rotor_radius / (rotor_radius + we * dx + NUM_EPS)) ** 2"), # This is "C" - 0.0, - ) - - velocity_deficit = ne.evaluate("2 * axial_induction_i * c") - - return velocity_deficit diff --git a/floris/core/wake_velocity/none.py b/floris/core/wake_velocity/none.py deleted file mode 100644 index af1ea448ac..0000000000 --- a/floris/core/wake_velocity/none.py +++ /dev/null @@ -1,50 +0,0 @@ - -from typing import Any, Dict - -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - FlowField, - Grid, -) - - -@define -class NoneVelocityDeficit(BaseModel): - """ - The None deficit model is a placeholder code that simple ignores any - wake wind speed deficits and returns an array of zeroes. - """ - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "u_initial": flow_field.u_initial_sorted, - } - return kwargs - - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - axial_induction_i: np.ndarray, - deflection_field_i: np.ndarray, - yaw_angle_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - hub_height_i: float, - rotor_diameter_i: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - u_initial: np.ndarray, - ) -> None: - self.logger.warning("The wake deficit model is set to 'none'. Wake modeling disabled.") - return np.zeros_like(u_initial) diff --git a/floris/core/wake_velocity/turbopark.py b/floris/core/wake_velocity/turbopark.py deleted file mode 100644 index 63ad6e06c4..0000000000 --- a/floris/core/wake_velocity/turbopark.py +++ /dev/null @@ -1,181 +0,0 @@ - - -from pathlib import Path -from typing import Any, Dict - -import numpy as np -import scipy.io -from attrs import define, field -from scipy import integrate -from scipy.interpolate import RegularGridInterpolator - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.utilities import ( - cosd, - sind, - tand, -) - - -@define -class TurbOParkVelocityDeficit(BaseModel): - """ - Model based on the TurbOPark model. For model details see - https://github.com/OrstedRD/TurbOPark, - https://github.com/OrstedRD/TurbOPark/blob/main/TurbOPark%20description.pdf, and - Nygaard, Nicolai Gayle, et al. "Modelling cluster wakes and wind farm blockage." - Journal of Physics: Conference Series. Vol. 1618. No. 6. IOP Publishing, 2020. - """ - - A: float = field(default=0.04) - sigma_max_rel: float = field(default=4.0) - overlap_gauss_interp: RegularGridInterpolator = field(init=False) - - def __attrs_post_init__(self) -> None: - lookup_table_matlab_file = Path(__file__).parent / "turbopark_lookup_table.mat" - lookup_table_file = scipy.io.loadmat(lookup_table_matlab_file) - dist = lookup_table_file['overlap_lookup_table'][0][0][0][0] - radius_down = lookup_table_file['overlap_lookup_table'][0][0][1][0] - overlap_gauss = lookup_table_file['overlap_lookup_table'][0][0][2] - self.overlap_gauss_interp = RegularGridInterpolator( - (dist, radius_down), - overlap_gauss, - method='linear', - bounds_error=False - ) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "u_initial": flow_field.u_initial_sorted, - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - ambient_turbulence_intensities: np.ndarray, - Cts: np.ndarray, - rotor_diameter_i: np.ndarray, - rotor_diameters: np.ndarray, - i: int, - deflection_field: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - u_initial: np.ndarray, - ) -> None: - delta_total = np.zeros_like(u_initial) - - # Normalized distances along x between the turbine i and all other turbines - # The downstream_mask is used to avoid negative numbers in the sqrt and the - # subsequent runtime warnings. - # Here self.NUM_EPS is to avoid precision issues with masking, and is slightly - # larger than 0.0 - downstream_mask = (x_i - x >= self.NUM_EPS) - x_dist = (x_i - x) * downstream_mask / rotor_diameters - - # Radial distance between turbine i and the center lines of wakes from all - # real/image turbines - r_dist = np.sqrt((y_i - (y + deflection_field)) ** 2 + (z_i - z) ** 2) - r_dist_image = np.sqrt((y_i - (y + deflection_field)) ** 2 + (z_i - (-z)) ** 2) - - Cts[:, i:, :, :] = 0.00001 - - # Characteristic wake widths from all turbines relative to turbine i - dw = characteristic_wake_width(x_dist, ambient_turbulence_intensities, Cts, self.A) - epsilon = 0.25 * np.sqrt( - np.min( 0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3, keepdims=True ) - ) - sigma = rotor_diameters * (epsilon + dw) - - # Peak wake deficits - val = 1 - Cts / (8 * (sigma / rotor_diameters) ** 2) - C = 1 - np.sqrt(val) - - # Compute deficit for all turbines and mask to keep upstream and overlapping turbines - # NOTE self.sigma_max_rel * sigma is an effective wake width - is_overlapping = (self.sigma_max_rel * sigma) / 2 + rotor_diameter_i / 2 > r_dist - wtg_overlapping = (x_dist > 0) * is_overlapping - - delta_real = np.empty(np.shape(u_initial)) * np.nan - delta_image = np.empty(np.shape(u_initial)) * np.nan - - # Compute deficits for real turbines and for mirrored (image) turbines - delta_real = C * wtg_overlapping * self.overlap_gauss_interp( - (r_dist / sigma, rotor_diameter_i / 2 / sigma) - ) - delta_image = C * wtg_overlapping * self.overlap_gauss_interp( - (r_dist_image / sigma, rotor_diameter_i / 2 / sigma) - ) - delta = np.concatenate((delta_real, delta_image), axis=1) - - delta_total[:, i, :, :] = np.sqrt(np.sum(np.nan_to_num(delta) ** 2, axis=1)) - - return delta_total - - -def precalculate_overlap(): - # TODO: first implementation to generate wake overlap lookup table - # (currently supplied by turbopark_lookup_table.mat.) - # However, the result of this function doesn't generate the same - # interpolant as the .mat file, so if used, needs to be corrected. - dist = np.arange(0, 10, 1.0) - radius_down = np.arange(0, 20, 1.0) - overlap_gauss = np.zeros((len(dist), len(radius_down))) - - for i in range(len(dist)): - for j in range(len(radius_down)): - if radius_down[j] > 0: - def fun(r, theta): - return r * np.exp( - -1 * (r ** 2 + dist[i] ** 2 - 2 * dist[i] * r * np.cos(theta)) / 2 - ) - out = integrate.dblquad(fun, 0, radius_down[j], lambda x: 0, lambda x: 2 * np.pi)[0] - out = out / (np.pi * radius_down[j] ** 2) - else: - out = np.exp(-(dist[i] ** 2) / 2) - overlap_gauss[i, j] = out - - return dist, radius_down, overlap_gauss - - -def characteristic_wake_width(x_dist, TI, Cts, A): - # Parameter values taken from S. T. Frandsen, “Risø-R-1188(EN) Turbulence - # and turbulence generated structural loading in wind turbine clusters” - # Risø, Roskilde, Denmark, 2007. - c1 = 1.5 - c2 = 0.8 - - alpha = TI * c1 - beta = c2 * TI / np.sqrt(Cts) - - dw = A * TI / beta * ( - np.sqrt((alpha + beta * x_dist) ** 2 + 1) - - np.sqrt(1 + alpha ** 2) - - np.log( - ((np.sqrt((alpha + beta * x_dist) ** 2 + 1) + 1) * alpha) - / ((np.sqrt(1 + alpha ** 2) + 1) * (alpha + beta * x_dist)) - ) - ) - - return dw diff --git a/floris/core/wake_velocity/turbopark_lookup_table.mat b/floris/core/wake_velocity/turbopark_lookup_table.mat deleted file mode 100644 index c1cb23c092..0000000000 Binary files a/floris/core/wake_velocity/turbopark_lookup_table.mat and /dev/null differ diff --git a/floris/core/wake_velocity/turboparkgauss.py b/floris/core/wake_velocity/turboparkgauss.py deleted file mode 100644 index 8656979244..0000000000 --- a/floris/core/wake_velocity/turboparkgauss.py +++ /dev/null @@ -1,130 +0,0 @@ -from typing import Any, Dict - -import numexpr as ne -import numpy as np -from attrs import define, field - -from floris.core import ( - BaseModel, - Farm, - FlowField, - Grid, - Turbine, -) -from floris.core.wake_velocity.gauss import gaussian_function -from floris.utilities import ( - cosd, - sind, - tand, -) - - -@define -class TurboparkgaussVelocityDeficit(BaseModel): - """ - Model based on the TurbOPark model with Gaussian wake profile. - For model details see: - Pedersen J G, Svensen E, Poulsen L, and Nygaard N G. "Turbulence Optimized - Park model with Gaussian wake profile." Journal of Physics: Conference - Series. Vol. 2265. No. 022063. IOP Publishing, 2020. - doi:10.1088/1742-6596/2265/2/022063 - """ - - A: float = field(default=0.04) - include_mirror_wake: bool = field(default=True) - - def prepare_function( - self, - grid: Grid, - flow_field: FlowField, - ) -> Dict[str, Any]: - - kwargs = { - "x": grid.x_sorted, - "y": grid.y_sorted, - "z": grid.z_sorted, - "u_initial": flow_field.u_initial_sorted, - "wind_veer": flow_field.wind_veer - } - return kwargs - - # @profile - def function( - self, - x_i: np.ndarray, - y_i: np.ndarray, - z_i: np.ndarray, - axial_induction_i: np.ndarray, - deflection_field_i: np.ndarray, - yaw_angle_i: np.ndarray, - turbulence_intensity_i: np.ndarray, - ct_i: np.ndarray, - hub_height_i: float, - rotor_diameter_i: np.ndarray, - # enforces the use of the below as keyword arguments and adherence to the - # unpacking of the results from prepare_function() - *, - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - u_initial: np.ndarray, - wind_veer: float, - ) -> None: - - # Initialize the velocity deficit array - velocity_deficit = np.zeros_like(u_initial) - - downstream_mask = (x - x_i >= self.NUM_EPS) - x_dist = (x - x_i) * downstream_mask / rotor_diameter_i - - # Characteristic wake widths from all turbines relative to turbine i - sigma = characteristic_wake_width( - x_dist, turbulence_intensity_i, ct_i, self.A - ) * rotor_diameter_i - - # Peak wake deficits - C = 1 - np.sqrt(np.clip(1 - ct_i / (8 * (sigma / rotor_diameter_i) ** 2), 0.0, 1.0)) - - r_dist = np.sqrt((y - y_i) ** 2 + (z - z_i) ** 2) - - # Compute deficits for real turbines and for mirrored (image) turbines - delta_real = (x_dist > 0) * gaussian_function(C, r_dist, 2, sigma) - if self.include_mirror_wake: - r_dist_image = np.sqrt((y - y_i) ** 2 + (z - 3*z_i) ** 2) - delta_image = (x_dist > 0) * gaussian_function(C, r_dist_image, 2, sigma) - delta = np.hypot(delta_real, delta_image) - else: # No mirror wakes - delta = delta_real - - velocity_deficit = np.nan_to_num(delta) - - return velocity_deficit - - -def characteristic_wake_width(x_D, ambient_TI, Cts, A): - # Parameter values taken from S. T. Frandsen, “Risø-R-1188(EN) Turbulence - # and turbulence generated structural loading in wind turbine clusters” - # Risø, Roskilde, Denmark, 2007. - c1 = 1.5 - c2 = 0.8 - - alpha = ambient_TI * c1 - beta = c2 * ambient_TI / np.sqrt(Cts) - - # Term for the initial width at the turbine location (denoted epsilon in Pedersen et al.) - # Saturate term in initial width to 3.0, as is done in Orsted Matlab code. - initial_width = 0.25 * np.sqrt(np.minimum(0.5 * (1 + np.sqrt(1 - Cts)) / np.sqrt(1 - Cts), 3.0)) - - # Term for the added width downstream of the turbine - added_width = A * ambient_TI / beta * ( - np.sqrt((alpha + beta * x_D) ** 2 + 1) - - np.sqrt(1 + alpha ** 2) - - np.log( - ((np.sqrt((alpha + beta * x_D) ** 2 + 1) + 1) * alpha) - / ((np.sqrt(1 + alpha ** 2) + 1) * (alpha + beta * x_D)) - ) - ) - - sigma_w_D = initial_width + added_width - - return sigma_w_D diff --git a/floris/default_inputs.yaml b/floris/default_inputs.yaml index 67e1995595..fed5e09d4e 100644 --- a/floris/default_inputs.yaml +++ b/floris/default_inputs.yaml @@ -33,74 +33,20 @@ flow_field: wind_veer: 0.0 wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_transverse_velocities: true - enable_active_wake_mixing: false - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - empirical_gauss: - horizontal_deflection_gain_D: 3.0 - vertical_deflection_gain_D: -1 - deflection_rate: 22 - mixing_gain_deflection: 0.0 - yaw_added_mixing_gain: 0.0 - - wake_velocity_parameters: - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - turbopark: - A: 0.04 - sigma_max_rel: 4.0 - turboparkgauss: - A: 0.04 - include_mirror_wake: True - empirical_gauss: - wake_expansion_rates: [0.023, 0.008] - breakpoints_D: [10] - sigma_0_D: 0.28 - smoothing_length_D: 2.0 - mixing_gain_velocity: 2.0 - awc_wake_exp: 1.2 - awc_wake_denominator: 400 - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 - wake_induced_mixing: - atmospheric_ti_gain: 0.0 + model: gauss + parameters: + enable_secondary_steering: true + enable_yaw_added_recovery: true + enable_transverse_velocities: true + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 + combination_model: sosfs diff --git a/floris/floris_model.py b/floris/floris_model.py index b475303c33..e612d6a575 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -12,6 +12,7 @@ from floris.core import Core, State from floris.core.rotor_velocity import average_velocity +from floris.core.turbine import BaseOperationModel from floris.core.turbine.operation_models import ( POWER_SETPOINT_DEFAULT, POWER_SETPOINT_DISABLED, @@ -21,6 +22,7 @@ power, thrust_coefficient, ) +from floris.core.wake_model import BaseWakeModel from floris.cut_plane import CutPlane from floris.logging_manager import LoggingManager from floris.type_dec import ( @@ -199,7 +201,7 @@ def _reinitialize( ) farm_dict["turbine_type"] = turbine_type if turbine_library_path is not None: - farm_dict["turbine_library_path"] = turbine_library_path + farm_dict["external_turbine_library_path"] = turbine_library_path ## If layout is changed and self._wind_data is not None, update the layout in wind_data if (layout_x is not None) or (layout_y is not None): @@ -516,7 +518,7 @@ def run(self) -> None: self.core.initialize_domain() # Perform the wake calculations - self.core.steady_state_atmospheric_condition() + self.core.solve_for_turbines() def run_no_wake(self) -> None: """ @@ -528,6 +530,17 @@ def run_no_wake(self) -> None: # Initialize solution space self.core.initialize_domain() + # Evaluate turbine quantities without wake effects + self.core.wake.model.evaluate_turbine_thrust_coefficient( + self.core.grid, self.core.farm, self.core.flow_field + ) + self.core.wake.model.evaluate_turbine_axial_induction( + self.core.grid, self.core.farm, self.core.flow_field + ) + self.core.wake.model.evaluate_turbine_power( + self.core.grid, self.core.farm, self.core.flow_field + ) + # Finalize values to user-supplied order self.core.finalize() @@ -551,23 +564,7 @@ def _get_turbine_powers(self) -> NDArrayFloat: if (self.core.flow_field.u < 0.0).any(): self.logger.warning("Some velocities at the rotor are negative.") - turbine_powers = power( - velocities=self.core.flow_field.u, - turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], - air_density=self.core.flow_field.air_density, - power_functions=self.core.farm.turbine_power_functions, - yaw_angles=self.core.farm.yaw_angles, - tilt_angles=self.core.farm.tilt_angles, - power_setpoints=self.core.farm.power_setpoints, - awc_modes = self.core.farm.awc_modes, - awc_amplitudes=self.core.farm.awc_amplitudes, - tilt_interps=self.core.farm.turbine_tilt_interps, - turbine_type_map=self.core.farm.turbine_type_map, - turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, - correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, - multidim_condition=self.core.flow_field.multidim_conditions, - ) - return turbine_powers + return self.core.farm.turbine_powers def get_turbine_powers(self): @@ -1014,47 +1011,11 @@ def get_farm_AVP( turbine_weights=turbine_weights ) * hours_per_year - def get_turbine_ais(self) -> NDArrayFloat: - turbine_ais = axial_induction( - velocities=self.core.flow_field.u, - turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], - air_density=self.core.flow_field.air_density, - yaw_angles=self.core.farm.yaw_angles, - tilt_angles=self.core.farm.tilt_angles, - power_setpoints=self.core.farm.power_setpoints, - awc_modes = self.core.farm.awc_modes, - awc_amplitudes=self.core.farm.awc_amplitudes, - axial_induction_functions=self.core.farm.turbine_axial_induction_functions, - tilt_interps=self.core.farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, - turbine_type_map=self.core.farm.turbine_type_map, - turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, - average_method=self.core.grid.average_method, - cubature_weights=self.core.grid.cubature_weights, - multidim_condition=self.core.flow_field.multidim_conditions, - ) - return turbine_ais + def get_turbine_axial_induction_factors(self) -> NDArrayFloat: + return self.core.farm.turbine_axial_inductions def get_turbine_thrust_coefficients(self) -> NDArrayFloat: - turbine_thrust_coefficients = thrust_coefficient( - velocities=self.core.flow_field.u, - turbulence_intensities=self.core.flow_field.turbulence_intensity_field[:,:,None,None], - air_density=self.core.flow_field.air_density, - yaw_angles=self.core.farm.yaw_angles, - tilt_angles=self.core.farm.tilt_angles, - power_setpoints=self.core.farm.power_setpoints, - awc_modes = self.core.farm.awc_modes, - awc_amplitudes=self.core.farm.awc_amplitudes, - thrust_coefficient_functions=self.core.farm.turbine_thrust_coefficient_functions, - tilt_interps=self.core.farm.turbine_tilt_interps, - correct_cp_ct_for_tilt=self.core.farm.correct_cp_ct_for_tilt, - turbine_type_map=self.core.farm.turbine_type_map, - turbine_power_thrust_tables=self.core.farm.turbine_power_thrust_tables, - average_method=self.core.grid.average_method, - cubature_weights=self.core.grid.cubature_weights, - multidim_condition=self.core.flow_field.multidim_conditions, - ) - return turbine_thrust_coefficients + return self.core.farm.turbine_thrust_coefficients def get_turbine_TIs(self) -> NDArrayFloat: return self.core.flow_field.turbulence_intensity_field @@ -1562,34 +1523,35 @@ def assign_hub_height_to_ref_height(self): self.core.flow_field.reference_wind_height = unique_heights[0] - def get_operation_model(self) -> str: + def get_operation_model(self) -> list[BaseOperationModel]: """Get the operation model of a FlorisModel. Returns: - str: The operation_model. - """ - operation_models = [ - self.core.farm.turbine_definitions[tindex]["operation_model"] - for tindex in range(self.core.farm.n_turbines) - ] - if len(set(operation_models)) == 1: - return operation_models[0] - else: - return operation_models + list[BaseOperationModel]: The operation_model instance for each turbine. + """ + return [t.operation_model for t in self.core.farm.turbines] - def set_operation_model(self, operation_model: str | List[str]): + def set_operation_model( + self, + operation_model: BaseOperationModel | str | List[BaseOperationModel | str] + ): """Set the turbine operation model(s). + Can be provided either as a string representing one of the built-in operation + models, or as a custom operation model object that inherits from + :py:class:`~.turbine_operation.BaseOperationModel`. Also, a list of operation + models can be provided to set different operation models for each turbine. + Args: - operation_model (str): The operation model to set. + operation_model (str, BaseOperationModel, list): The operation model to set. """ - if isinstance(operation_model, str): + if (not isinstance(operation_model, (list, np.ndarray))): if len(self.core.farm.turbine_type) == 1: # Set a single one here, then, and return - turbine_type = self.core.farm.turbine_definitions[0] - turbine_type["operation_model"] = operation_model + turbine_dict = self.core.farm.turbines[0].as_dict() + turbine_dict["operation_model"] = operation_model self.set( - turbine_type=[turbine_type], + turbine_type=[turbine_dict], reference_wind_height=self.reference_wind_height ) return @@ -1602,19 +1564,30 @@ def set_operation_model(self, operation_model: str | List[str]): "equal to the number of turbines." ) - turbine_type_list = self.core.farm.turbine_definitions + # Proceed to update turbine definitions + turbine_dicts = [t.as_dict() for t in self.core.farm.turbines] for tindex in range(self.core.farm.n_turbines): - turbine_type_list[tindex]["turbine_type"] = ( - turbine_type_list[tindex]["turbine_type"]+"_"+operation_model[tindex] + turbine_dicts[tindex]["turbine_type"] = ( + turbine_dicts[tindex]["turbine_type"]+"_"+str(operation_model[tindex]) ) - turbine_type_list[tindex]["operation_model"] = operation_model[tindex] + turbine_dicts[tindex]["operation_model"] = operation_model[tindex] self.set( - turbine_type=turbine_type_list, + turbine_type=turbine_dicts, reference_wind_height=self.reference_wind_height ) + def set_wake_model(self, wake_model: BaseWakeModel): + """Set the wake model. + + Args: + wake_model (BaseWakeModel): The wake model to set. + """ + self.core.wake.assign_user_defined_wake_model(wake_model) + # Run top-level reinitialization routine + self.set() + def copy(self): """Create an independent copy of the current FlorisModel object @@ -1624,15 +1597,15 @@ def copy(self): """ return self.__class__(self.core.as_dict(), **self.secondary_init_kwargs) - def get_param( + def get_wake_parameter( self, - param: List[str], - param_idx: Optional[int] = None + parameter: str, + parameter_idx: Optional[int] = None ) -> Any: """Get a parameter from a FlorisModel object. Args: - param (List[str]): A list of keys to traverse the FlorisModel dictionary. + parameter (str): The name of the wake parameter to get. param_idx (Optional[int], optional): The index to get the value at. Defaults to None. If None, the entire parameter is returned. @@ -1641,26 +1614,26 @@ def get_param( """ fm_dict = self.core.as_dict() - if param_idx is None: - return nested_get(fm_dict, param) + if parameter_idx is None: + return nested_get(fm_dict, ["wake", "parameters", parameter]) else: - return nested_get(fm_dict, param)[param_idx] + return nested_get(fm_dict, ["wake", "parameters", parameter])[parameter_idx] - def set_param( + def set_wake_parameter( self, - param: List[str], + parameter: str, value: Any, - param_idx: Optional[int] = None + parameter_idx: Optional[int] = None ): """Set a parameter in a FlorisModel object. Args: - param (List[str]): A list of keys to traverse the FlorisModel dictionary. + parameter (str): The name of the wake parameter to set. value (Any): The value to set. param_idx (Optional[int], optional): The index to set the value at. Defaults to None. """ fm_dict_mod = self.core.as_dict() - nested_set(fm_dict_mod, param, value, param_idx) + nested_set(fm_dict_mod, ["wake", "parameters", parameter], value, parameter_idx) self.__init__(fm_dict_mod, **self.secondary_init_kwargs) def get_turbine_layout(self, z=False): diff --git a/floris/layout_visualization.py b/floris/layout_visualization.py index 876c6474e7..e267b62fe7 100644 --- a/floris/layout_visualization.py +++ b/floris/layout_visualization.py @@ -428,13 +428,11 @@ def plot_waking_directions( } wake_plotting_dict = {**def_wake_plotting_dict, **wake_plotting_dict} - # N_turbs = len(fmodel.core.farm.turbine_definitions) - if D is None: - D = fmodel.core.farm.turbine_definitions[0]["rotor_diameter"] + D = fmodel.core.farm.turbines[0].rotor_diameter # TODO: build out capability to use multiple diameters, if of interest. - # D = np.array([turb['rotor_diameter'] for turb in - # fmodel.core.farm.turbine_definitions]) + # D = np.array([t.rotor_diameter for t in + # fmodel.core.farm.turbines]) # else: # D = D*np.ones(N_turbs) diff --git a/floris/optimization/load_optimization/load_optimization.py b/floris/optimization/load_optimization/load_optimization.py index e4979ae780..13a04c021d 100644 --- a/floris/optimization/load_optimization/load_optimization.py +++ b/floris/optimization/load_optimization/load_optimization.py @@ -5,8 +5,10 @@ from floris import FlorisModel from floris.core import State from floris.core.turbine.operation_models import ( + MixedOperationTurbine, POWER_SETPOINT_DEFAULT, POWER_SETPOINT_DISABLED, + SimpleDeratingTurbine, ) @@ -465,23 +467,19 @@ def optimize_power_setpoints( # Ensure we're in an operation model which includes derating # presently this can be "mixed" or "simple-derating" - if fmodel.get_operation_model() not in ["mixed", "simple-derating"]: + valid_op_models = (SimpleDeratingTurbine, MixedOperationTurbine) + if not all(isinstance(m, valid_op_models) for m in fmodel.get_operation_model()): raise ValueError( "Operation model must include derating (e.g., 'mixed' or 'simple-derating')" ) # Raise an error if there is more than one turbine type specified - if not np.array( - [ - fmodel.core.farm.turbine_definitions[0] == td - for td in fmodel.core.farm.turbine_definitions - ] - ).all(): + if not all(fmodel.core.farm.turbines[0] == t for t in fmodel.core.farm.turbines): raise NotImplementedError("Only one turbine type is currently supported for optimization") # If initial set point not provided, set to rated (assumed max) power if power_setpoint_initial is None: - max_power = fmodel.core.farm.turbine_map[0].power_thrust_table["power"].max() * 1000.0 + max_power = fmodel.core.farm.turbines[0].power_thrust_table["power"].max() * 1000.0 power_setpoint_initial = np.tile(max_power, (fmodel.n_findex, 1)) # Initialize the test power setpoints diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py index 042683a1f9..3d3af9d8a4 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_geometric.py @@ -53,7 +53,7 @@ def optimize(self): self.fmodel_subset.layout_x[active_turbines[nwdi]], self.fmodel_subset.layout_y[active_turbines[nwdi]], wd, - self.fmodel.core.farm.turbine_definitions[0]["rotor_diameter"], + self.fmodel.core.farm.turbines[0].rotor_diameter, top_left_yaw_upper=self.maximum_yaw_angle[0, 0], bottom_left_yaw_upper=self.maximum_yaw_angle[0, 0], top_left_yaw_lower=self.minimum_yaw_angle[0, 0], diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py index 810144c507..56e797dcc7 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_scipy.py @@ -2,6 +2,8 @@ import numpy as np from scipy.optimize import minimize +from floris.core.turbine.operation_models import CosineLossTurbine + from .yaw_optimization_base import YawOptimization @@ -30,11 +32,11 @@ def __init__( Instantiate YawOptimizationScipy object with a FlorisModel object and assign parameter values. """ - valid_op_models = ["cosine-loss"] - if fmodel.get_operation_model() not in valid_op_models: + valid_op_models = (CosineLossTurbine,) + if not all(isinstance(m, valid_op_models) for m in fmodel.get_operation_model()): raise ValueError( "YawOptimizationScipy is currently limited to the following operation models: " - + ", ".join(valid_op_models) + + ", ".join([m.__name__ for m in valid_op_models]) ) if opt_options is None: # Default SciPy parameters diff --git a/floris/par_floris_model.py b/floris/par_floris_model.py index a13b9b8cfb..7455192096 100644 --- a/floris/par_floris_model.py +++ b/floris/par_floris_model.py @@ -7,6 +7,7 @@ from floris.core import State from floris.floris_model import FlorisModel from floris.type_dec import ( + floris_float_type, NDArrayFloat, ) from floris.utilities import is_all_scalar_dict @@ -165,7 +166,7 @@ def run(self) -> None: self._fmodels_split = list(self._fmodels_split) t2 = timerpc() self._postprocessing() - self.core.farm.finalize(self.core.grid.unsorted_indices) + self.core.farm.finalize() self.core.state = State.USED t3 = timerpc() self._print_timings(t0, t1, t2, t3) @@ -351,34 +352,73 @@ def _postprocessing(self): if self.return_turbine_powers_only: self._stored_turbine_powers = np.vstack(self._turbine_powers_split) else: - # Ensure fields to set have correct dimensions - self.core.flow_field.u = self._fmodels_split[0].core.flow_field.u - self.core.flow_field.v = self._fmodels_split[0].core.flow_field.v - self.core.flow_field.w = self._fmodels_split[0].core.flow_field.w - self.core.flow_field.turbulence_intensity_field = \ - self._fmodels_split[0].core.flow_field.turbulence_intensity_field - - for fm in self._fmodels_split[1:]: - self.core.flow_field.u = np.append( - self.core.flow_field.u, + # Reconstruct full flow_field object + g_1, g_2 = self._fmodels_split[0].core.flow_field.u.shape[2:] + u_temp = np.empty((0, self.n_turbines, g_1, g_2), floris_float_type) + v_temp = np.empty((0, self.n_turbines, g_1, g_2), floris_float_type) + w_temp = np.empty((0, self.n_turbines, g_1, g_2), floris_float_type) + ti_temp = np.empty((0, self.n_turbines), floris_float_type) + + for fm in self._fmodels_split: + u_temp = np.append( + u_temp, fm.core.flow_field.u, axis=0 ) - self.core.flow_field.v = np.append( - self.core.flow_field.v, + v_temp = np.append( + v_temp, fm.core.flow_field.v, axis=0 ) - self.core.flow_field.w = np.append( - self.core.flow_field.w, + w_temp = np.append( + w_temp, fm.core.flow_field.w, axis=0 ) - self.core.flow_field.turbulence_intensity_field = np.append( - self.core.flow_field.turbulence_intensity_field, + ti_temp = np.append( + ti_temp, fm.core.flow_field.turbulence_intensity_field, axis=0 ) + self.core.flow_field.u = u_temp + self.core.flow_field.v = v_temp + self.core.flow_field.w = w_temp + self.core.flow_field.turbulence_intensity_field = ti_temp + + # Reconstruct full farm object + powers_temp = np.empty((0, self.n_turbines), floris_float_type) + cts_temp = np.empty((0, self.n_turbines), floris_float_type) + ais_temp = np.empty((0, self.n_turbines), floris_float_type) + ravs_temp = np.empty((0, self.n_turbines), floris_float_type) + + for fm in self._fmodels_split: + powers_temp = np.append( + powers_temp, + fm.core.farm.turbine_powers, + axis=0 + ) + cts_temp = np.append( + cts_temp, + fm.core.farm.turbine_thrust_coefficients, + axis=0 + ) + ais_temp = np.append( + ais_temp, + fm.core.farm.turbine_axial_inductions, + axis=0 + ) + ravs_temp = np.append( + ravs_temp, + fm.core.farm.turbine_rotor_average_velocities, + axis=0 + ) + + self.core.farm.set_turbine_outputs_by_original_ordering( + powers=powers_temp, + thrust_coefficients=cts_temp, + axial_inductions=ais_temp, + rotor_average_velocities=ravs_temp + ) def _print_timings(self, t0, t1, t2, t3): """ diff --git a/floris/turbine_library/__init__.py b/floris/turbine_library/__init__.py index 42e1962f3f..dacd3f8495 100644 --- a/floris/turbine_library/__init__.py +++ b/floris/turbine_library/__init__.py @@ -1,4 +1,3 @@ -from floris.turbine_library.turbine_previewer import TurbineInterface, TurbineLibrary from floris.turbine_library.turbine_utilities import ( build_cosine_loss_turbine_dict, check_smooth_power_curve, diff --git a/floris/turbine_library/turbine_previewer.py b/floris/turbine_library/turbine_previewer.py deleted file mode 100644 index 9b59e179fe..0000000000 --- a/floris/turbine_library/turbine_previewer.py +++ /dev/null @@ -1,846 +0,0 @@ -from pathlib import Path - -import attrs -import matplotlib.pyplot as plt -import numpy as np -from attrs import define, field - -from floris.core.turbine.operation_models import POWER_SETPOINT_DEFAULT -from floris.core.turbine.turbine import ( - power, - thrust_coefficient, - Turbine, -) -from floris.type_dec import convert_to_path, NDArrayFloat -from floris.utilities import ( - load_yaml, - round_nearest, - round_nearest_2_or_5, -) - - -INTERNAL_LIBRARY = Path(__file__).parent -DEFAULT_WIND_SPEEDS = np.linspace(0, 40, 81) - -DEPRECATION_MESSAGE = ( - "The TurbineInterface and TurbineLibrary classes are now deprecated as will be removed in a", - " future FLORIS release." -) - - -@define(auto_attribs=True) -class TurbineInterface: - turbine: Turbine = field(validator=attrs.validators.instance_of(Turbine)) - - @classmethod - def from_library(cls, library_path: str | Path, file_name: str): - """Loads the turbine definition from a YAML configuration file located in either the - internal turbine library ``floris/floris/turbine_library/``, or a user-specified location. - - Args: - library_path (:obj:`str` | :obj:`pathlib.Path`): The location of the turbine library; - use "internal" to use the FLORIS-provided library. - file_name (:obj:`str` | :obj:`pathlib.Path`): The name of the configuration file. - - Returns: - (TurbineInterface): Creates a new ``TurbineInterface`` object. - """ - print(DEPRECATION_MESSAGE) - # Use the pre-mapped internal turbine library or validate the user's library - if library_path == "internal": - library_path = INTERNAL_LIBRARY - else: - library_path = convert_to_path(library_path) - - # Add in the library specification if needed, and load from dict - turb_dict = load_yaml(library_path / file_name) - return cls(turbine=Turbine.from_dict(turb_dict)) - - @classmethod - def from_yaml(cls, file_path: str | Path): - """Loads the turbine definition from a YAML configuration file. - - Args: - file_path : str | Path - The full path and file name of the turbine configuration file. - - Returns: - (TurbineInterface): Creates a new ``TurbineInterface`` object. - """ - print(DEPRECATION_MESSAGE) - file_path = Path(file_path).resolve() - - # Add in the library specification if needed, and load from dict - turb_dict = load_yaml(file_path) - return cls(turbine=Turbine.from_dict(turb_dict)) - - @classmethod - def from_turbine_dict(cls, config_dict: dict): - """Loads the turbine definition from a dictionary. - - Args: - config_dict : dict - The ``Turbine`` configuration dictionary. - - Returns: - (`TurbineInterface`): Returns a ``TurbineInterface`` object. - """ - print(DEPRECATION_MESSAGE) - return cls(turbine=Turbine.from_dict(config_dict)) - - def power_curve( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - ) -> tuple[NDArrayFloat, NDArrayFloat] | tuple[NDArrayFloat, dict[tuple, NDArrayFloat]]: - """Produces a plot-ready power curve for the turbine for wind speed vs power (MW), assuming - no tilt or yaw effects. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - - Returns: - (tuple[NDArrayFloat, NDArrayFloat] | tuple[NDArrayFloat, dict[tuple, NDArrayFloat]]): - Returns the wind speed array and the power array, or the wind speed array and a - dictionary of the multidimensional parameters and their associated power arrays. - """ - shape = (wind_speeds.size, 1) - if self.turbine.multi_dimensional_cp_ct: - power_mw = { - k: power( - velocities=wind_speeds.reshape(shape), - turbulence_intensities=np.zeros(shape), - air_density=np.full(shape, v["ref_air_density"]), - power_functions={self.turbine.turbine_type: self.turbine.power_function}, - yaw_angles=np.zeros(shape), - tilt_angles=np.full(shape, v["ref_tilt"]), - power_setpoints=np.full(shape, POWER_SETPOINT_DEFAULT), - awc_modes=np.full(shape, ["baseline"]), - awc_amplitudes=np.zeros(shape), - tilt_interps={self.turbine.turbine_type: self.turbine.tilt_interp}, - turbine_type_map=np.full(shape, self.turbine.turbine_type), - turbine_power_thrust_tables={self.turbine.turbine_type: v}, - ).flatten() / 1e6 - for k,v in self.turbine.power_thrust_table.items() - } - else: - power_mw = power( - velocities=wind_speeds.reshape(shape), - turbulence_intensities=np.zeros(shape), - air_density=np.full(shape, self.turbine.power_thrust_table["ref_air_density"]), - power_functions={self.turbine.turbine_type: self.turbine.power_function}, - yaw_angles=np.zeros(shape), - tilt_angles=np.full(shape, self.turbine.power_thrust_table["ref_tilt"]), - power_setpoints=np.full(shape, POWER_SETPOINT_DEFAULT), - awc_modes=np.full(shape, ["baseline"]), - awc_amplitudes=np.zeros(shape), - tilt_interps={self.turbine.turbine_type: self.turbine.tilt_interp}, - turbine_type_map=np.full(shape, self.turbine.turbine_type), - turbine_power_thrust_tables={ - self.turbine.turbine_type: self.turbine.power_thrust_table - }, - ).flatten() / 1e6 - return wind_speeds, power_mw - - def thrust_coefficient_curve( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - ) -> tuple[NDArrayFloat, NDArrayFloat]: - """Produces a plot-ready thrust curve for the turbine for wind speed vs thrust coefficient - assuming no tilt or yaw effects. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - - Returns: - tuple[NDArrayFloat, NDArrayFloat] - Returns the wind speed array and the thrust coefficient array. - """ - shape = (wind_speeds.size, 1) - if self.turbine.multi_dimensional_cp_ct: - ct_curve = { - k: thrust_coefficient( - velocities=wind_speeds.reshape(shape), - turbulence_intensities=np.zeros(shape), - air_density=np.full(shape, v["ref_air_density"]), - yaw_angles=np.zeros(shape), - tilt_angles=np.full(shape, v["ref_tilt"]), - power_setpoints=np.full(shape, POWER_SETPOINT_DEFAULT), - awc_modes=np.full(shape, ["baseline"]), - awc_amplitudes=np.zeros(shape), - thrust_coefficient_functions={ - self.turbine.turbine_type: self.turbine.thrust_coefficient_function - }, - tilt_interps={self.turbine.turbine_type: self.turbine.tilt_interp}, - correct_cp_ct_for_tilt=np.zeros(shape, dtype=bool), - turbine_type_map=np.full(shape, self.turbine.turbine_type), - turbine_power_thrust_tables={self.turbine.turbine_type: v}, - ).flatten() - for k,v in self.turbine.power_thrust_table.items() - } - else: - ct_curve = thrust_coefficient( - velocities=wind_speeds.reshape(shape), - turbulence_intensities=np.zeros(shape), - air_density=np.full(shape, self.turbine.power_thrust_table["ref_air_density"]), - yaw_angles=np.zeros(shape), - tilt_angles=np.full(shape, self.turbine.power_thrust_table["ref_tilt"]), - power_setpoints=np.full(shape, POWER_SETPOINT_DEFAULT), - awc_modes=np.full(shape, ["baseline"]), - awc_amplitudes=np.zeros(shape), - thrust_coefficient_functions={ - self.turbine.turbine_type: self.turbine.thrust_coefficient_function - }, - tilt_interps={self.turbine.turbine_type: self.turbine.tilt_interp}, - correct_cp_ct_for_tilt=np.zeros(shape, dtype=bool), - turbine_type_map=np.full(shape, self.turbine.turbine_type), - turbine_power_thrust_tables={ - self.turbine.turbine_type: self.turbine.power_thrust_table - }, - ).flatten() - return wind_speeds, ct_curve - - def plot_power_curve( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - fig_kwargs: dict | None = None, - plot_kwargs: dict | None = None, - legend_kwargs: dict | None = None, - return_fig: bool = False - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots the power curve for a given set of wind speeds. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. - Defaults to 0 m/s -> 40 m/s, every 0.5 m/s. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.plot()``. - Defaults to None. - legend_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.legend()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - wind_speeds, power_mw = self.power_curve(wind_speeds=wind_speeds) - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - plot_kwargs = {} if plot_kwargs is None else plot_kwargs - legend_kwargs = {} if legend_kwargs is None else legend_kwargs - - # Set the figure defaults if none are provided - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - ax = fig.add_subplot(111) - - min_windspeed = 0 - max_windspeed = max(wind_speeds) - min_power = 0 - max_power = 0 - if isinstance(power_mw, dict): - for key, _power_mw in power_mw.items(): - max_power = max(max_power, *_power_mw) - _cond = "; ".join((f"{c}: {k}" for c, k in zip(self.turbine.condition_keys, key))) - label = f"{self.turbine.turbine_type} - {_cond}" - ax.plot(wind_speeds, _power_mw, label=label, **plot_kwargs) - else: - max_power = max(power_mw) - ax.plot(wind_speeds, power_mw, label=self.turbine.turbine_type, **plot_kwargs) - - ax.grid() - ax.set_axisbelow(True) - ax.legend(**legend_kwargs) - - max_power = round_nearest_2_or_5(max_power) - ax.set_xlim(min_windspeed, max_windspeed) - ax.set_ylim(min_power, max_power) - - ax.set_xlabel("Wind Speed (m/s)") - ax.set_ylabel("Power (MW)") - - if return_fig: - return fig, ax - - fig.tight_layout() - - def plot_thrust_coefficient_curve( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - fig_kwargs: dict | None = None, - plot_kwargs: dict | None = None, - legend_kwargs: dict | None = None, - return_fig: bool = False - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots the thrust coefficient curve for a given set of wind speeds. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.plot()``. - Defaults to None. - legend_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.legend()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - wind_speeds, thrust = self.thrust_coefficient_curve(wind_speeds=wind_speeds) - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - plot_kwargs = {} if plot_kwargs is None else plot_kwargs - legend_kwargs = {} if legend_kwargs is None else legend_kwargs - - # Set the figure defaults if none are provided - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - ax = fig.add_subplot(111) - - min_windspeed = 0 - max_thrust = 0 - max_windspeed = max(wind_speeds) - if isinstance(thrust, dict): - for key, _thrust in thrust.items(): - max_thrust = max(max_thrust, *_thrust) - _cond = "; ".join((f"{c}: {k}" for c, k in zip(self.turbine.condition_keys, key))) - label = f"{self.turbine.turbine_type} - {_cond}" - ax.plot(wind_speeds, _thrust, label=label, **plot_kwargs) - else: - max_thrust = max(thrust) - ax.plot(wind_speeds, thrust, label=self.turbine.turbine_type, **plot_kwargs) - - ax.grid() - ax.set_axisbelow(True) - ax.legend(**legend_kwargs) - - ax.set_xlim(min_windspeed, max_windspeed) - ax.set_ylim(0, round_nearest(max_thrust * 100, base=10) / 100) - - ax.set_xlabel("Wind Speed (m/s)") - ax.set_ylabel("Thrust Coefficient") - - if return_fig: - return fig, ax - - fig.tight_layout() - - -@define(auto_attribs=True) -class TurbineLibrary: - turbine_map: dict[str: TurbineInterface] = field(factory=dict) - power_curves: dict[str, tuple[NDArrayFloat, NDArrayFloat]] = field(factory=dict) - thrust_coefficient_curves: dict[str, tuple[NDArrayFloat, NDArrayFloat]] = field(factory=dict) - - def load_internal_library(self, which: list[str] = [], exclude: list[str] = []) -> None: - """Loads all of the turbine configurations from ``floris/floris/turbine_libary``, - except any turbines defined in :py:attr:`exclude`. - - Args: - which (list[str], optional): A list of which file names to include from loading. - Defaults to []. - exclude (list[str], optional): A list of file names to exclude from loading. - Defaults to []. - """ - print(DEPRECATION_MESSAGE) - include = [el for el in INTERNAL_LIBRARY.iterdir() if el.suffix in (".yaml", ".yml")] - which = [INTERNAL_LIBRARY / el for el in which] if which != [] else include - exclude = [INTERNAL_LIBRARY / el for el in exclude] - include = set(which).intersection(include).difference(exclude) - for fn in include: - turbine_dict = load_yaml(fn) - self.turbine_map.update({ - turbine_dict["turbine_type"]: TurbineInterface.from_turbine_dict(turbine_dict) - }) - - def load_external_library( - self, - library_path: str | Path, - which: list[str] = [], - exclude: list[str] = [], - ) -> None: - """Loads all the turbine configurations from :py:attr:`library_path`, except the file names - defined in :py:attr:`exclude`, and adds each to ``turbine_map`` via a dictionary - update. - - Args: - library_path : str | Path - The external turbine library that should be used for loading the turbines. - which (list[str], optional): A list of which file names to include from loading. - Defaults to []. - exclude (list[str], optional): A list of file names to exclude from loading. - Defaults to []. - """ - print(DEPRECATION_MESSAGE) - library_path = Path(library_path).resolve() - include = [el for el in library_path.iterdir() if el.suffix in (".yaml", ".yml")] - which = [library_path / el for el in which] if which != [] else include - exclude = [library_path / el for el in exclude] - include = set(which).intersection(include).difference(exclude) - for fn in include: - turbine_dict = load_yaml(fn) - self.turbine_map.update({ - turbine_dict["turbine_type"]: TurbineInterface.from_turbine_dict(turbine_dict) - }) - - def compute_power_curves( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - ) -> None: - """Computes the power curves for each turbine in ``turbine_map`` and sets the - ``power_curves`` attribute. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - """ - self.power_curves = { - name: t.power_curve(wind_speeds) for name, t in self.turbine_map.items() - } - - def compute_thrust_coefficient_curves( - self, - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - ) -> None: - """Computes the thrust curves for each turbine in ``turbine_map`` and sets the - ``thrust_coefficient_curves`` attribute. - - Args: - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - """ - self.thrust_coefficient_curves = { - name: t.thrust_coefficient_curve(wind_speeds) for name, t in self.turbine_map.items() - } - - def plot_power_curves( - self, - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, - which: list[str] = [], - exclude: list[str] = [], - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - fig_kwargs: dict | None = None, - plot_kwargs: dict | None = None, - legend_kwargs: dict | None = None, - return_fig: bool = False, - show: bool = False, - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots each power curve in ``turbine_map`` in a single plot. - - Args: - fig (plt.figure, optional): A pre-made figure where the plot should exist. - ax (plt.Axes, optional): A pre-initialized axes object that should be used for the plot. - which (list[str], optional): A list of which turbine types/names to include. Defaults to - []. - exclude (list[str], optional): A list of turbine types/names names to exclude. Defaults - to []. - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.plot()``. - Defaults to None. - legend_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.legend()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - show (bool, optional): Indicator if the figure should be automatically displayed. - Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - if self.power_curves == {} or wind_speeds is not None: - self.compute_power_curves(wind_speeds=wind_speeds) - - which = [*self.turbine_map] if which == [] else which - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - plot_kwargs = {} if plot_kwargs is None else plot_kwargs - legend_kwargs = {} if legend_kwargs is None else legend_kwargs - - # Set the figure defaults if none are provided - if fig is None: - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - if ax is None: - ax = fig.add_subplot(111) - - min_windspeed = 0 - max_windspeed = 0 - min_power = 0 - max_power = 0 - for name, (ws, p) in self.power_curves.items(): - if name in exclude or name not in which: - continue - if isinstance(p, dict): - max_windspeed = max(ws.max(), max_windspeed) - for k, _p in p.items(): - max_power = max(_p.max(), max_power) - label = f"{name} - {k}" - ax.plot(ws, _p, label=label, linestyle="--", **plot_kwargs) - else: - max_power = max(p.max(), max_power) - max_windspeed = max(ws.max(), max_windspeed) - ax.plot(ws, p, label=name, **plot_kwargs) - - ax.grid() - ax.set_axisbelow(True) - ax.legend(**legend_kwargs) - - max_power = round_nearest(max_power, base=5) - ax.set_xlim(min_windspeed, max_windspeed) - ax.set_ylim(min_power, max_power) - - ax.set_xlabel("Wind Speed (m/s)") - ax.set_ylabel("Power (MW)") - - if return_fig: - return fig, ax - - if show: - fig.tight_layout() - - def plot_thrust_coefficient_curves( - self, - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, - which: list[str] = [], - exclude: list[str] = [], - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - fig_kwargs: dict | None = None, - plot_kwargs: dict | None = None, - legend_kwargs: dict | None = None, - return_fig: bool = False, - show: bool = False, - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots each thrust coefficient curve in ``turbine_map`` in a single plot. - - Args: - fig (plt.figure, optional): A pre-made figure where the plot should exist. - ax (plt.Axes, optional): A pre-initialized axes object that should be used for the plot. - which (list[str], optional): A list of which turbine types/names to include. Defaults to - []. - exclude (list[str], optional): A list of turbine types/names names to exclude. Defaults - to []. - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.plot()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.legend()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - show (bool, optional): Indicator if the figure should be automatically displayed. - Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - if self.thrust_coefficient_curves == {} or wind_speeds is None: - self.compute_thrust_coefficient_curves(wind_speeds=wind_speeds) - - which = [*self.turbine_map] if which == [] else which - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - plot_kwargs = {} if plot_kwargs is None else plot_kwargs - legend_kwargs = {} if legend_kwargs is None else legend_kwargs - - - # Set the figure defaults if none are provided - if fig is None: - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - if ax is None: - ax = fig.add_subplot(111) - - min_windspeed = 0 - max_windspeed = 0 - max_thrust = 0 - for name, (ws, t) in self.thrust_coefficient_curves.items(): - if name in exclude or name not in which: - continue - if isinstance(t, dict): - max_windspeed = max(ws.max(), max_windspeed) - for k, _t in t.items(): - max_thrust = max(_t.max(), max_thrust) - label = f"{name} - {k}" - ax.plot(ws, _t, label=label, linestyle="--", **plot_kwargs) - else: - max_windspeed = max(ws.max(), max_windspeed) - max_thrust = max(t.max(), max_thrust) - ax.plot(ws, t, label=name, **plot_kwargs) - - ax.grid() - ax.set_axisbelow(True) - ax.legend(**legend_kwargs) - - ax.set_xlim(min_windspeed, max_windspeed) - ax.set_ylim(0, round_nearest(max_thrust * 100, base=10) / 100) - - ax.set_xlabel("Wind Speed (m/s)") - ax.set_ylabel("Thrust Coefficient") - - if return_fig: - return fig, ax - - if show: - fig.tight_layout() - - def plot_rotor_diameters( - self, - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, - which: list[str] = [], - exclude: list[str] = [], - fig_kwargs: dict | None = None, - bar_kwargs: dict | None = None, - return_fig: bool = False, - show: bool = False, - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots a bar chart of rotor diameters for each turbine in ``turbine_map``. - - Args: - fig (plt.figure, optional): A pre-made figure where the plot should exist. - ax (plt.Axes, optional): A pre-initialized axes object that should be used for the plot. - which (list[str], optional): A list of which turbine types/names to include. Defaults to - []. - exclude (list[str], optional): A list of turbine types/names names to exclude. Defaults - to []. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - bar_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.bar()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - show (bool, optional): Indicator if the figure should be automatically displayed. - Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - which = [*self.turbine_map] if which == [] else which - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - bar_kwargs = {} if bar_kwargs is None else bar_kwargs - - # Set the figure defaults if none are provided - if fig is None: - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - if ax is None: - ax = fig.add_subplot(111) - - subset_map = { - name: t for name, t in self.turbine_map.items() - if name not in exclude or name in which - } - x = np.arange(len(subset_map)) - y = [ti.turbine.rotor_diameter for ti in subset_map.values()] - ix_sort = np.argsort(y) - y_sorted = np.array(y)[ix_sort] - ax.bar(x, y_sorted, **bar_kwargs) - - ax.grid(axis="y") - ax.set_axisbelow(True) - - ax.set_xlim(-0.5, len(x) - 0.5) - ax.set_ylim(0, round_nearest(max(y) / 10, base=5) * 10) - - ax.set_xticks(x) - ax.set_xticklabels(np.array([*subset_map])[ix_sort], rotation=30, ha="right") - ax.set_ylabel("Rotor Diameter (m)") - - if return_fig: - return fig, ax - - if show: - fig.tight_layout() - - def plot_hub_heights( - self, - fig: plt.Figure | None = None, - ax: plt.Axes | None = None, - which: list[str] = [], - exclude: list[str] = [], - fig_kwargs: dict | None = None, - bar_kwargs: dict | None = None, - return_fig: bool = False, - show: bool = False, - ) -> None | tuple[plt.Figure, plt.Axes]: - """Plots a bar chart of hub heights for each turbine in ``turbine_map``. - - Args: - fig (plt.figure, optional): A pre-made figure where the plot should exist. - ax (plt.Axes, optional): A pre-initialized axes object that should be used for the plot. - which (list[str], optional): A list of which turbine types/names to include. Defaults to - []. - exclude (list[str], optional): A list of turbine types/names names to exclude. Defaults - to []. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - bar_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.bar()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - show (bool, optional): Indicator if the figure should be automatically displayed. - Defaults to False. - - Returns: - None | tuple[plt.Figure, plt.Axes]: None, if :py:attr:`return_fig` is False, otherwise - a tuple of the Figure and Axes objects are returned. - """ - which = [*self.turbine_map] if which == [] else which - - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - bar_kwargs = {} if bar_kwargs is None else bar_kwargs - - # Set the figure defaults if none are provided - if fig is None: - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (4, 3)) - - fig = plt.figure(**fig_kwargs) - if ax is None: - ax = fig.add_subplot(111) - - subset_map = { - name: t for name, t in self.turbine_map.items() - if name not in exclude or name in which - } - x = np.arange(len(subset_map)) - y = [ti.turbine.hub_height for ti in subset_map.values()] - ix_sort = np.argsort(y) - y_sorted = np.array(y)[ix_sort] - ax.bar(x, y_sorted, **bar_kwargs) - - ax.grid(axis="y") - ax.set_axisbelow(True) - - ax.set_xlim(-0.5, len(x) - 0.5) - ax.set_ylim(0, round_nearest(max(y) / 10, base=5) * 10) - - ax.set_xticks(x) - ax.set_xticklabels(np.array([*subset_map])[ix_sort], rotation=30, ha="right") - ax.set_ylabel("Hub Height (m)") - - if return_fig: - return fig, ax - - if show: - fig.tight_layout() - - def plot_comparison( - self, - which: list[str] = [], - exclude: list[str] = [], - wind_speeds: NDArrayFloat = DEFAULT_WIND_SPEEDS, - fig_kwargs: dict | None = None, - plot_kwargs: dict | None = None, - bar_kwargs: dict | None = None, - legend_kwargs: dict | None = None, - return_fig: bool = False - ) -> None | tuple[plt.Figure, list[plt.Axes]]: - """Plots each thrust curve in ``turbine_map`` in a single plot. - - Args: - which (list[str], optional): A list of which turbine types/names to include. Defaults to - []. - exclude (list[str], optional): A list of turbine types/names names to exclude. Defaults - to []. - wind_speeds (NDArrayFloat, optional): A 1-D array of wind speeds, in m/s. Defaults to - 0 m/s -> 40 m/s, every 0.5 m/s. - fig_kwargs (dict, optional): Any keywords arguments to be passed to ``plt.Figure()``. - Defaults to None. - plot_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.plot()``. - Defaults to None. - bar_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.bar()``. - Defaults to None. - legend_kwargs (dict, optional): Any keyword arguments to be passed to ``plt.legend()``. - Defaults to None. - return_fig (bool, optional): Indicator if the ``Figure`` and ``Axes`` objects should be - returned. Defaults to False. - - Returns: - None | tuple[plt.Figure, list[plt.Axes]]: None, if :py:attr:`return_fig` is False, - otherwise a tuple of the Figure and Axes objects are returned. - """ - # Initialize kwargs if None - fig_kwargs = {} if fig_kwargs is None else fig_kwargs - plot_kwargs = {} if plot_kwargs is None else plot_kwargs - bar_kwargs = {} if bar_kwargs is None else bar_kwargs - legend_kwargs = {} if legend_kwargs is None else legend_kwargs - - # Set the figure defaults if none are provided - fig_kwargs.setdefault("dpi", 200) - fig_kwargs.setdefault("figsize", (6, 5)) - legend_kwargs.setdefault("fontsize", 6) - - fig = plt.figure(**fig_kwargs) - ax1 = fig.add_subplot(321) - ax2 = fig.add_subplot(322) - ax3 = fig.add_subplot(323) - ax4 = fig.add_subplot(324) - ax_list = [ax1, ax2, ax3, ax4] - - self.plot_power_curves( - fig, - ax1, - which=which, - exclude=exclude, - wind_speeds=wind_speeds, - plot_kwargs=plot_kwargs, - ) - self.plot_thrust_coefficient_curves( - fig, - ax3, - which=which, - exclude=exclude, - wind_speeds=wind_speeds, - plot_kwargs=plot_kwargs, - ) - self.plot_rotor_diameters(fig, ax2, which=which, exclude=exclude, bar_kwargs=bar_kwargs) - self.plot_hub_heights(fig, ax4, which=which, bar_kwargs=bar_kwargs) - - for ax in ax_list: - ax.tick_params(axis='both', which='major', labelsize=7) - ax.xaxis.label.set_size(7) - ax.yaxis.label.set_size(8) - - for ax in (ax1, ax3): - ax.legend(**legend_kwargs) - - if return_fig: - return fig, ax_list - - fig.tight_layout() diff --git a/floris/uncertain_floris_model.py b/floris/uncertain_floris_model.py index 2fc6a78325..507accd8a1 100644 --- a/floris/uncertain_floris_model.py +++ b/floris/uncertain_floris_model.py @@ -9,6 +9,7 @@ from floris import FlorisModel from floris.core import average_velocity, State +from floris.core.turbine import BaseOperationModel from floris.logging_manager import LoggingManager from floris.par_floris_model import ParFlorisModel from floris.type_dec import ( @@ -957,20 +958,13 @@ def _get_weights(self, wd_std, wd_sample_points): return weights - def get_operation_model(self) -> str: + def get_operation_model(self) -> list[BaseOperationModel]: """Get the operation model of a FlorisModel. Returns: - str: The operation_model. + list[BaseOperationModel]: The operation_model instance(s). """ - operation_models = [ - self.fmodel_unexpanded.core.farm.turbine_definitions[tindex]["operation_model"] - for tindex in range(self.fmodel_unexpanded.core.farm.n_turbines) - ] - if len(set(operation_models)) == 1: - return operation_models[0] - else: - return operation_models + return [t.operation_model for t in self.fmodel_expanded.core.farm.turbines] def set_operation_model(self, operation_model: str | List[str]): """Set the turbine operation model(s). @@ -981,10 +975,10 @@ def set_operation_model(self, operation_model: str | List[str]): if isinstance(operation_model, str): if len(self.fmodel_unexpanded.core.farm.turbine_type) == 1: # Set a single one here, then, and return - turbine_type = self.fmodel_unexpanded.core.farm.turbine_definitions[0] - turbine_type["operation_model"] = operation_model + turbine_dict = self.fmodel_unexpanded.core.farm.turbines[0].as_dict() + turbine_dict["operation_model"] = operation_model self.set( - turbine_type=[turbine_type], + turbine_type=[turbine_dict], reference_wind_height=self.reference_wind_height ) return @@ -996,16 +990,16 @@ def set_operation_model(self, operation_model: str | List[str]): "The length of the operation_model list must be " "equal to the number of turbines." ) - turbine_type_list = self.fmodel_unexpanded.core.farm.turbine_definitions + turbine_dicts = [t.as_dict() for t in self.fmodel_unexpanded.core.farm.turbines] for tindex in range(self.fmodel_unexpanded.core.farm.n_turbines): - turbine_type_list[tindex]["turbine_type"] = ( - turbine_type_list[tindex]["turbine_type"] + "_" + operation_model[tindex] + turbine_dicts[tindex]["turbine_type"] = ( + turbine_dicts[tindex]["turbine_type"] + "_" + operation_model[tindex] ) - turbine_type_list[tindex]["operation_model"] = operation_model[tindex] + turbine_dicts[tindex]["operation_model"] = operation_model[tindex] self.set( - turbine_type=turbine_type_list, + turbine_type=turbine_dicts, reference_wind_height=self.reference_wind_height ) @@ -1019,34 +1013,35 @@ def copy(self): """ return self.__class__(self.fmodel_unexpanded.copy(), **self.secondary_init_kwargs) - def get_param(self, param: List[str], param_idx: Optional[int] = None) -> Any: + def get_wake_parameter(self, parameter: str, parameter_idx: Optional[int] = None) -> Any: """Get a parameter from a FlorisModel object. Args: - param (List[str]): A list of keys to traverse the FlorisModel dictionary. - param_idx (Optional[int], optional): The index to get the value at. Defaults to None. - If None, the entire parameter is returned. + parameter (str): The wake parameter to get. + parameter_idx (Optional[int], optional): The index to get the value at. + Defaults to None. If None, the entire parameter is returned. Returns: Any: The value of the parameter. """ fm_dict = self.fmodel_unexpanded.core.as_dict() - if param_idx is None: - return nested_get(fm_dict, param) + if parameter_idx is None: + return nested_get(fm_dict, ["wake", "parameters", parameter]) else: - return nested_get(fm_dict, param)[param_idx] + return nested_get(fm_dict, ["wake", "parameters", parameter])[parameter_idx] - def set_param(self, param: List[str], value: Any, param_idx: Optional[int] = None): + def set_wake_parameter(self, parameter: str, value: Any, parameter_idx: Optional[int] = None): """Set a parameter in a FlorisModel object. Args: - param (List[str]): A list of keys to traverse the FlorisModel dictionary. + parameter (str): The wake parameter to set. value (Any): The value to set. - param_idx (Optional[int], optional): The index to set the value at. Defaults to None. + parameter_idx (Optional[int], optional): The index to set the value at. + Defaults to None. """ fm_dict_mod = self.fmodel_unexpanded.core.as_dict() - nested_set(fm_dict_mod, param, value, param_idx) + nested_set(fm_dict_mod, ["wake", "parameters", parameter], value, parameter_idx) self.fmodel_unexpanded.__init__(fm_dict_mod, **self.fmodel_unexpanded.secondary_init_kwargs) self.set() diff --git a/floris/wind_data.py b/floris/wind_data.py index 02828d12ac..a775f5bfe5 100644 --- a/floris/wind_data.py +++ b/floris/wind_data.py @@ -277,8 +277,7 @@ def __init__( # If heterogeneous_inflow_config_by_wd is not None, then create a HeterogeneousMap object # using the dictionary if heterogeneous_inflow_config_by_wd is not None: - # TODO: In future, add deprecation warning for this parameter here - + # TODO: Remove before v5 release self.heterogeneous_map = HeterogeneousMap(**heterogeneous_inflow_config_by_wd) # Else if heterogeneous_map is not None @@ -1278,7 +1277,7 @@ def __init__( # If heterogeneous_inflow_config_by_wd is not None, then create a HeterogeneousMap object # using the dictionary if heterogeneous_inflow_config_by_wd is not None: - # TODO: In future, add deprectation warning for this parameter here + # TODO: Remove before v5 release self.heterogeneous_map = HeterogeneousMap(**heterogeneous_inflow_config_by_wd) @@ -2332,7 +2331,7 @@ def __init__( # If heterogeneous_inflow_config_by_wd is not None, then create a HeterogeneousMap object # using the dictionary if heterogeneous_inflow_config_by_wd is not None: - # TODO: In future, add deprecation warning for this parameter here + # TODO: Remove before v5 release self.heterogeneous_map = HeterogeneousMap(**heterogeneous_inflow_config_by_wd) diff --git a/profiling/profiling.py b/profiling/profiling.py index a4fcc769d2..de44c8dba3 100644 --- a/profiling/profiling.py +++ b/profiling/profiling.py @@ -24,17 +24,16 @@ def run_floris(): # floris.farm.flow_field.calculate_wake() # start = time.time() - # cProfile.run('re.compile("floris.steady_state_atmospheric_condition()")') + # cProfile.run('re.compile("floris.solve_for_turbines()")') # end = time.time() # print(start, end, end - start) sample_inputs = SampleInputs() - sample_inputs.core["wake"]["model_strings"]["velocity_model"] = "gauss" - sample_inputs.core["wake"]["model_strings"]["deflection_model"] = "gauss" - sample_inputs.core["wake"]["enable_secondary_steering"] = True - sample_inputs.core["wake"]["enable_yaw_added_recovery"] = True - sample_inputs.core["wake"]["enable_transverse_velocities"] = True + sample_inputs.switch_wake_model("gauss") + sample_inputs.core["wake"]["parameters"]["enable_secondary_steering"] = True + sample_inputs.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True + sample_inputs.core["wake"]["parameters"]["enable_transverse_velocities"] = True N_TURBINES = 100 N_FINDEX = 72 * 25 # Size of a characteristic wind rose @@ -51,4 +50,4 @@ def run_floris(): for i in range(N): core = Core.from_dict(copy.deepcopy(sample_inputs.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() diff --git a/profiling/quality_metrics.py b/profiling/quality_metrics.py index 142480550d..6e4650936e 100644 --- a/profiling/quality_metrics.py +++ b/profiling/quality_metrics.py @@ -35,7 +35,7 @@ def run_floris(input_dict): start = time.perf_counter() core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() end = time.perf_counter() return end - start except KeyError: @@ -57,29 +57,25 @@ def time_profile(input_dict): def test_time_jensen_jimenez(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = "jensen" - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = "jimenez" + sample_inputs_fixture.switch_wake_model("jensen") return time_profile(sample_inputs_fixture) def test_time_gauss(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = "gauss" - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = "gauss" + sample_inputs_fixture.switch_wake_model("gauss") return time_profile(sample_inputs_fixture) def test_time_gch(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = "gauss" - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = "gauss" - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = True - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = True + sample_inputs_fixture.switch_wake_model("gauss") + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True return time_profile(sample_inputs_fixture) def test_time_cumulative(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = "cc" - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = "gauss" + sample_inputs_fixture.switch_wake_model("cc") return time_profile(sample_inputs_fixture) @@ -87,13 +83,13 @@ def memory_profile(input_dict): # Run once to initialize Python and memory core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() with perf(): for i in range(N_ITERATIONS): core = Core.from_dict(copy.deepcopy(input_dict.core)) core.initialize_domain() - core.steady_state_atmospheric_condition() + core.solve_for_turbines() print( "Size of one data array: " @@ -102,8 +98,7 @@ def memory_profile(input_dict): def test_mem_jensen_jimenez(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = "jensen" - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = "jimenez" + sample_inputs_fixture.switch_wake_model("jensen") memory_profile(sample_inputs_fixture) diff --git a/pyproject.toml b/pyproject.toml index 5111ba8643..cd291e7b22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "floris" -version = "4.6.4" +version = "4.6.6" description = "A controls-oriented engineering wake model." readme = "README.md" requires-python = ">=3.10, <3.15" @@ -36,7 +36,7 @@ dependencies = [ "numpy~=2.0", "scipy~=1.1", "matplotlib~=3.0", - "pandas~=2.0", + "pandas>=2.0,<4", "shapely~=2.0", "coloredlogs~=15.0", "pathos~=0.3", @@ -57,7 +57,7 @@ develop = [ "pytest-benchmark~=5.1", "pre-commit~=4.0", "ruff~=0.9", - "isort>=5,<8" + "isort>=5,<9" ] [tool.setuptools.packages.find] @@ -196,6 +196,7 @@ lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" "floris/core/wake_velocity/jensen.py" = ["F841"] "floris/core/wake_velocity/gauss.py" = ["F841"] "floris/core/wake_velocity/empirical_gauss.py" = ["F841"] +"floris/core/wake_model/*.py" = ["F841"] # Ignore `F401` (import violations) in all `__init__.py` files, and in `path/to/file.py`. "__init__.py" = ["F401"] @@ -234,6 +235,7 @@ lines_after_imports = 2 line_length = 100 order_by_type = false split_on_trailing_comma = true +skip = ["floris/core/turbine/__init__.py"] # length_sort = true # case_sensitive: False diff --git a/tests/conftest.py b/tests/conftest.py index 2be43a4c77..04088999d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ from floris.core import ( Core, FlowField, - FlowFieldGrid, PointsGrid, TurbineGrid, ) @@ -160,17 +159,6 @@ def turbine_grid_fixture(sample_inputs_fixture) -> TurbineGrid: grid_resolution=TURBINE_GRID_RESOLUTION, ) -@pytest.fixture -def flow_field_grid_fixture(sample_inputs_fixture) -> FlowFieldGrid: - turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) - rotor_diameters = ROTOR_DIAMETER * np.ones( (N_FINDEX, N_TURBINES) ) - return FlowFieldGrid( - turbine_coordinates=turbine_coordinates, - turbine_diameters=rotor_diameters, - wind_directions=np.array(WIND_DIRECTIONS), - grid_resolution=[3,2,2] - ) - @pytest.fixture def points_grid_fixture(sample_inputs_fixture) -> PointsGrid: turbine_coordinates = np.array(list(zip(X_COORDS, Y_COORDS, Z_COORDS))) @@ -453,91 +441,108 @@ def __init__(self): "reference_wind_height": self.turbine["hub_height"], } - self.wake = { - "model_strings": { - "velocity_model": "jensen", - "deflection_model": "jimenez", - "combination_model": "sosfs", - "turbulence_model": "crespo_hernandez", + self._wake_gauss = { + "model": "gauss", + "parameters": { + "ad": 0.0, + "alpha": 0.58, + "bd": 0.0, + "beta": 0.077, + "dm": 1.0, + "ka": 0.38, + "kb": 0.004, + "initial": 0.1, + "constant": 0.5, + "ai": 0.8, + "downstream": -0.32, + "enable_secondary_steering": False, + "enable_yaw_added_recovery": False, + "enable_transverse_velocities": False, }, - "wake_deflection_parameters": { - "gauss": { - "ad": 0.0, - "alpha": 0.58, - "bd": 0.0, - "beta": 0.077, - "dm": 1.0, - "ka": 0.38, - "kb": 0.004 - }, - "jimenez": { - "ad": 0.0, - "bd": 0.0, - "kd": 0.05, - }, - "empirical_gauss": { - "horizontal_deflection_gain_D": 3.0, - "vertical_deflection_gain_D": -1, - "deflection_rate": 22, - "mixing_gain_deflection": 0.0, - "yaw_added_mixing_gain": 0.0 - }, + "combination_model": "sosfs", + } + + self._wake_jensen = { + "model": "jensen", + "parameters": { + "initial": 0.01, + "constant": 0.9, + "ai": 0.83, + "downstream": -0.25, + "we": 0.05, + "ad": 0.0, + "bd": 0.0, }, - "wake_velocity_parameters": { - "gauss": { - "alpha": 0.58, - "beta": 0.077, - "ka": 0.38, - "kb": 0.004 - }, - "jensen": { - "we": 0.05, - }, - "cc": { - "a_s": 0.179367259, - "b_s": 0.0118889215, - "c_s1": 0.0563691592, - "c_s2": 0.13290157, - "a_f": 3.11, - "b_f": -0.68, - "c_f": 2.41, - "alpha_mod": 1.0 - }, - "turbopark": { - "A": 0.04, - "sigma_max_rel": 4.0 - }, - "turboparkgauss": { - "A": 0.04, - "include_mirror_wake": True - }, - "empirical_gauss": { - "wake_expansion_rates": [0.023, 0.008], - "breakpoints_D": [10], - "sigma_0_D": 0.28, - "smoothing_length_D": 2.0, - "mixing_gain_velocity": 2.0, - "awc_wake_exp": 1.2, - "awc_wake_denominator": 400 - }, + "combination_model": "sosfs", + } + + self._wake_empirical_gauss = { + "model": "empirical_gauss", + "parameters": { + "wake_expansion_rates": [0.023, 0.008], + "breakpoints_D": [10], + "sigma_0_D": 0.28, + "smoothing_length_D": 2.0, + "mixing_gain_velocity": 2.0, + "awc_wake_exp": 1.2, + "awc_wake_denominator": 400, + "horizontal_deflection_gain_D": 3.0, + "vertical_deflection_gain_D": -1, + "deflection_rate": 22, + "mixing_gain_deflection": 0.0, + "yaw_added_mixing_gain": 0.0, + "enable_active_wake_mixing": False, + "enable_yaw_added_recovery": False, }, - "wake_turbulence_parameters": { - "crespo_hernandez": { - "initial": 0.1, - "constant": 0.5, - "ai": 0.8, - "downstream": -0.32 - }, - "wake_induced_mixing": { - "atmospheric_ti_gain": 0.0 - } + "combination_model": "sosfs", + } + + self._wake_cc = { + "model": "cc", + "parameters": { + "a_s": 0.179367259, + "b_s": 0.0118889215, + "c_s1": 0.0563691592, + "c_s2": 0.13290157, + "a_f": 3.11, + "b_f": -0.68, + "c_f": 2.41, + "alpha_mod": 1.0, + "ad": 0.0, + "alpha": 0.58, + "bd": 0.0, + "beta": 0.077, + "dm": 1.0, + "ka": 0.38, + "kb": 0.004, + "initial": 0.1, + "constant": 0.5, + "ai": 0.8, + "downstream": -0.32, + "enable_secondary_steering": False, + "enable_yaw_added_recovery": False, + "enable_transverse_velocities": False, + }, + "combination_model": "none", + } + + self._wake_turboparkgauss = { + "model": "turboparkgauss", + "parameters": { + "A": 0.04, + "include_mirror_wake": True, }, - "enable_secondary_steering": False, - "enable_yaw_added_recovery": False, - "enable_active_wake_mixing": False, - "enable_transverse_velocities": False, + "combination_model": "sosfs", + } + + self._wake_none = { + "model": "none", + "parameters": {}, + "combination_model": "none", } + self.wake = self._wake_gauss + self.core = { "farm": self.farm, "flow_field": self.flow_field, @@ -737,3 +742,25 @@ def __init__(self): ], }, } + + def switch_wake_model(self, model_name: str): + if model_name == "gauss": + self.wake = self._wake_gauss + self.core["wake"] = self.wake + elif model_name == "jensen": + self.wake = self._wake_jensen + self.core["wake"] = self.wake + elif model_name == "empirical_gauss": + self.wake = self._wake_empirical_gauss + self.core["wake"] = self.wake + elif model_name == "cc": + self.wake = self._wake_cc + self.core["wake"] = self.wake + elif model_name == "turboparkgauss": + self.wake = self._wake_turboparkgauss + self.core["wake"] = self.wake + elif model_name == "none": + self.wake = self._wake_none + self.core["wake"] = self.wake + else: + raise ValueError(f"Unknown wake model: {model_name}") diff --git a/tests/controller_dependent_operation_model_unit_test.py b/tests/controller_dependent_operation_model_unit_test.py index a10e130ed1..5cf7cc0005 100644 --- a/tests/controller_dependent_operation_model_unit_test.py +++ b/tests/controller_dependent_operation_model_unit_test.py @@ -477,7 +477,7 @@ def test_CpCt_data_consistency(): yaml_file = Path(__file__).resolve().parent / "data" / "input_full.yaml" fmodel = FlorisModel(configuration=yaml_file) fmodel.set(turbine_type=[turbine]) - power_thrust_table = fmodel.core.farm.turbine_map[0].power_thrust_table + power_thrust_table = fmodel.core.farm.turbines[0].power_thrust_table tilt_angles_nom = power_thrust_table["ref_tilt"] * np.ones((N_test, n_turbines)) diff --git a/tests/convert_v3_to_v4_test.py b/tests/convert_v3_to_v4_test.py deleted file mode 100644 index dbaa7bc673..0000000000 --- a/tests/convert_v3_to_v4_test.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -from pathlib import Path - -import floris -from floris import FlorisModel - - -CONVERT_FOLDER = Path(__file__).resolve().parent / "v3_to_v4_convert_test" -FLORIS_FOLDER = Path(floris.__file__).resolve().parent - - -def test_v3_to_v4_convert(): - # Note certain filenames - filename_v3_floris = "gch.yaml" - filename_v4_floris = "gch_v4.yaml" - filename_v3_turbine = "nrel_5MW_v3.yaml" - filename_v4_turbine = "nrel_5MW_v3_v4.yaml" - - # Copy convert scripts from FLORIS_FOLDER to CONVERT_FOLDER - os.system(f"cp {FLORIS_FOLDER / 'convert_turbine_v3_to_v4.py'} {CONVERT_FOLDER}") - os.system(f"cp {FLORIS_FOLDER / 'convert_floris_input_v3_to_v4.py'} {CONVERT_FOLDER}") - - # Change directory to the test folder - os.chdir(CONVERT_FOLDER) - - # Run the converter on the turbine file - os.system(f"python convert_turbine_v3_to_v4.py {filename_v3_turbine}") - - # Run the converter on the floris file - os.system(f"python convert_floris_input_v3_to_v4.py {filename_v3_floris}") - - # Go through the file filename_v4_floris and replace f"!include {filename_v3_turbine}" - # with f"!include {filename_v4_turbine}" - with open(filename_v4_floris, "r") as file: - filedata = file.read() - filedata = filedata.replace( - f"!include {filename_v3_turbine}", f"!include {filename_v4_turbine}" - ) - with open(filename_v4_floris, "w") as file: - file.write(filedata) - - # Now confirm that the converted file can be loaded by FLORIS - fmodel = FlorisModel(filename_v4_floris) - - # Now confirm this model runs - fmodel.run() - - # Delete the newly created files to clean up - os.system(f"rm {filename_v4_floris}") - os.system(f"rm {filename_v4_turbine}") - os.system("rm convert_turbine_v3_to_v4.py") - os.system("rm convert_floris_input_v3_to_v4.py") diff --git a/tests/convert_v4_to_v5_test.py b/tests/convert_v4_to_v5_test.py new file mode 100644 index 0000000000..f451cb744c --- /dev/null +++ b/tests/convert_v4_to_v5_test.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +import floris +from floris import FlorisModel + + +CONVERT_FOLDER = Path(__file__).resolve().parent / "v4_to_v5_converter_test" +FLORIS_FOLDER = Path(floris.__file__).resolve().parent + + +def test_v4_to_v5_converter(): + # Note certain filenames + filename_v4_floris = "gch.yaml" + filename_v5_floris = "gch_v5.yaml" + + # Copy convert scripts from FLORIS_FOLDER to CONVERT_FOLDER + os.system(f"cp {FLORIS_FOLDER / 'convert_floris_input_v4_to_v5.py'} {CONVERT_FOLDER}") + + # Change directory to the test folder + os.chdir(CONVERT_FOLDER) + + # Run the converter on the floris file + os.system(f"python convert_floris_input_v4_to_v5.py {filename_v4_floris}") + + # Now confirm that the converted file can be loaded by FLORIS + fmodel = FlorisModel(filename_v5_floris) + + # Now confirm this model runs + fmodel.run() + + # Delete the newly created files to clean up + os.system(f"rm {filename_v5_floris}") + os.system("rm convert_floris_input_v4_to_v5.py") diff --git a/tests/data/input_full.yaml b/tests/data/input_full.yaml index 49c41273d1..50905c9978 100644 --- a/tests/data/input_full.yaml +++ b/tests/data/input_full.yaml @@ -1,8 +1,6 @@ - name: test_input description: Single turbine for testing -floris_version: v4 - +floris_version: v5 logging: console: enable: false @@ -36,55 +34,17 @@ flow_field: wind_veer: 0.0 wake: - model_strings: - combination_model: sosfs - deflection_model: gauss - turbulence_model: crespo_hernandez - velocity_model: gauss - - enable_secondary_steering: true - enable_yaw_added_recovery: true - enable_active_wake_mixing: true - enable_transverse_velocities: true - - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - turboparkgauss: - A: 0.04 - include_mirror_wake: True - - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.01 - constant: 0.9 - ai: 0.83 - downstream: -0.25 + model: gauss + parameters: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/tests/data/input_full_v5.yaml b/tests/data/input_full_v5.yaml new file mode 100644 index 0000000000..3db234e997 --- /dev/null +++ b/tests/data/input_full_v5.yaml @@ -0,0 +1,46 @@ +name: test_input +description: Single turbine for testing +floris_version: v5 +logging: + console: + enable: false + level: WARNING + file: + enable: false + level: WARNING +solver: + type: turbine_grid + turbine_grid_points: 3 +farm: + layout_x: + - 0.0 + layout_y: + - 0.0 + turbine_type: + - nrel_5MW +flow_field: + air_density: 1.225 + reference_wind_height: 90.0 + turbulence_intensities: + - 0.06 + wind_directions: + - 270.0 + wind_shear: 0.12 + wind_speeds: + - 8.0 + wind_veer: 0.0 +wake: + model: gauss + parameters: + initial: 0.01 + constant: 0.9 + ai: 0.83 + downstream: -0.25 + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + combination_model: sosfs diff --git a/tests/farm_unit_test.py b/tests/farm_unit_test.py index 81d20f8ae7..f675f92aab 100644 --- a/tests/farm_unit_test.py +++ b/tests/farm_unit_test.py @@ -14,7 +14,7 @@ ) -def test_farm_init_homogenous_turbines(): +def test_farm_init_homogeneous_turbines(): farm_data = SampleInputs().farm turbine_data = SampleInputs().turbine @@ -34,7 +34,6 @@ def test_farm_init_homogenous_turbines(): # turbine_type=[turbine_data] # turbine_type=[turbine_data["turbine_type"]] - farm.construct_hub_heights() farm.set_yaw_angles_to_ref_yaw(N_FINDEX) # Check initial values @@ -45,25 +44,11 @@ def test_farm_init_homogenous_turbines(): def test_asdict(sample_inputs_fixture: SampleInputs): farm = Farm.from_dict(sample_inputs_fixture.farm) - farm.construct_hub_heights() - farm.construct_turbine_ref_tilts() - farm.set_yaw_angles_to_ref_yaw(N_FINDEX) - farm.set_tilt_to_ref_tilt(N_FINDEX) - farm.set_power_setpoints_to_ref_power(N_FINDEX) - farm.set_awc_modes_to_ref_mode(N_FINDEX) - farm.set_awc_amplitudes_to_ref_amp(N_FINDEX) - farm.set_awc_frequencies_to_ref_freq(N_FINDEX) + farm.set_control_setpoints_to_reference(N_FINDEX) dict1 = farm.as_dict() new_farm = farm.from_dict(dict1) - new_farm.construct_hub_heights() - new_farm.construct_turbine_ref_tilts() - new_farm.set_yaw_angles_to_ref_yaw(N_FINDEX) - new_farm.set_tilt_to_ref_tilt(N_FINDEX) - new_farm.set_power_setpoints_to_ref_power(N_FINDEX) - new_farm.set_awc_modes_to_ref_mode(N_FINDEX) - new_farm.set_awc_amplitudes_to_ref_amp(N_FINDEX) - new_farm.set_awc_frequencies_to_ref_freq(N_FINDEX) + new_farm.set_control_setpoints_to_reference(N_FINDEX) dict2 = new_farm.as_dict() assert dict1 == dict2 @@ -77,7 +62,7 @@ def test_check_turbine_type(sample_inputs_fixture: SampleInputs): farm_data["layout_y"] = np.zeros(5) farm = Farm.from_dict(farm_data) assert len(farm.turbine_type) == 1 - assert len(farm.turbine_definitions) == 5 + assert len(farm.turbines) == 5 # N definitions for M turbines farm_data = deepcopy(sample_inputs_fixture.farm) @@ -94,7 +79,7 @@ def test_check_turbine_type(sample_inputs_fixture: SampleInputs): farm_data["layout_y"] = np.zeros(5) farm = Farm.from_dict(farm_data) assert len(farm.turbine_type) == 5 - assert len(farm.turbine_definitions) == 5 + assert len(farm.turbines) == 5 # String not found in internal library farm_data = deepcopy(sample_inputs_fixture.farm) @@ -113,7 +98,7 @@ def test_check_turbine_type(sample_inputs_fixture: SampleInputs): farm_data["layout_y"] = np.zeros(5) Farm.from_dict(farm_data) assert len(farm.turbine_type) == 5 - assert len(farm.turbine_definitions) == 5 + assert len(farm.turbines) == 5 # Check that error is correctly raised if two turbines have the same name farm_data = deepcopy(sample_inputs_fixture.farm) @@ -146,17 +131,17 @@ def test_check_turbine_type(sample_inputs_fixture: SampleInputs): farm_data["turbine_type"] = [turbine_def]*4 + [turbine_def_mod] farm = Farm.from_dict(farm_data) for i in range(4): - assert farm.turbine_definitions[i]["hub_height"] == turbine_def["hub_height"] - assert farm.turbine_definitions[-1]["hub_height"] == 100.0 - farm.construct_turbine_map() + assert farm.turbines[i].hub_height == turbine_def["hub_height"] + assert farm.turbines[-1].hub_height == 100.0 + farm.construct_turbines() for i in range(4): - assert farm.turbine_map[i].hub_height == turbine_def["hub_height"] - assert farm.turbine_map[-1].hub_height == 100.0 + assert farm.turbines[i].hub_height == turbine_def["hub_height"] + assert farm.turbines[-1].hub_height == 100.0 # Duplicate type found in external and internal library farm_data = deepcopy(sample_inputs_fixture.farm) external_library = Path(__file__).parent / "data" - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = ["nrel_5MW"] with pytest.raises(ValueError): Farm.from_dict(farm_data) @@ -171,18 +156,18 @@ def test_check_turbine_type(sample_inputs_fixture: SampleInputs): farm_data["turbine_type"] = ["nrel_5MW", turbine_def, "nrel_5MW", turbine_def, "nrel_5MW"] Farm.from_dict(farm_data) assert len(farm.turbine_type) == 5 - assert len(farm.turbine_definitions) == 5 + assert len(farm.turbines) == 5 # 1 turbine as string from internal library, 1 turbine as string from external library farm_data = deepcopy(sample_inputs_fixture.farm) external_library = Path(__file__).parent / "data" - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = 4 * ["iea_10MW"] + ["nrel_5MW_custom"] farm_data["layout_x"] = np.arange(0, 500, 100) farm_data["layout_y"] = np.zeros(5) Farm.from_dict(farm_data) assert len(farm.turbine_type) == 5 - assert len(farm.turbine_definitions) == 5 + assert len(farm.turbines) == 5 def test_farm_external_library(sample_inputs_fixture: SampleInputs): @@ -190,34 +175,67 @@ def test_farm_external_library(sample_inputs_fixture: SampleInputs): # Demonstrate a passing case farm_data = deepcopy(SampleInputs().farm) - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = ["nrel_5MW_custom"] * N_TURBINES farm = Farm.from_dict(farm_data) - assert farm.turbine_library_path == external_library + assert farm.external_turbine_library_path == external_library # Demonstrate a file not existing in the user library, but exists in the internal library, so # the loading is successful - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = ["iea_10MW"] * N_TURBINES farm = Farm.from_dict(farm_data) - assert farm.turbine_definitions[0]["turbine_type"] == "iea_10MW" + assert farm.turbines[0].turbine_type == "iea_10MW" # Demonstrate a failing case with an incorrect library location - farm_data["turbine_library_path"] = external_library / "turbine_library_path" + farm_data["external_turbine_library_path"] = external_library / "turbine_library_path" with pytest.raises(FileExistsError): Farm.from_dict(farm_data) # Demonstrate a failing case where there is a duplicated turbine between the internal # and external turbine libraries farm_data = deepcopy(SampleInputs().farm) - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = ["nrel_5MW"] * N_TURBINES with pytest.raises(ValueError): Farm.from_dict(farm_data) # Demonstrate a failing case where there a turbine does not exist in either farm_data = deepcopy(SampleInputs().farm) - farm_data["turbine_library_path"] = external_library + farm_data["external_turbine_library_path"] = external_library farm_data["turbine_type"] = ["FAKE_TURBINE"] * N_TURBINES with pytest.raises(FileNotFoundError): Farm.from_dict(farm_data) + +def test_turbine_outputs(): + farm_data = SampleInputs().farm + turbine_data = SampleInputs().turbine + + layout_x = farm_data["layout_x"] + layout_y = farm_data["layout_y"] + + farm = Farm( + layout_x=layout_x, + layout_y=layout_y, + turbine_type=[turbine_data] + ) + + # "normal" order; switched order + powers_orig = np.array([[100, 200, 300], [100, 200, 300]]) + sorted_indices = np.array([[0, 1, 2], [1, 2, 0]]) + farm.set_sorted_indices(sorted_indices) + farm.initialize() + + assert farm.turbine_powers_sorted.shape == sorted_indices.shape + + # First, set powers in sorted order and return "unsorted" + powers_test = np.take_along_axis(powers_orig, sorted_indices, axis=1) + farm.turbine_powers_sorted = powers_test + + assert (farm.turbine_powers == powers_orig).all() + + # Now, use built-in method to set the "unsorted" turbine powers + farm.turbine_powers_sorted = np.full(powers_orig.shape, np.nan) + farm.set_turbine_outputs_by_original_ordering(powers=powers_orig) + assert (farm.turbine_powers == powers_orig).all() + assert (farm.turbine_powers_sorted == powers_test).all() diff --git a/tests/floris_model_integration_test.py b/tests/floris_model_integration_test.py index 0f22eeff89..181a4831db 100644 --- a/tests/floris_model_integration_test.py +++ b/tests/floris_model_integration_test.py @@ -300,7 +300,7 @@ def test_disable_turbines(): # Set to mixed turbine model with open( str( - fmodel.core.as_dict()["farm"]["turbine_library_path"] + fmodel.core.as_dict()["farm"]["external_turbine_library_path"] / (fmodel.core.as_dict()["farm"]["turbine_type"][0] + ".yaml") ) ) as t: @@ -728,64 +728,64 @@ def test_get_powers_with_wind_data(): assert np.allclose(farm_power_weighted, fmodel.get_turbine_powers()[:,:,:-1].sum(axis=2)) -def test_get_and_set_param(): +def test_get_and_set_wake_parameter(): fmodel = FlorisModel(configuration=YAML_INPUT) - # Get the wind speed - wind_speeds = fmodel.get_param(['flow_field', 'wind_speeds']) - assert wind_speeds[0] == 8.0 - - # Set the wind speed - fmodel.set_param(['flow_field', 'wind_speeds'], 10.0, param_idx=0) - wind_speed = fmodel.get_param(['flow_field', 'wind_speeds'], param_idx=0 ) - assert wind_speed == 10.0 - - # Repeat with wake parameter - fmodel.set_param(['wake', 'wake_velocity_parameters', 'gauss', 'alpha'], 0.1) - alpha = fmodel.get_param(['wake', 'wake_velocity_parameters', 'gauss', 'alpha']) + # Wake parameter + fmodel.set_wake_parameter("alpha", 0.1) + alpha = fmodel.get_wake_parameter("alpha") assert alpha == 0.1 def test_get_operation_model(): fmodel = FlorisModel(configuration=YAML_INPUT) - assert fmodel.get_operation_model() == "cosine-loss" + assert fmodel.get_operation_model()[0].__class__.__name__ == "CosineLossTurbine" def test_set_operation_model(): fmodel = FlorisModel(configuration=YAML_INPUT) fmodel.set_operation_model("simple-derating") - assert fmodel.get_operation_model() == "simple-derating" + assert fmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" reference_wind_height = fmodel.reference_wind_height # Check multiple turbine types works fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) fmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert fmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + assert ( + [om.__class__.__name__ for om in fmodel.get_operation_model()] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) # Check that setting a single turbine type, and then altering the operation model works fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") - assert fmodel.get_operation_model() == "simple-derating" + assert fmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" # Check that setting over mutliple turbine types works fmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") - assert fmodel.get_operation_model() == "simple-derating" + assert fmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" fmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert fmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + assert ( + [om.__class__.__name__ for om in fmodel.get_operation_model()] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) # Check setting over single turbine type; then updating layout works fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model("simple-derating") fmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) - assert fmodel.get_operation_model() == "simple-derating" + assert fmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" # Check that setting for multiple turbine types and then updating layout breaks fmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) fmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) fmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert fmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + assert ( + [om.__class__.__name__ for om in fmodel.get_operation_model()] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) with pytest.raises(ValueError): fmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) diff --git a/tests/geometric_yaw_unit_test.py b/tests/geometric_yaw_unit_test.py index 61edafc45d..6ff534a2e3 100644 --- a/tests/geometric_yaw_unit_test.py +++ b/tests/geometric_yaw_unit_test.py @@ -1,6 +1,5 @@ import numpy as np -import pandas as pd from floris import FlorisModel from floris.optimization.yaw_optimization.yaw_optimizer_geometric import ( @@ -10,8 +9,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" # Inputs for basic yaw optimizations WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] @@ -26,8 +24,7 @@ def test_basic_optimization(sample_inputs_fixture): The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw optimization scheme. This test checks basic properties of the optimization result. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) @@ -73,8 +70,8 @@ def test_disabled_turbines(sample_inputs_fixture): is not too large. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) diff --git a/tests/grid_unit_test.py b/tests/grid_unit_test.py new file mode 100644 index 0000000000..7978d2ae07 --- /dev/null +++ b/tests/grid_unit_test.py @@ -0,0 +1,106 @@ +import logging + +import numpy as np +import pytest + +from floris.core import ( + Core, + FlowFieldPlanarGrid, + TurbineGrid, +) + + +def test_turbine_grid_init(caplog): + + # Basic instantiation + TurbineGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + grid_resolution=2 + ) + + # Invalid grid_resolution should raise TypeError + with pytest.raises(TypeError): + TurbineGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + grid_resolution=2.5 + ) + with pytest.raises(TypeError): + TurbineGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + grid_resolution=[2, 2] + ) + + # Invalid z value raises warning + with caplog.at_level(logging.WARNING): + TurbineGrid( + turbine_coordinates=np.array([[0.0, 0.0, 0.0]]), # z = 0 + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + grid_resolution=2 + ) + assert "Non-positive z coordinates detected" in caplog.text + caplog.clear() + with caplog.at_level(logging.WARNING): + TurbineGrid( + turbine_coordinates=np.array([[0.0, 0.0, -1]]), # z < 0 + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + grid_resolution=2 + ) + assert "Non-positive z coordinates detected" in caplog.text + caplog.clear() + +def test_flow_field_planar_grid_init(): + + # Basic instantiation + FlowFieldPlanarGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + normal_vector="x", + planar_coordinate=0.0, + grid_resolution=[2, 2], + x1_bounds=None, + x2_bounds=None, + ) + + # Invalid grid_resolution should raise TypeError + with pytest.raises(TypeError): + FlowFieldPlanarGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + normal_vector="x", + planar_coordinate=0.0, + grid_resolution=2, # Invalid type (int instead of list) + x1_bounds=None, + x2_bounds=None, + ) + with pytest.raises(TypeError): + FlowFieldPlanarGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + normal_vector="x", + planar_coordinate=0.0, + grid_resolution=[2, 2, 3], # Invalid length (should be 2) + x1_bounds=None, + x2_bounds=None, + ) + with pytest.raises(TypeError): + FlowFieldPlanarGrid( + turbine_coordinates=np.array([[0.0, 0.0, 90.0]]), + turbine_diameters=np.array([126.0]), + wind_directions=np.array([270.0]), + normal_vector="x", + planar_coordinate=0.0, + grid_resolution=[2.0, 2.0], # Invalid type in list (must be ints) + x1_bounds=None, + x2_bounds=None, + ) diff --git a/tests/par_floris_model_unit_test.py b/tests/par_floris_model_unit_test.py index d4bc696fae..188bb08324 100644 --- a/tests/par_floris_model_unit_test.py +++ b/tests/par_floris_model_unit_test.py @@ -14,16 +14,14 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" def test_None_interface(sample_inputs_fixture): """ With interface=None, the ParFlorisModel should behave exactly like the FlorisModel. (ParFlorisModel.run() simply calls the parent FlorisModel.run()). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel( @@ -45,8 +43,7 @@ def test_multiprocessing_interface(sample_inputs_fixture): With interface="multiprocessing", the ParFlorisModel should return the same powers as the FlorisModel. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel( @@ -68,8 +65,7 @@ def test_pathos_interface(sample_inputs_fixture): With interface="pathos", the ParFlorisModel should return the same powers as the FlorisModel. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel( @@ -104,8 +100,7 @@ def test_concurrent_interface(sample_inputs_fixture): With interface="concurrent", the ParFlorisModel should return the same powers as the FlorisModel. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel( @@ -140,8 +135,7 @@ def test_return_turbine_powers_only(sample_inputs_fixture): With return_turbine_powers_only=True, the ParFlorisModel should return only the turbine powers, not the full results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel( @@ -163,8 +157,7 @@ def test_run_error(sample_inputs_fixture, caplog): """ Check that an error is raised if an output is requested before calling run(). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) pfmodel = ParFlorisModel( sample_inputs_fixture.core, @@ -190,8 +183,7 @@ def test_configuration_compatibility(sample_inputs_fixture, caplog): UncertainFlorisModel configurations. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) @@ -215,8 +207,7 @@ def test_wind_data_objects(sample_inputs_fixture): Check that the ParFlorisModel is compatible with WindData objects. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel(sample_inputs_fixture.core, max_workers=2) @@ -267,8 +258,7 @@ def test_control_setpoints(sample_inputs_fixture): Check that the ParFlorisModel is compatible with control set points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel = ParFlorisModel(sample_inputs_fixture.core, n_wind_condition_splits=2) @@ -346,8 +336,7 @@ def test_control_setpoints(sample_inputs_fixture): def test_sample_flow_at_points(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) @@ -372,9 +361,7 @@ def test_sample_flow_at_points(sample_inputs_fixture): def test_sample_ti_at_points(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = "crespo_hernandez" + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) @@ -402,8 +389,7 @@ def test_copy(sample_inputs_fixture): Check that the ParFlorisModel copies correctly as a ParFlorisModel. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) pfmodel = ParFlorisModel(sample_inputs_fixture.core, max_workers=2) pfmodel_copy = pfmodel.copy() @@ -488,8 +474,7 @@ def test_multidim_conditions(sample_inputs_fixture): Check that the ParFlorisModel works with multidim_conditions set in the TimeSeries object. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) fmodel.set(turbine_type=[sample_inputs_fixture.turbine_multi_dim]) diff --git a/tests/parallel_floris_model_integration_test.py b/tests/parallel_floris_model_integration_test.py index 21857b5b30..e41825a983 100644 --- a/tests/parallel_floris_model_integration_test.py +++ b/tests/parallel_floris_model_integration_test.py @@ -16,15 +16,14 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" def test_raise_deprecation_warning(sample_inputs_fixture, caplog): """ Test that a warning is raised when instantiating the ParallelFlorisModel. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) @@ -48,8 +47,8 @@ def test_parallel_turbine_powers(sample_inputs_fixture): the serial floris interface. The expected result is that the turbine powers should be exactly the same. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) pfmodel_input = copy.deepcopy(fmodel) @@ -75,8 +74,8 @@ def test_parallel_turbine_powers(sample_inputs_fixture): def test_parallel_get_AEP(sample_inputs_fixture): - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + freq=np.linspace(0, 1, 16)/8 @@ -102,8 +101,8 @@ def test_parallel_uncertain_error(sample_inputs_fixture): """ """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + ufmodel = UncertainFlorisModel( sample_inputs_fixture.core, diff --git a/tests/reg_tests/cumulative_curl_regression_test.py b/tests/reg_tests/cumulative_curl_regression_test.py index c9428b2619..3875a9afe6 100644 --- a/tests/reg_tests/cumulative_curl_regression_test.py +++ b/tests/reg_tests/cumulative_curl_regression_test.py @@ -18,8 +18,7 @@ DEBUG = False -VELOCITY_MODEL = "cc" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "cc" baseline = np.array( [ @@ -189,21 +188,20 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex + turbines = floris.farm.turbines velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -213,48 +211,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -314,8 +301,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -334,7 +321,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -358,8 +345,7 @@ def test_regression_yaw(sample_inputs_fixture): """ Tandem turbines with the upstream turbine yawed """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) @@ -368,7 +354,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -377,7 +363,6 @@ def test_regression_yaw(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -387,48 +372,37 @@ def test_regression_yaw(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -455,12 +429,11 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): correction enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = False - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = False + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True floris = Core.from_dict(sample_inputs_fixture.core) @@ -469,7 +442,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -478,7 +451,6 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -488,48 +460,37 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -555,12 +516,11 @@ def test_regression_secondary_steering(sample_inputs_fixture): Tandem turbines with the upstream turbine yawed and secondary steering enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = True - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = False + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = False floris = Core.from_dict(sample_inputs_fixture.core) @@ -569,7 +529,7 @@ def test_regression_secondary_steering(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -578,7 +538,6 @@ def test_regression_secondary_steering(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -588,48 +547,37 @@ def test_regression_secondary_steering(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -671,8 +619,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -685,31 +633,27 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -732,8 +676,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/empirical_gauss_regression_test.py b/tests/reg_tests/empirical_gauss_regression_test.py index e664881dff..b830e1f9f6 100644 --- a/tests/reg_tests/empirical_gauss_regression_test.py +++ b/tests/reg_tests/empirical_gauss_regression_test.py @@ -1,4 +1,6 @@ +import copy + import numpy as np from floris.core import ( @@ -18,9 +20,7 @@ DEBUG = False -VELOCITY_MODEL = "empirical_gauss" -DEFLECTION_MODEL = "empirical_gauss" -TURBULENCE_MODEL = "wake_induced_mixing" +WAKE_MODEL = "empirical_gauss" baseline = np.array( @@ -221,13 +221,11 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -236,7 +234,6 @@ def test_regression_tandem(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -246,48 +243,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -347,9 +333,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -368,7 +353,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -392,9 +377,7 @@ def test_regression_yaw(sample_inputs_fixture): """ Tandem turbines with the upstream turbine yawed """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) @@ -403,7 +386,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -412,7 +395,6 @@ def test_regression_yaw(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -422,48 +404,37 @@ def test_regression_yaw(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -488,18 +459,22 @@ def test_regression_tilt(sample_inputs_fixture): """ Tandem turbines with the upstream turbine tilted """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) - tilt_angles = np.zeros((N_FINDEX, N_TURBINES)) - tilt_angles[:,0] = 8.0 - floris.farm.tilt_angles = tilt_angles + # Set ref tilt to 0.0 on all turbines + for turb in floris.farm.turbines: + turb.ref_tilt = 0.0 + # Replace first turbine with a new turbine that is tilted at 8 degrees + turb_front = copy.deepcopy(floris.farm.turbines[0]) + turb_front.turbine_type = 'nrel_5mw_tilt8' + turb_front.ref_tilt = 8.0 + floris.farm.turbines[0] = turb_front + floris.farm.construct_turbine_type_map() floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -508,7 +483,6 @@ def test_regression_tilt(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -518,48 +492,37 @@ def test_regression_tilt(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -586,12 +549,10 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): correction enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) # Turn on yaw added recovery - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True # First pass, leave at default value of 0; should then do nothing floris = Core.from_dict(sample_inputs_fixture.core) @@ -601,7 +562,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -610,7 +571,6 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -620,48 +580,37 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -675,8 +624,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): assert_results_arrays(test_results[0:4], yawed_baseline) # Second pass, use nonzero gain - sample_inputs_fixture.core["wake"]["wake_deflection_parameters"]\ - ["empirical_gauss"]["yaw_added_mixing_gain"] = 0.1 + sample_inputs_fixture.core["wake"]["parameters"]["yaw_added_mixing_gain"] = 0.1 floris = Core.from_dict(sample_inputs_fixture.core) @@ -685,7 +633,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -694,7 +642,6 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -704,48 +651,37 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -769,9 +705,7 @@ def test_regression_helix(sample_inputs_fixture): """ Tandem turbines with the upstream turbine applying the helix """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) @@ -781,7 +715,7 @@ def test_regression_helix(sample_inputs_fixture): floris.farm.awc_amplitudes = awc_amplitudes floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -790,7 +724,6 @@ def test_regression_helix(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -800,48 +733,37 @@ def test_regression_helix(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -883,9 +805,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -898,38 +819,22 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field - # farm_eff_velocities = rotor_effective_velocity( - # floris.flow_field.air_density, - # floris.farm.ref_air_densities, - # velocities, - # yaw_angles, - # tilt_angles, - # floris.farm.ref_tilts, - # floris.farm.pPs, - # floris.farm.pTs, - # floris.farm.turbine_tilt_interps, - # floris.farm.correct_cp_ct_for_tilt, - # floris.farm.turbine_type_map, - # ) farm_powers = power( - velocities, - turbulence_intensities, - floris.flow_field.air_density, - floris.farm.turbine_power_functions, - floris.farm.yaw_angles, - floris.farm.tilt_angles, - floris.farm.power_setpoints, - floris.farm.awc_modes, - floris.farm.awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=floris.flow_field.air_density, + yaw_angles=floris.farm.yaw_angles, + power_setpoints=floris.farm.power_setpoints, + awc_modes=floris.farm.awc_modes, + awc_amplitudes=floris.farm.awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -951,9 +856,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = TURBULENCE_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/gauss_regression_test.py b/tests/reg_tests/gauss_regression_test.py index 3c97ee0a16..4e71bbaaae 100644 --- a/tests/reg_tests/gauss_regression_test.py +++ b/tests/reg_tests/gauss_regression_test.py @@ -18,8 +18,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" baseline = np.array( [ @@ -281,12 +280,11 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -295,7 +293,6 @@ def test_regression_tandem(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -305,48 +302,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -406,8 +392,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -427,7 +413,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -451,8 +437,7 @@ def test_regression_yaw(sample_inputs_fixture): """ Tandem turbines with the upstream turbine yawed """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) @@ -461,7 +446,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -470,7 +455,6 @@ def test_regression_yaw(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -480,48 +464,37 @@ def test_regression_yaw(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -547,8 +520,7 @@ def test_regression_gch(sample_inputs_fixture): Tandem turbines with the upstream turbine yawed, yaw added recovery correction enabled, and secondary steering enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) ### With GCH off (via conftest), GCH should be same as Gauss @@ -559,7 +531,7 @@ def test_regression_gch(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -568,7 +540,6 @@ def test_regression_gch(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -578,48 +549,37 @@ def test_regression_gch(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -628,22 +588,22 @@ def test_regression_gch(sample_inputs_fixture): test_results[i, j, 2] = farm_powers[i, j] test_results[i, j, 3] = farm_axial_inductions[i, j] - # Don't use the test values here, gch is off! See the docstring. - # if DEBUG: - # print_test_values( - # farm_avg_velocities, - # farm_cts, - # farm_powers, - # farm_axial_inductions, - # ) + if DEBUG: + print_test_values( + farm_avg_velocities, + farm_cts, + farm_powers, + farm_axial_inductions, + max_findex_print=4, + ) assert_results_arrays(test_results[0:4], yawed_baseline) ### With GCH on, the results should change - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = True - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True floris = Core.from_dict(sample_inputs_fixture.core) @@ -652,7 +612,7 @@ def test_regression_gch(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -661,7 +621,6 @@ def test_regression_gch(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -671,48 +630,37 @@ def test_regression_gch(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -739,12 +687,11 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): correction enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = False - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = False + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = True floris = Core.from_dict(sample_inputs_fixture.core) @@ -753,7 +700,7 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -762,7 +709,6 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -772,48 +718,37 @@ def test_regression_yaw_added_recovery(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -839,12 +774,11 @@ def test_regression_secondary_steering(sample_inputs_fixture): Tandem turbines with the upstream turbine yawed and secondary steering enabled """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) - sample_inputs_fixture.core["wake"]["enable_transverse_velocities"] = True - sample_inputs_fixture.core["wake"]["enable_secondary_steering"] = True - sample_inputs_fixture.core["wake"]["enable_yaw_added_recovery"] = False + sample_inputs_fixture.core["wake"]["parameters"]["enable_transverse_velocities"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_secondary_steering"] = True + sample_inputs_fixture.core["wake"]["parameters"]["enable_yaw_added_recovery"] = False floris = Core.from_dict(sample_inputs_fixture.core) @@ -853,7 +787,7 @@ def test_regression_secondary_steering(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -862,7 +796,6 @@ def test_regression_secondary_steering(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -872,48 +805,37 @@ def test_regression_secondary_steering(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -955,8 +877,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -969,30 +891,26 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes farm_powers = power( - velocities, - turbulence_intensities, - floris.flow_field.air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=floris.flow_field.air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -1015,8 +933,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/jensen_jimenez_regression_test.py b/tests/reg_tests/jensen_jimenez_regression_test.py index 026bfc0c9a..fe17b0b4ad 100644 --- a/tests/reg_tests/jensen_jimenez_regression_test.py +++ b/tests/reg_tests/jensen_jimenez_regression_test.py @@ -18,9 +18,7 @@ DEBUG = False -VELOCITY_MODEL = "jensen" -DEFLECTION_MODEL = "jimenez" - +WAKE_MODEL = "jensen" baseline = np.array( [ @@ -131,12 +129,12 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -145,7 +143,6 @@ def test_regression_tandem(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -155,48 +152,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -256,8 +242,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -276,7 +262,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -300,8 +286,8 @@ def test_regression_yaw(sample_inputs_fixture): """ Tandem turbines with the upstream turbine yawed """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + floris = Core.from_dict(sample_inputs_fixture.core) @@ -310,7 +296,7 @@ def test_regression_yaw(sample_inputs_fixture): floris.farm.yaw_angles = yaw_angles floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -319,7 +305,6 @@ def test_regression_yaw(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -329,48 +314,37 @@ def test_regression_yaw(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -412,8 +386,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -426,14 +400,13 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -452,18 +425,15 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): # floris.farm.turbine_type_map, # ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -484,8 +454,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/none_regression_test.py b/tests/reg_tests/none_regression_test.py index 5f50920cb4..2bd3534e01 100644 --- a/tests/reg_tests/none_regression_test.py +++ b/tests/reg_tests/none_regression_test.py @@ -19,8 +19,7 @@ DEBUG = False -VELOCITY_MODEL = "none" -DEFLECTION_MODEL = "none" +WAKE_MODEL = "none" baseline = np.array( @@ -132,12 +131,12 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -146,7 +145,6 @@ def test_regression_tandem(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -156,48 +154,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -257,8 +244,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -277,7 +264,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -297,24 +284,6 @@ def test_regression_rotation(sample_inputs_fixture): assert np.allclose(t3_270, t2_360) -def test_regression_yaw(sample_inputs_fixture): - """ - Tandem turbines with the upstream turbine yawed - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - floris = Core.from_dict(sample_inputs_fixture.core) - - yaw_angles = np.zeros((N_FINDEX, N_TURBINES)) - yaw_angles[:,0] = 5.0 - floris.farm.yaw_angles = yaw_angles - - floris.initialize_domain() - with pytest.raises(ValueError): - floris.steady_state_atmospheric_condition() - - def test_regression_small_grid_rotation(sample_inputs_fixture): """ This utilizes a 5x5 wind farm with the layout in a regular grid oriented along the cardinal @@ -336,8 +305,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -350,31 +319,27 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -397,8 +362,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/random_search_layout_opt_regression_test.py b/tests/reg_tests/random_search_layout_opt_regression_test.py index d5f3313de2..0a89247940 100644 --- a/tests/reg_tests/random_search_layout_opt_regression_test.py +++ b/tests/reg_tests/random_search_layout_opt_regression_test.py @@ -1,6 +1,5 @@ import numpy as np -import pandas as pd from floris import FlorisModel, WindRose from floris.optimization.layout_optimization.layout_optimization_random_search import ( @@ -12,8 +11,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" locations_baseline_aep = np.array( [ @@ -38,8 +36,8 @@ def test_random_search_layout_opt(sample_inputs_fixture): compares the optimization results from the SciPy layout optimization for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + boundaries = [(0.0, 0.0), (0.0, 1000.0), (1000.0, 1000.0), (1000.0, 0.0), (0.0, 0.0)] @@ -88,8 +86,8 @@ def test_random_search_layout_opt_value(sample_inputs_fixture): the value is much higher when the wind is from the north or south, the turbines are staggered to avoid wake interactions for northerly and southerly winds. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + boundaries = [(0.0, 0.0), (0.0, 400.0), (400.0, 400.0), (400.0, 0.0), (0.0, 0.0)] diff --git a/tests/reg_tests/scipy_layout_opt_regression.py b/tests/reg_tests/scipy_layout_opt_regression.py index 1029dfd764..923714bb8c 100644 --- a/tests/reg_tests/scipy_layout_opt_regression.py +++ b/tests/reg_tests/scipy_layout_opt_regression.py @@ -1,6 +1,5 @@ import numpy as np -import pandas as pd from floris import FlorisModel, WindRose from floris.optimization.layout_optimization.layout_optimization_scipy import ( @@ -12,8 +11,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" baseline = np.array( [ @@ -36,8 +34,8 @@ def test_scipy_layout_opt(sample_inputs_fixture): compares the optimization results from the SciPy layout optimization for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + opt_options = { "maxiter": 5, @@ -81,8 +79,8 @@ def test_scipy_layout_opt_value(sample_inputs_fixture): the value is much higher when the wind is from the north or south, the turbines are staggered to avoid wake interactions for northerly and southerly winds. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + opt_options = { "maxiter": 5, diff --git a/tests/reg_tests/turbopark_regression_test.py b/tests/reg_tests/turbopark_regression_test.py deleted file mode 100644 index f4be3f3845..0000000000 --- a/tests/reg_tests/turbopark_regression_test.py +++ /dev/null @@ -1,450 +0,0 @@ - -import numpy as np - -from floris.core import ( - average_velocity, - axial_induction, - Core, - power, - rotor_effective_velocity, - thrust_coefficient, -) -from tests.conftest import ( - assert_results_arrays, - N_FINDEX, - N_TURBINES, - print_test_values, -) - - -DEBUG = False -VELOCITY_MODEL = "turbopark" -DEFLECTION_MODEL = "gauss" -COMBINATION_MODEL = "fls" - -baseline = np.array( - [ - # 8 m/s - [ - [7.9736858, 0.7871515, 1753954.4591792, 0.2693224], - [6.0332948, 0.8593353, 752557.9240063, 0.3124735], - [5.4029800, 0.8947888, 538370.5108659, 0.3378186], - ], - # 9 m/s - [ - [8.9703965, 0.7858774, 2496427.8618358, 0.2686331], - [6.7887441, 0.8249788, 1092199.1775234, 0.2908223], - [6.0678594, 0.8577634, 768097.7785191, 0.3114286], - ], - # 10 m/s - [ - [9.9671073, 0.7838789, 3417797.0050916, 0.2675559], - [7.5453629, 0.7962514, 1487438.4031455, 0.2743074], - [6.7548552, 0.8265200, 1076963.1412833, 0.2917453], - ], - # 11 m/s - [ - [10.9638180, 0.7565157, 4519404.3072862, 0.2532794], - [8.3436376, 0.7866851, 2027996.3027579, 0.2690699], - [7.4626804, 0.7989174, 1439263.3915910, 0.2757889], - ], - ] -) - - -yawed_baseline = np.array( - [ - # 8 m/s - [ - [7.9736858, 0.7841561, 1741508.6722008, 0.2671213], - [6.0523119, 0.8584704, 761107.7639542, 0.3118979], - [5.4177841, 0.8939472, 543310.4550423, 0.3371713], - ], - # 9 m/s - [ - [8.9703965, 0.7828869, 2480428.8963141, 0.2664440], - [6.8101438, 0.8240055, 1101820.2623232, 0.2902415], - [6.0851644, 0.8569764, 775877.8906008, 0.3109077], - ], - # 10 m/s - [ - [9.9671073, 0.7808960, 3395681.0032992, 0.2653854], - [7.5691494, 0.7955016, 1501458.3309846, 0.2738925], - [6.7745474, 0.8256244, 1085816.5021615, 0.2912085], - ], - # 11 m/s - [ - [10.9638180, 0.7536370, 4488242.9153943, 0.2513413], - [8.3695194, 0.7866518, 2047340.0279521, 0.2690518], - [7.4830530, 0.7982426, 1450966.1620998, 0.2754129], - ], - ] -) - -# Note: compare the yawed vs non-yawed results. The upstream turbine -# power should be lower in the yawed case. The following turbine -# powers should higher in the yawed case. - - -def test_regression_tandem(sample_inputs_fixture): - """ - Tandem turbines - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL - - floris = Core.from_dict(sample_inputs_fixture.core) - floris.initialize_domain() - floris.steady_state_atmospheric_condition() - - n_turbines = floris.farm.n_turbines - n_findex = floris.flow_field.n_findex - - velocities = floris.flow_field.u - turbulence_intensities = floris.flow_field.turbulence_intensity_field - air_density = floris.flow_field.air_density - yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles - power_setpoints = floris.farm.power_setpoints - awc_modes = floris.farm.awc_modes - awc_amplitudes = floris.farm.awc_amplitudes - test_results = np.zeros((n_findex, n_turbines, 4)) - - farm_avg_velocities = average_velocity( - velocities, - ) - farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - for i in range(n_findex): - for j in range(n_turbines): - test_results[i, j, 0] = farm_avg_velocities[i, j] - test_results[i, j, 1] = farm_cts[i, j] - test_results[i, j, 2] = farm_powers[i, j] - test_results[i, j, 3] = farm_axial_inductions[i, j] - - if DEBUG: - print_test_values( - farm_avg_velocities, - farm_cts, - farm_powers, - farm_axial_inductions, - max_findex_print=4, - ) - - assert_results_arrays(test_results[0:4], baseline) - - -def test_regression_rotation(sample_inputs_fixture): - """ - Turbines in tandem and rotated. - The result from 270 degrees should match the results from 360 degrees. - - Wind from the West (Left) - - ^ - | - y - - 1|1 3 - | - | - | - 0|0 2 - |----------| - 0 1 x-> - - - Wind from the North (Top), rotated - - ^ - | - y - - 1|3 2 - | - | - | - 0|1 0 - |----------| - 0 1 x-> - - In 270, turbines 2 and 3 are waked. In 360, turbines 0 and 2 are waked. - The test compares turbines 2 and 3 with 0 and 2 from 270 and 360. - """ - TURBINE_DIAMETER = 126.0 - - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL - sample_inputs_fixture.core["farm"]["layout_x"] = [ - 0.0, - 0.0, - 5 * TURBINE_DIAMETER, - 5 * TURBINE_DIAMETER, - ] - sample_inputs_fixture.core["farm"]["layout_y"] = [ - 0.0, - 5 * TURBINE_DIAMETER, - 0.0, - 5 * TURBINE_DIAMETER - ] - sample_inputs_fixture.core["flow_field"]["wind_directions"] = [270.0, 360.0] - sample_inputs_fixture.core["flow_field"]["wind_speeds"] = [8.0, 8.0] - sample_inputs_fixture.core["flow_field"]["turbulence_intensities"] = [0.1, 0.1] - - floris = Core.from_dict(sample_inputs_fixture.core) - floris.initialize_domain() - floris.steady_state_atmospheric_condition() - - farm_avg_velocities = average_velocity(floris.flow_field.u) - - t0_270 = farm_avg_velocities[0, 0] # upstream - t1_270 = farm_avg_velocities[0, 1] # upstream - t2_270 = farm_avg_velocities[0, 2] # waked - t3_270 = farm_avg_velocities[0, 3] # waked - - t0_360 = farm_avg_velocities[1, 0] # waked - t1_360 = farm_avg_velocities[1, 1] # upstream - t2_360 = farm_avg_velocities[1, 2] # waked - t3_360 = farm_avg_velocities[1, 3] # upstream - - assert np.allclose(t0_270, t1_360) - assert np.allclose(t1_270, t3_360) - assert np.allclose(t2_270, t0_360) - assert np.allclose(t3_270, t2_360) - - -def test_regression_yaw(sample_inputs_fixture): - """ - Tandem turbines with the upstream turbine yawed - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - floris = Core.from_dict(sample_inputs_fixture.core) - - yaw_angles = np.zeros((N_FINDEX, N_TURBINES)) - yaw_angles[:,0] = 5.0 - floris.farm.yaw_angles = yaw_angles - - floris.initialize_domain() - floris.steady_state_atmospheric_condition() - - n_turbines = floris.farm.n_turbines - n_findex = floris.flow_field.n_findex - - velocities = floris.flow_field.u - turbulence_intensities = floris.flow_field.turbulence_intensity_field - air_density = floris.flow_field.air_density - yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles - power_setpoints = floris.farm.power_setpoints - awc_modes = floris.farm.awc_modes - awc_amplitudes = floris.farm.awc_amplitudes - test_results = np.zeros((n_findex, n_turbines, 4)) - - farm_avg_velocities = average_velocity( - velocities, - ) - farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - for i in range(n_findex): - for j in range(n_turbines): - test_results[i, j, 0] = farm_avg_velocities[i, j] - test_results[i, j, 1] = farm_cts[i, j] - test_results[i, j, 2] = farm_powers[i, j] - test_results[i, j, 3] = farm_axial_inductions[i, j] - - if DEBUG: - print_test_values( - farm_avg_velocities, - farm_cts, - farm_powers, - farm_axial_inductions, - max_findex_print=4, - ) - - assert_results_arrays(test_results[0:4], yawed_baseline) - -def test_regression_small_grid_rotation(sample_inputs_fixture): - """ - Where wake models are masked based on the x-location of a turbine, numerical precision - can cause masking to fail unexpectedly. For example, in the configuration here one of - the turbines has these delta x values; - - [[4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13] - [4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13] - [4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13] - [4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13] - [4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13 4.54747351e-13]] - - and therefore the masking statement is False when it should be True. This causes the current - turbine to be affected by its own wake. This test requires that at least in this particular - configuration the masking correctly filters grid points. - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL - X, Y = np.meshgrid( - 6.0 * 126.0 * np.arange(0, 5, 1), - 6.0 * 126.0 * np.arange(0, 5, 1) - ) - X = X.flatten() - Y = Y.flatten() - - sample_inputs_fixture.core["farm"]["layout_x"] = X - sample_inputs_fixture.core["farm"]["layout_y"] = Y - - floris = Core.from_dict(sample_inputs_fixture.core) - floris.initialize_domain() - floris.steady_state_atmospheric_condition() - - # farm_avg_velocities = average_velocity(floris.flow_field.u) - velocities = floris.flow_field.u - turbulence_intensities = floris.flow_field.turbulence_intensity_field - yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles - power_setpoints = floris.farm.power_setpoints - awc_modes = floris.farm.awc_modes - awc_amplitudes = floris.farm.awc_amplitudes - - farm_powers = power( - velocities, - turbulence_intensities, - floris.flow_field.air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - - # A "column" is oriented parallel to the wind direction - # Columns 1 - 4 should have the same power profile - # Column 5 leading turbine is completely unwaked - # and the rest of the turbines have a partial wake from their immediate upstream turbine - assert np.allclose(farm_powers[8,0:5], farm_powers[8,5:10]) - assert np.allclose(farm_powers[8,0:5], farm_powers[8,10:15]) - assert np.allclose(farm_powers[8,0:5], farm_powers[8,15:20]) - assert np.allclose(farm_powers[8,20], farm_powers[8,0]) - assert np.allclose(farm_powers[8,21], farm_powers[8,21:25]) - -''' -## Not implemented in TurbOPark -def test_full_flow_solver(sample_inputs_fixture): - """ - Full flow solver test with the flow field planar grid. - This requires one wind condition, and the grid is deliberately coarse to allow for - visually comparing results, as needed. - The u-component of velocity is compared, and the array has the shape - (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). - """ - - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["solver"] = { - "type": "flow_field_planar_grid", - "normal_vector": "z", - "planar_coordinate": sample_inputs_fixture.core["farm"]["turbine_type"][0]["hub_height"], - "flow_field_grid_points": [5, 5], - "flow_field_bounds": [None, None], - } - sample_inputs_fixture.core["flow_field"]["wind_directions"] = [270.0] - sample_inputs_fixture.core["flow_field"]["wind_speeds"] = [8.0] - - floris = Core.from_dict(sample_inputs_fixture.core) - floris.solve_for_viz() - - velocities = floris.flow_field.u_sorted - print(velocities) - assert_results_arrays(velocities, full_flow_baseline) -''' diff --git a/tests/reg_tests/turboparkgauss_regression_test.py b/tests/reg_tests/turboparkgauss_regression_test.py index 1548b71440..94342b1b56 100644 --- a/tests/reg_tests/turboparkgauss_regression_test.py +++ b/tests/reg_tests/turboparkgauss_regression_test.py @@ -18,9 +18,7 @@ DEBUG = False -VELOCITY_MODEL = "turboparkgauss" -DEFLECTION_MODEL = "gauss" -COMBINATION_MODEL = "sosfs" +WAKE_MODEL = "turboparkgauss" baseline = np.array( [ @@ -28,55 +26,25 @@ [ [7.9736858, 0.7871515, 1753954.4591792, 0.2693224], [5.3669227, 0.8968386, 526338.6265211, 0.3394063], - [4.7291434, 0.9398463, 342625.1907593, 0.3773687], + [4.7093471, 0.9414651, 338146.6902995, 0.3790301], ], # 9 m/s [ [8.9703965, 0.7858774, 2496427.8618358, 0.2686331], [6.0385619, 0.8590958, 754925.9561188, 0.3123139], - [5.2198714, 0.9051982, 477269.3475684, 0.3460505], + [5.1942704, 0.9066535, 468726.5982167, 0.3472367], ], # 10 m/s [ [9.9671073, 0.7838789, 3417797.0050916, 0.2675559], [6.7109723, 0.8285157, 1057233.8964038, 0.2929467], - [5.7609373, 0.8744397, 657816.5966079, 0.3228276], + [5.7307698, 0.8761547, 647750.0520513, 0.3240417], ], # 11 m/s [ [10.9638180, 0.7565157, 4519404.3072862, 0.2532794], [7.4177796, 0.8004049, 1413470.6329668, 0.2766196], - [6.3467168, 0.8450814, 893468.8191848, 0.3032015], - ], - ] -) - - -yawed_baseline = np.array( - [ - # 8 m/s - [ - [7.9736858, 0.7841561, 1741508.6722008, 0.2671213], - [5.3686096, 0.8967427, 526901.4969868, 0.3393316], - [4.7296392, 0.9398058, 342737.3617937, 0.3773274], - ], - # 9 m/s - [ - [8.9703965, 0.7828869, 2480428.8963141, 0.2664440], - [6.0405714, 0.8590044, 755829.3886024, 0.3122531], - [5.2206194, 0.9051556, 477518.9548881, 0.3460159], - ], - # 10 m/s - [ - [9.9671073, 0.7808960, 3395681.0032992, 0.2653854], - [6.7133964, 0.8284054, 1058323.7446597, 0.2928801], - [5.7619351, 0.8743830, 658149.5244311, 0.3227875], - ], - # 11 m/s - [ - [10.9638180, 0.7536370, 4488242.9153943, 0.2513413], - [7.4229011, 0.8002352, 1416412.6499511, 0.2765247], - [6.3490875, 0.8449736, 894534.6529145, 0.3031330], + [6.3151278, 0.8465180, 879266.7640038, 0.3041161], ], ] ) @@ -85,11 +53,11 @@ [ [ [ - [7.88772361, 8. , 8.10178821], - [7.88772361, 8. , 8.10178821], - [7.88772361, 8. , 8.10178821], - [7.88772361, 8. , 8.10178821], - [7.88772361, 8. , 8.10178821], + [7.88772361, 8., 8.10178821], + [7.88772361, 8., 8.10178821], + [7.88772361, 8., 8.10178821], + [7.88772361, 8., 8.10178821], + [7.88772361, 8., 8.10178821], ], [ [7.88772229, 7.99999863, 8.10178685], @@ -99,25 +67,25 @@ [7.88772229, 7.99999863, 8.10178685], ], [ - [7.88768632, 7.99996148, 8.1017499 ], - [7.66326846, 7.7681154 , 7.87123883], - [3.69538982, 3.66849132, 3.79562999], - [7.66326846, 7.7681154 , 7.87123883], - [7.88768632, 7.99996148, 8.1017499 ], + [7.88768866, 7.99996389, 8.1017523], + [7.66496443, 7.76984581, 7.87298101], + [3.67167327, 3.64367424, 3.771272], + [7.66496443, 7.76984581, 7.87298101], + [7.88768866, 7.99996389, 8.1017523], ], [ - [7.88740669, 7.99967377, 8.10146266], - [7.50793067, 7.6089272 , 7.71165714], - [3.64994795, 3.63535913, 3.74869 ], - [7.50793067, 7.6089272 , 7.71165714], - [7.88740669, 7.99967377, 8.10146266], + [7.88743244, 7.99970024, 8.10148911], + [7.51202294, 7.61308124, 7.71586343], + [3.59890431, 3.58223279, 3.69628297], + [7.51202294, 7.61308124, 7.71586343], + [7.88743244, 7.99970024, 8.10148911], ], [ - [7.88664826, 7.99889554, 8.10068331], - [7.44424308, 7.54429736, 7.64614946], - [4.32643439, 4.33927499, 4.44299895], - [7.44424308, 7.54429736, 7.64614946], - [7.88664826, 7.99889554, 8.10068331], + [7.88684976, 7.99910218, 8.1008904], + [7.45425328, 7.55442766, 7.6564573], + [4.22344363, 4.23243562, 4.33734116], + [7.45425328, 7.55442766, 7.6564573], + [7.88684976, 7.99910218, 8.1008904] ] ] ] @@ -132,13 +100,11 @@ def test_regression_tandem(sample_inputs_fixture): """ Tandem turbines """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() n_turbines = floris.farm.n_turbines n_findex = floris.flow_field.n_findex @@ -147,7 +113,6 @@ def test_regression_tandem(sample_inputs_fixture): turbulence_intensities = floris.flow_field.turbulence_intensity_field air_density = floris.flow_field.air_density yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes @@ -157,48 +122,37 @@ def test_regression_tandem(sample_inputs_fixture): velocities, ) farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) for i in range(n_findex): for j in range(n_turbines): @@ -258,9 +212,8 @@ def test_regression_rotation(sample_inputs_fixture): """ TURBINE_DIAMETER = 126.0 - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["farm"]["layout_x"] = [ 0.0, 0.0, @@ -279,7 +232,7 @@ def test_regression_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() farm_avg_velocities = average_velocity(floris.flow_field.u) @@ -298,101 +251,6 @@ def test_regression_rotation(sample_inputs_fixture): assert np.allclose(t2_270, t0_360) assert np.allclose(t3_270, t2_360) - -def test_regression_yaw(sample_inputs_fixture): - """ - Tandem turbines with the upstream turbine yawed - """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - - floris = Core.from_dict(sample_inputs_fixture.core) - - yaw_angles = np.zeros((N_FINDEX, N_TURBINES)) - yaw_angles[:,0] = 5.0 - floris.farm.yaw_angles = yaw_angles - - floris.initialize_domain() - floris.steady_state_atmospheric_condition() - - n_turbines = floris.farm.n_turbines - n_findex = floris.flow_field.n_findex - - velocities = floris.flow_field.u - turbulence_intensities = floris.flow_field.turbulence_intensity_field - air_density = floris.flow_field.air_density - yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles - power_setpoints = floris.farm.power_setpoints - awc_modes = floris.farm.awc_modes - awc_amplitudes = floris.farm.awc_amplitudes - test_results = np.zeros((n_findex, n_turbines, 4)) - - farm_avg_velocities = average_velocity( - velocities, - ) - farm_cts = thrust_coefficient( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_thrust_coefficient_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_powers = power( - velocities, - turbulence_intensities, - air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - farm_axial_inductions = axial_induction( - velocities, - turbulence_intensities, - air_density, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_axial_induction_functions, - floris.farm.turbine_tilt_interps, - floris.farm.correct_cp_ct_for_tilt, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, - ) - for i in range(n_findex): - for j in range(n_turbines): - test_results[i, j, 0] = farm_avg_velocities[i, j] - test_results[i, j, 1] = farm_cts[i, j] - test_results[i, j, 2] = farm_powers[i, j] - test_results[i, j, 3] = farm_axial_inductions[i, j] - - if DEBUG: - print_test_values( - farm_avg_velocities, - farm_cts, - farm_powers, - farm_axial_inductions, - max_findex_print=4, - ) - - assert_results_arrays(test_results[0:4], yawed_baseline) - def test_regression_small_grid_rotation(sample_inputs_fixture): """ Where wake models are masked based on the x-location of a turbine, numerical precision @@ -409,9 +267,8 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): turbine to be affected by its own wake. This test requires that at least in this particular configuration the masking correctly filters grid points. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["combination_model"] = COMBINATION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + X, Y = np.meshgrid( 6.0 * 126.0 * np.arange(0, 5, 1), 6.0 * 126.0 * np.arange(0, 5, 1) @@ -424,30 +281,26 @@ def test_regression_small_grid_rotation(sample_inputs_fixture): floris = Core.from_dict(sample_inputs_fixture.core) floris.initialize_domain() - floris.steady_state_atmospheric_condition() + floris.solve_for_turbines() # farm_avg_velocities = average_velocity(floris.flow_field.u) velocities = floris.flow_field.u turbulence_intensities = floris.flow_field.turbulence_intensity_field yaw_angles = floris.farm.yaw_angles - tilt_angles = floris.farm.tilt_angles power_setpoints = floris.farm.power_setpoints awc_modes = floris.farm.awc_modes awc_amplitudes = floris.farm.awc_amplitudes farm_powers = power( - velocities, - turbulence_intensities, - floris.flow_field.air_density, - floris.farm.turbine_power_functions, - yaw_angles, - tilt_angles, - power_setpoints, - awc_modes, - awc_amplitudes, - floris.farm.turbine_tilt_interps, - floris.farm.turbine_type_map, - floris.farm.turbine_power_thrust_tables, + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=floris.flow_field.air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, ) # A "column" is oriented parallel to the wind direction @@ -470,8 +323,8 @@ def test_full_flow_solver(sample_inputs_fixture): (n_findex, n_turbines, n grid points in x, n grid points in y, 3 grid points in z). """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + sample_inputs_fixture.core["solver"] = { "type": "flow_field_planar_grid", "normal_vector": "z", diff --git a/tests/reg_tests/turbulence_models_regression_test.py b/tests/reg_tests/turbulence_models_regression_test.py deleted file mode 100644 index 42e63be411..0000000000 --- a/tests/reg_tests/turbulence_models_regression_test.py +++ /dev/null @@ -1,30 +0,0 @@ -from floris.core import Core -from floris.core.wake_turbulence import NoneWakeTurbulence - - -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" - -def test_NoneWakeTurbulence(sample_inputs_fixture): - - turbulence_intensities = [0.1, 0.05] - - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["turbulence_model"] = "none" - sample_inputs_fixture.core["farm"]["layout_x"] = [0.0, 0.0, 600.0, 600.0] - sample_inputs_fixture.core["farm"]["layout_y"] = [0.0, 600.0, 0.0, 600.0] - sample_inputs_fixture.core["flow_field"]["wind_directions"] = [270.0, 360.0] - sample_inputs_fixture.core["flow_field"]["wind_speeds"] = [8.0, 8.0] - sample_inputs_fixture.core["flow_field"]["turbulence_intensities"] = turbulence_intensities - - core = Core.from_dict(sample_inputs_fixture.core) - core.initialize_domain() - core.steady_state_atmospheric_condition() - - assert ( - core.flow_field.turbulence_intensity_field_sorted[0,:] == turbulence_intensities[0] - ).all() - assert ( - core.flow_field.turbulence_intensity_field_sorted[1,:] == turbulence_intensities[1] - ).all() diff --git a/tests/reg_tests/yaw_optimization_regression_test.py b/tests/reg_tests/yaw_optimization_regression_test.py index d87d40b397..419305f80a 100644 --- a/tests/reg_tests/yaw_optimization_regression_test.py +++ b/tests/reg_tests/yaw_optimization_regression_test.py @@ -11,8 +11,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" # These inputs and baseline power are common for all optimization methods WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] @@ -77,8 +76,8 @@ def test_serial_refine(sample_inputs_fixture): optimization scheme. This test compares the optimization results from the SR method for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) wd_array = np.arange(0.0, 360.0, 90.0) @@ -110,8 +109,8 @@ def test_geometric_yaw(sample_inputs_fixture): optimal yaw relationships. This test compares the optimization results from the Geometric Yaw optimization for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) wd_array = np.arange(0.0, 360.0, 90.0) @@ -152,8 +151,8 @@ def test_scipy_yaw_opt(sample_inputs_fixture): compares the optimization results from the SciPy yaw optimization for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + opt_options = { "maxiter": 5, diff --git a/tests/rotor_velocity_unit_test.py b/tests/rotor_velocity_unit_test.py index ab6250e4a4..72c66dbc2e 100644 --- a/tests/rotor_velocity_unit_test.py +++ b/tests/rotor_velocity_unit_test.py @@ -228,9 +228,8 @@ def test_compute_tilt_angles_for_floating_turbines(): # Multiple turbines tilt_N_turbines = compute_tilt_angles_for_floating_turbines_map( + turbines=[turbine_floating]*N_TURBINES, turbine_type_map=np.array(turbine_type_map), - tilt_angles=5.0*np.ones((1, N_TURBINES)), - tilt_interps={turbine_floating.turbine_type: turbine_floating.tilt_interp}, rotor_effective_velocities=rotor_effective_velocities_N_TURBINES, ) diff --git a/tests/serial_refine_unit_test.py b/tests/serial_refine_unit_test.py index cfda030a7c..d3542f8391 100644 --- a/tests/serial_refine_unit_test.py +++ b/tests/serial_refine_unit_test.py @@ -1,14 +1,12 @@ import numpy as np -import pandas as pd from floris import FlorisModel from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" # Inputs for basic yaw optimizations WIND_DIRECTIONS = [0.0, 90.0, 180.0, 270.0] @@ -23,8 +21,7 @@ def test_basic_optimization(sample_inputs_fixture): The Serial Refine (SR) method optimizes yaw angles based on a sequential, iterative yaw optimization scheme. This test checks basic properties of the optimization result. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) @@ -67,8 +64,8 @@ def test_disabled_turbines(sample_inputs_fixture): is not too large. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) + fmodel = FlorisModel(sample_inputs_fixture.core) diff --git a/tests/turbine_multi_dim_unit_test.py b/tests/turbine_multi_dim_unit_test.py index 0a0f2882a2..3f52adc04a 100644 --- a/tests/turbine_multi_dim_unit_test.py +++ b/tests/turbine_multi_dim_unit_test.py @@ -2,7 +2,6 @@ from pathlib import Path import numpy as np -import pandas as pd import pytest from floris.core import ( @@ -23,29 +22,6 @@ INDEX_FILTER = [0, 2] -# NOTE: MultiDimensionalPowerThrustTable not used anywhere, so I'm commenting -# this out. - -# def test_multi_dimensional_power_thrust_table(): -# turbine_data = SampleInputs().turbine_multi_dim -# turbine_data["power_thrust_data_file"] = CSV_INPUT -# df_data = pd.read_csv(turbine_data["power_thrust_data_file"]) -# flattened_dict = MultiDimensionalPowerThrustTable.from_dataframe(df_data) -# flattened_dict_base = { -# ('Tp', '2', 'Hs', '1'): [], -# ('Tp', '2', 'Hs', '5'): [], -# ('Tp', '4', 'Hs', '1'): [], -# ('Tp', '4', 'Hs', '5'): [], -# } -# assert flattened_dict == flattened_dict_base - -# # Test for initialization errors -# for el in ("ws", "Cp", "Ct"): -# df_data = pd.read_csv(turbine_data["power_thrust_data_file"]) -# df = df_data.drop(el, axis=1) -# with pytest.raises(ValueError): -# MultiDimensionalPowerThrustTable.from_dataframe(df) - def test_turbine_init(): turbine_data = SampleInputs().turbine_multi_dim @@ -77,23 +53,23 @@ def test_ct(): turbine_type_map = turbine_type_map[None, :] condition = {"Tp":2, "Hs":1} + # Force a 5 degree yaw angle + turbine.ref_tilt = 5.0 + turbine.correct_cp_ct_for_tilt = False + # Single turbine # yaw angle / fCt are (n wind direction, n wind speed, n turbine) wind_speed = 10.0 thrust = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT,\ awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, 1)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition ) @@ -102,6 +78,7 @@ def test_ct(): # Multiple turbines with index filter # 4 turbines with 3 x 3 grid arrays thrusts = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 16 x 4 x 3 x 3 turbulence_intensities=( 0.06 * np.ones((N_TURBINES, 3, 3)) @@ -109,16 +86,11 @@ def test_ct(): ), air_density=None, yaw_angles=np.zeros((1, N_TURBINES)), - tilt_angles=np.ones((1, N_TURBINES)) * 5.0, power_setpoints=np.ones((1, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, N_TURBINES)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False] * N_TURBINES]), turbine_type_map=turbine_type_map, ix_filter=INDEX_FILTER, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition ) assert len(thrusts[0]) == len(INDEX_FILTER) @@ -156,21 +128,21 @@ def test_power(): condition = {"Tp":2, "Hs":1} condition_tuple = tuple(condition[k] for k in condition.keys()) + # Use reference tilt angle for testing power + turbine.correct_cp_ct_for_tilt = False + # Single turbine wind_speed = 10.0 p = power( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=AIR_DENSITY, - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, 1)), # 1 findex, 1 turbine - tilt_angles=turbine.power_thrust_table[condition_tuple]["ref_tilt"] * np.ones((1, 1)), power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, 1)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition, ) @@ -178,25 +150,23 @@ def test_power(): np.testing.assert_allclose(p, power_truth) - # Multiple turbines with ix filter + # Multiple turbines with ix filter, switch to different tilt angle + turbine.ref_tilt = 5.0 velocities = np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST p = power( + turbines=[turbine] * N_TURBINES, velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 16 x 4 x 3 x 3 turbulence_intensities=( 0.06 * np.ones((N_TURBINES, 3, 3)) * np.ones_like(WIND_CONDITION_BROADCAST) ), air_density=AIR_DENSITY, - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, N_TURBINES)), - tilt_angles=np.ones((1, N_TURBINES)) * 5.0, power_setpoints=np.ones((1, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, N_TURBINES)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, ix_filter=INDEX_FILTER, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition ) assert len(p[0]) == len(INDEX_FILTER) @@ -222,30 +192,31 @@ def test_axial_induction(): turbine_type_map = turbine_type_map[None, :] condition = {"Tp":2, "Hs":1} + # Force a 5 degree yaw angle + turbine.ref_tilt = 5.0 + turbine.correct_cp_ct_for_tilt = False + baseline_ai = np.array([[0.26551081]]) # Single turbine wind_speed = 10.0 ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints = np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, 1)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map[0,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition ) np.testing.assert_allclose(ai, baseline_ai) # Multiple turbines with ix filter ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 16 x 4 x 3 x 3 turbulence_intensities=( 0.06 * np.ones((N_TURBINES, 3, 3)) @@ -253,16 +224,11 @@ def test_axial_induction(): ), air_density=None, yaw_angles=np.zeros((1, N_TURBINES)), - tilt_angles=np.ones((1, N_TURBINES)) * 5.0, power_setpoints=np.ones((1, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, N_TURBINES)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False] * N_TURBINES]), turbine_type_map=turbine_type_map, ix_filter=INDEX_FILTER, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=condition ) @@ -292,120 +258,103 @@ def test_multiple_conditions(): turbine_type_map = np.array(N_TURBINES * [turbine.turbine_type]) turbine_type_map = turbine_type_map[None, :] + # Force a 5 degree yaw angle + ref_tilt_orig = turbine.power_thrust_table[(2,1)]["ref_tilt"] + ref_tilt_test = 5.0 + turbine.ref_tilt = ref_tilt_test + turbine.correct_cp_ct_for_tilt = False + # First, test the same condition repeated conditions = {"Tp":[2, 2], "Hs":[1, 1]} # Single turbine wind_speed = 10.0 thrust = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(thrust, 0.77958497) ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(ai, 0.26551081) + # Set original reference tilt angle + turbine.ref_tilt = ref_tilt_orig p = power( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=1.225, - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=turbine.power_thrust_table[(2,1)]["ref_tilt"] * np.ones( - (N_CONDITIONS, N_TURBINES) - ), # Same ref_tilt on all power_thrust_tables power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions, - correct_cp_ct_for_tilt=np.zeros((N_CONDITIONS, N_TURBINES), dtype=bool) ) assert np.allclose(p, 12424759.67683091) # Next, test different conditions (one which must be inferred) + turbine.ref_tilt = ref_tilt_test conditions = {"Tp":[2, 4], "Hs":[1, 4]} thrust = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(thrust, np.array([[0.77958497], [0.09744812]])) ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(ai, np.array([[0.26551081], [0.02498745]])) + turbine.ref_tilt = ref_tilt_orig p = power( + turbines=[turbine]*N_TURBINES, velocities=wind_speed * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=1.225, - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=turbine.power_thrust_table[(2,1)]["ref_tilt"] * np.ones( - (N_CONDITIONS, N_TURBINES) - ), # Same ref_tilt on all power_thrust_tables power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions, ) assert np.allclose(p, np.array([[12424759.67683091], [ 1553094.95985386]])) @@ -413,57 +362,46 @@ def test_multiple_conditions(): # Multiple findices with broadcast multidim conditions wind_speeds = np.array([10., 11.]) conditions = {"Tp":2, "Hs":1} + turbine.ref_tilt = ref_tilt_test thrust = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=np.tile(wind_speeds[:,None,None,None], (1, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(thrust, np.array([[0.77958497], [0.66749069]])) ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=np.tile(wind_speeds[:,None,None,None], (1, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=None, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=np.ones((N_CONDITIONS, N_TURBINES)) * 5.0, power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions ) assert np.allclose(ai, np.array([[0.26551081], [0.2118128]])) + turbine.ref_tilt = ref_tilt_orig p = power( + turbines=[turbine]*N_TURBINES, velocities=np.tile(wind_speeds[:,None,None,None], (1, N_TURBINES, 3, 3)), turbulence_intensities=0.06 * np.ones((N_CONDITIONS, N_TURBINES, 3, 3)), air_density=1.225, - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_angles=turbine.power_thrust_table[(2,1)]["ref_tilt"] * np.ones( - (N_CONDITIONS, N_TURBINES) - ), # Same ref_tilt on all power_thrust_tables power_setpoints=np.ones((N_CONDITIONS, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*N_CONDITIONS), awc_amplitudes=np.zeros((N_CONDITIONS, N_TURBINES)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, multidim_condition=conditions, ) assert np.allclose(p, np.array([[12424759.67683091], [15000000.0]])) diff --git a/tests/turbine_operation_models_integration_test.py b/tests/turbine_operation_models_integration_test.py new file mode 100644 index 0000000000..cc852b2f6b --- /dev/null +++ b/tests/turbine_operation_models_integration_test.py @@ -0,0 +1,114 @@ +import numpy as np +import pytest +from attrs import define, field + +from floris import FlorisModel +from floris.core.turbine import BaseOperationModel +from floris.type_dec import floris_float_type + + +# Establish a static class +@define +class UserDefinedStatic(BaseOperationModel): + @staticmethod + def power(velocities, **_): + return 1000*np.ones(velocities.shape[:2]) + @staticmethod + def thrust_coefficient(velocities, **_): + return 0.8*np.ones(velocities.shape[:2]) + @staticmethod + def axial_induction(velocities, **_): + return 1/3*np.ones(velocities.shape[:2]) + +# Establish a dynamic class +@define +class UserDefinedDynamic(BaseOperationModel): + flat_power: floris_float_type = field(init=True, default=500.0) + flat_thrust_coefficient: floris_float_type = field(init=True, default=0.7) + flat_axial_induction: floris_float_type = field(init=True, default=0.3) + def power(self, velocities, **_): + return self.flat_power*np.ones(velocities.shape[:2]) + def thrust_coefficient(self, velocities, **_): + return self.flat_thrust_coefficient*np.ones(velocities.shape[:2]) + def axial_induction(self, velocities, **_): + return self.flat_axial_induction*np.ones(velocities.shape[:2]) + + +def test_static_user_defined_op_model(): + fmodel = FlorisModel("defaults") + fmodel.set( + layout_x=[0.0, 500.0, 1000.0], + layout_y=[0.0, 0.0, 0.0], + wind_speeds=[8.0, 9.0], + wind_directions=[270.0, 280.0], + turbulence_intensities=[0.06, 0.06] + ) + fmodel.set_operation_model(UserDefinedStatic) + fmodel.run() + power = fmodel.get_turbine_powers() + thrust_coefficients = fmodel.get_turbine_thrust_coefficients() + axial_inductions = fmodel.get_turbine_axial_induction_factors() + + assert np.all(power.shape == (2, 3)) + assert np.all(thrust_coefficients.shape == (2, 3)) + assert np.all(axial_inductions.shape == (2, 3)) + + assert np.allclose(power, 1000.0) + assert np.allclose(thrust_coefficients, 0.8) + assert np.allclose(axial_inductions, 1/3) + +def test_dynamic_user_defined_op_model(): + + fmodel = FlorisModel("defaults") + fmodel.set( + layout_x=[0.0, 500.0, 1000.0], + layout_y=[0.0, 0.0, 0.0], + wind_speeds=[8.0, 9.0], + wind_directions=[270.0, 280.0], + turbulence_intensities=[0.06, 0.06] + ) + # Try without instantiating (TODO: create more helpful error?) + with pytest.raises(TypeError): + fmodel.set_operation_model(UserDefinedDynamic) + fmodel.run() + # Now instantiate and try again + instantiated_operation_model = UserDefinedDynamic() + fmodel.set_operation_model(instantiated_operation_model) + fmodel.run() + power = fmodel.get_turbine_powers() + thrust_coefficients = fmodel.get_turbine_thrust_coefficients() + axial_inductions = fmodel.get_turbine_axial_induction_factors() + + assert np.all(power.shape == (2, 3)) + assert np.all(thrust_coefficients.shape == (2, 3)) + assert np.all(axial_inductions.shape == (2, 3)) + + assert np.allclose(power, 500.0) + assert np.allclose(thrust_coefficients, 0.7) + assert np.allclose(axial_inductions, 0.3) + +def test_set_run_ordering(): + fmodel = FlorisModel("defaults") + fmodel.set_operation_model(UserDefinedStatic) + fmodel.set( + layout_x=[0.0, 500.0, 1000.0], + layout_y=[0.0, 0.0, 0.0], + wind_speeds=[8.0, 9.0], + wind_directions=[270.0, 280.0], + turbulence_intensities=[0.06, 0.06] + ) + fmodel.run() + + # Reset, rerun + fmodel.set( + wind_directions=[300.0, 310.0], + ) + fmodel.run() + + # Now, try a dynamic model + fmodel.set_operation_model(UserDefinedDynamic(flat_power=850.0)) + fmodel.run() + fmodel.set( + wind_directions=[240.0, 250.0], + ) + fmodel.run() diff --git a/tests/turbine_unit_test.py b/tests/turbine_unit_test.py index 73e87c8533..d11abad028 100644 --- a/tests/turbine_unit_test.py +++ b/tests/turbine_unit_test.py @@ -172,19 +172,15 @@ def test_ct(): # yaw angle / fCt are (n_findex, n turbine) wind_speed = 10.0 thrust = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.zeros((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) truth_index = turbine_data["power_thrust_table"]["wind_speed"].index(wind_speed) @@ -196,6 +192,7 @@ def test_ct(): # Multiple turbines with index filter # 4 turbines with 3 x 3 grid arrays thrusts = thrust_coefficient( + turbines=[turbine] * N_TURBINES, velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 12 x 4 x 3 x 3 turbulence_intensities=( 0.06 * np.ones((N_TURBINES, 3, 3)) @@ -203,15 +200,10 @@ def test_ct(): ), air_density=None, yaw_angles=np.zeros((1, N_TURBINES)), - tilt_angles=np.ones((1, N_TURBINES)) * 5.0, power_setpoints=np.ones((1, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, N_TURBINES)), - thrust_coefficient_functions={turbine.turbine_type: turbine.thrust_coefficient_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False] * N_TURBINES]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ix_filter=INDEX_FILTER, ) assert len(thrusts[0]) == len(INDEX_FILTER) @@ -225,21 +217,15 @@ def test_ct(): # Single floating turbine; note that 'tilt_interp' is not set to None thrust = thrust_coefficient( + turbines=[turbine_floating] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), # One findex, one turbine turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - thrust_coefficient_functions={ - turbine.turbine_type: turbine_floating.thrust_coefficient_function - }, - tilt_interps={turbine_floating.turbine_type: turbine_floating.tilt_interp}, - correct_cp_ct_for_tilt=np.array([[True]]), turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) truth_index = turbine_floating_data["power_thrust_table"]["wind_speed"].index(wind_speed) @@ -260,18 +246,15 @@ def test_power(): turbine_type_map = np.array(n_turbines * [turbine.turbine_type]) turbine_type_map = turbine_type_map[None, :] test_power = power( + turbines=[turbine] * n_turbines, velocities=wind_speed * np.ones((1, 1, 3, 3)), # 1 findex, 1 turbine, 3x3 grid turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=turbine.power_thrust_table["ref_air_density"], - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, 1)), # 1 findex, 1 turbine power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - tilt_angles=turbine.power_thrust_table["ref_tilt"] * np.ones((1, 1)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) # Recompute using the provided power @@ -283,18 +266,15 @@ def test_power(): # At rated, the power calculated should be 5MW since the test data is the NREL 5MW turbine wind_speed = 18.0 rated_power = power( + turbines=[turbine] * n_turbines, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=turbine.power_thrust_table["ref_air_density"], - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, 1)), # 1 findex, 1 turbine - tilt_angles=turbine.power_thrust_table["ref_tilt"] * np.ones((1, 1)), power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) assert np.allclose(rated_power, 5e6) @@ -302,18 +282,15 @@ def test_power(): # At wind speed = 0.0, the power should be 0 based on the provided Cp curve wind_speed = 0.0 zero_power = power( + turbines=[turbine] * n_turbines, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=turbine.power_thrust_table["ref_air_density"], - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, 1)), # 1 findex, 1 turbine - tilt_angles=turbine.power_thrust_table["ref_tilt"] * np.ones((1, 1)), power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map[:,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) assert np.allclose(zero_power, 0.0) @@ -326,18 +303,15 @@ def test_power(): turbine_type_map = np.array(n_turbines * [turbine.turbine_type]) turbine_type_map = turbine_type_map[None, :] test_4_power = power( + turbines=[turbine] * n_turbines, velocities=wind_speed * np.ones((1, n_turbines, 3, 3)), turbulence_intensities=0.06 * np.ones((1, n_turbines, 3, 3)), air_density=turbine.power_thrust_table["ref_air_density"], - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, n_turbines)), - tilt_angles=turbine.power_thrust_table["ref_tilt"] * np.ones((1, n_turbines)), power_setpoints=np.ones((1, n_turbines)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*n_turbines]*1), awc_amplitudes=np.zeros((1, n_turbines)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) baseline_4_power = baseline_power * np.ones((1, n_turbines)) assert np.allclose(baseline_4_power, test_4_power) @@ -350,18 +324,15 @@ def test_power(): turbine_type_map = np.array(n_turbines * [turbine.turbine_type]) turbine_type_map = turbine_type_map[None, :] test_grid_power = power( + turbines=[turbine] * n_turbines, velocities=wind_speed * np.ones((1, n_turbines, 1)), turbulence_intensities=0.06 * np.ones((1, n_turbines, 3)), air_density=turbine.power_thrust_table["ref_air_density"], - power_functions={turbine.turbine_type: turbine.power_function}, yaw_angles=np.zeros((1, n_turbines)), - tilt_angles=turbine.power_thrust_table["ref_tilt"] * np.ones((1, n_turbines)), power_setpoints=np.ones((1, n_turbines)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*n_turbines]*1), awc_amplitudes=np.zeros((1, n_turbines)), - tilt_interps={turbine.turbine_type: turbine.tilt_interp}, turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) baseline_grid_power = baseline_power * np.ones((1, n_turbines)) assert np.allclose(baseline_grid_power, test_grid_power) @@ -384,24 +355,21 @@ def test_axial_induction(): # Single turbine wind_speed = 10.0 ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), # 1 findex, 1 Turbine turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False]]), turbine_type_map=turbine_type_map[0,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) np.testing.assert_allclose(ai, baseline_ai) # Multiple turbines with ix filter ai = axial_induction( + turbines=[turbine] * N_TURBINES, velocities=np.ones((N_TURBINES, 3, 3)) * WIND_CONDITION_BROADCAST, # 12 x 4 x 3 x 3 turbulence_intensities=( 0.06 * np.ones((N_TURBINES, 3, 3)) @@ -409,15 +377,10 @@ def test_axial_induction(): ), air_density=None, yaw_angles=np.zeros((1, N_TURBINES)), - tilt_angles=np.ones((1, N_TURBINES)) * 5.0, power_setpoints=np.ones((1, N_TURBINES)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array([["baseline"]*N_TURBINES]*1), awc_amplitudes=np.zeros((1, N_TURBINES)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine.turbine_type: None}, - correct_cp_ct_for_tilt=np.array([[False] * N_TURBINES]), turbine_type_map=turbine_type_map, - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ix_filter=INDEX_FILTER, ) @@ -428,19 +391,15 @@ def test_axial_induction(): # Single floating turbine; note that 'tilt_interp' is not set to None ai = axial_induction( + turbines=[turbine_floating] * N_TURBINES, velocities=wind_speed * np.ones((1, 1, 3, 3)), turbulence_intensities=0.06 * np.ones((1, 1, 3, 3)), air_density=None, yaw_angles=np.zeros((1, 1)), - tilt_angles=np.ones((1, 1)) * 5.0, power_setpoints=np.ones((1, 1)) * POWER_SETPOINT_DEFAULT, awc_modes=np.array("baseline"), awc_amplitudes=np.zeros((1, 1)), - axial_induction_functions={turbine.turbine_type: turbine.axial_induction_function}, - tilt_interps={turbine_floating.turbine_type: turbine_floating.tilt_interp}, - correct_cp_ct_for_tilt=np.array([[True]]), turbine_type_map=turbine_type_map[0,0], - turbine_power_thrust_tables={turbine.turbine_type: turbine.power_thrust_table}, ) np.testing.assert_allclose(ai, baseline_ai) diff --git a/tests/turboparkgauss_unit_test.py b/tests/turboparkgauss_unit_test.py index 9561ad007e..1ca012e3ea 100644 --- a/tests/turboparkgauss_unit_test.py +++ b/tests/turboparkgauss_unit_test.py @@ -15,14 +15,9 @@ def test_row_of_turbines(): # Configure as turboparkgauss fmodel_dict = fmodel.core.as_dict() - fmodel_dict["wake"]["model_strings"]["velocity_model"] = "turboparkgauss" - fmodel_dict["wake"]["model_strings"]["turbulence_model"] = "none" - fmodel_dict["wake"]["model_strings"]["deflection_model"] = "none" - fmodel_dict["wake"]["model_strings"]["combination_model"] = "sosfs" - fmodel_dict["wake"]["enable_secondary_steering"] = False - fmodel_dict["wake"]["enable_yaw_added_recovery"] = False - fmodel_dict["wake"]["enable_active_wake_mixing"] = False - fmodel_dict["wake"]["enable_transverse_velocities"] = False + fmodel_dict["wake"]["model"] = "turboparkgauss" + fmodel_dict["wake"]["parameters"] = {"A": 0.04, "include_mirror_wake": True} + fmodel_dict["wake"]["combination_model"] = "sosfs" fmodel_dict["solver"]["type"] = "turbine_cubature_grid" fmodel_dict["solver"]["turbine_grid_points"] = 6 fmodel = FlorisModel(configuration=fmodel_dict) diff --git a/tests/uncertain_floris_model_integration_test.py b/tests/uncertain_floris_model_integration_test.py index 34015ded0a..a0d73be48f 100644 --- a/tests/uncertain_floris_model_integration_test.py +++ b/tests/uncertain_floris_model_integration_test.py @@ -420,24 +420,22 @@ def test_expected_farm_value_regression(): assert np.allclose(expected_farm_value, 75108001.05154414, atol=1e-1) -def test_get_and_set_param(): +def test_get_and_set_wake_parameter(): ufmodel = UncertainFlorisModel(configuration=YAML_INPUT) # Set the wake parameter - ufmodel.set_param(["wake", "wake_velocity_parameters", "gauss", "alpha"], 0.1) - alpha = ufmodel.get_param(["wake", "wake_velocity_parameters", "gauss", "alpha"]) + ufmodel.set_wake_parameter("alpha", 0.1) + alpha = ufmodel.get_wake_parameter("alpha") assert alpha == 0.1 # Confirm also correct in expanded floris model - alpha_e = ufmodel.fmodel_expanded.get_param( - ["wake", "wake_velocity_parameters", "gauss", "alpha"] - ) + alpha_e = ufmodel.fmodel_expanded.get_wake_parameter("alpha") assert alpha_e == 0.1 def test_get_operation_model(): ufmodel = UncertainFlorisModel(configuration=YAML_INPUT) - assert ufmodel.get_operation_model() == "cosine-loss" + assert ufmodel.get_operation_model()[0].__class__.__name__ == "CosineLossTurbine" def test_set_operation_model(): @@ -446,42 +444,74 @@ def test_set_operation_model(): ufmodel = UncertainFlorisModel(configuration=YAML_INPUT) ufmodel.set_operation_model("simple-derating") - assert ufmodel.get_operation_model() == "simple-derating" + assert ufmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" reference_wind_height = ufmodel.reference_wind_height # Check multiple turbine types works ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert ufmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + operation_models = ufmodel.get_operation_model() + assert isinstance(operation_models, list) + assert ( + [om.__class__.__name__ for om in operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) # Confirm this passed through to expanded model - assert ufmodel.fmodel_expanded.get_operation_model() == ["simple-derating", "cosine-loss"] + expanded_operation_models = ufmodel.fmodel_expanded.get_operation_model() + assert isinstance(expanded_operation_models, list) + assert ( + [om.__class__.__name__ for om in expanded_operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) # Check that setting a single turbine type, and then altering the operation model works ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") - assert ufmodel.get_operation_model() == "simple-derating" + assert ufmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" # Check that setting over mutliple turbine types works ufmodel.set(turbine_type=["nrel_5MW", "iea_15MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") - assert ufmodel.get_operation_model() == "simple-derating" + assert ufmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert ufmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + operation_models = ufmodel.get_operation_model() + assert isinstance(operation_models, list) + assert ( + [om.__class__.__name__ for om in operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) + expanded_operation_models = ufmodel.fmodel_expanded.get_operation_model() + assert isinstance(expanded_operation_models, list) + assert ( + [om.__class__.__name__ for om in expanded_operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) # Check setting over single turbine type; then updating layout works ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model("simple-derating") ufmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) - assert ufmodel.get_operation_model() == "simple-derating" + assert ufmodel.get_operation_model()[0].__class__.__name__ == "SimpleDeratingTurbine" # Check that setting for multiple turbine types and then updating layout breaks ufmodel.set(layout_x=[0, 0], layout_y=[0, 1000]) ufmodel.set(turbine_type=["nrel_5MW"], reference_wind_height=reference_wind_height) ufmodel.set_operation_model(["simple-derating", "cosine-loss"]) - assert ufmodel.get_operation_model() == ["simple-derating", "cosine-loss"] + operation_models = ufmodel.get_operation_model() + assert isinstance(operation_models, list) + assert ( + [om.__class__.__name__ for om in operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) + expanded_operation_models = ufmodel.fmodel_expanded.get_operation_model() + assert isinstance(expanded_operation_models, list) + assert ( + [om.__class__.__name__ for om in expanded_operation_models] + == ["SimpleDeratingTurbine", "CosineLossTurbine"] + ) with pytest.raises(ValueError): ufmodel.set(layout_x=[0, 0, 0], layout_y=[0, 1000, 2000]) diff --git a/tests/v3_to_v4_convert_test/gch.yaml b/tests/v3_to_v4_convert_test/gch.yaml deleted file mode 100644 index b383e9c5b7..0000000000 --- a/tests/v3_to_v4_convert_test/gch.yaml +++ /dev/null @@ -1,237 +0,0 @@ - -### -# A name for this input file. -# This is not currently only for the user's reference. -name: GCH - -### -# A description of the contents of this input file. -# This is not currently only for the user's reference. -description: Three turbines using Gauss Curl Hybrid model - -### -# The earliest verion of FLORIS this input file supports. -# This is not currently only for the user's reference. -floris_version: v3.0.0 - -### -# Configure the logging level and where to show the logs. -logging: - - ### - # Settings for logging to the console (i.e. terminal). - console: - - ### - # Can be "true" or "false". - enable: true - - ### - # Set the severity to show output. Messages at this level or higher will be shown. - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". - level: WARNING - - ### - # Settings for logging to a file. - file: - - ### - # Can be "true" or "false". - enable: false - - ### - # Set the severity to show output. Messages at this level or higher will be shown. - # Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". - level: WARNING - -### -# Configure the solver for the type of simulation. -solver: - - ### - # Select the solver type. - # Can be one of: "turbine_grid", "flow_field_grid", "flow_field_planar_grid". - type: turbine_grid - - ### - # Options for the turbine type selected above. See the solver documentation for available parameters. - turbine_grid_points: 3 - -### -# Configure the turbine types and their placement within the wind farm. -farm: - - ### - # Coordinates for the turbine locations in the x-direction which is typically considered - # to be the streamwise direction (left, right) when the wind is out of the west. - # The order of the coordinates here corresponds to the index of the turbine in the primary - # data structures. - layout_x: - - 0.0 - - - ### - # Coordinates for the turbine locations in the y-direction which is typically considered - # to be the spanwise direction (up, down) when the wind is out of the west. - # The order of the coordinates here corresponds to the index of the turbine in the primary - # data structures. - layout_y: - - 0.0 - - - ### - # Listing of turbine types for placement at the x and y coordinates given above. - # The list length must be 1 or the same as ``layout_x`` and ``layout_y``. If it is a - # single value, all turbines are of the same type. Otherwise, the turbine type - # is mapped to the location at the same index in ``layout_x`` and ``layout_y``. - # The types can be either a name included in the turbine_library or - # a full definition of a wind turbine directly. - turbine_type: - - !include nrel_5MW_v3.yaml - -### -# Configure the atmospheric conditions. -flow_field: - - ### - # Air density. - air_density: 1.225 - - ### - # The height to consider the "center" of the vertical wind speed profile - # due to shear. With a shear exponent not 1, the wind speed at this height - # will be the value given in ``wind_speeds``. Above and below this height, - # the wind speed will change according to the shear profile; see - # :py:meth:`.FlowField.initialize_velocity_field`. - # For farms consisting of one wind turbine type, use ``reference_wind_height: -1`` - # to use the hub height of the wind turbine definition. For multiple wind turbine - # types, the reference wind height must be given explicitly. - reference_wind_height: -1 - - ### - # The level of turbulence intensity level in the wind. - turbulence_intensity: 0.06 - - ### - # The wind directions to include in the simulation. - # 0 is north and 270 is west. - wind_directions: - - 270.0 - - ### - # The exponent used to model the wind shear profile; see - # :py:meth:`.FlowField.initialize_velocity_field`. - wind_shear: 0.12 - - ### - # The wind speeds to include in the simulation. - wind_speeds: - - 8.0 - - ### - # The wind veer as a constant value for all points in the grid. - wind_veer: 0.0 - - ### - # The conditions that are specified for use with the multi-dimensional Cp/Ct capbility. - # These conditions are external to FLORIS and specified by the user. They are used internally - # through a nearest-neighbor selection process to choose the correct Cp/Ct interpolants - # to use. These conditions are only used with the ``multidim_cp_ct`` velocity deficit model. - multidim_conditions: - Tp: 2.5 - Hs: 3.01 - -### -# Configure the wake model. -wake: - - ### - # Select the models to use for the simulation. - # See :py:mod:`~.wake` for a list - # of available models and their descriptions. - model_strings: - - ### - # Select the wake combination model. - combination_model: sosfs - - ### - # Select the wake deflection model. - deflection_model: gauss - - ### - # Select the wake turbulence model. - turbulence_model: crespo_hernandez - - ### - # Select the wake velocity deficit model. - velocity_model: gauss - - ### - # Can be "true" or "false". - enable_secondary_steering: true - - ### - # Can be "true" or "false". - enable_yaw_added_recovery: true - - ### - # Can be "true" or "false". - enable_transverse_velocities: true - - ### - # Configure the parameters for the wake deflection model - # selected above. - # Additional blocks can be provided for - # models that are not enabled, but the enabled model - # must have a corresponding parameter block. - wake_deflection_parameters: - gauss: - ad: 0.0 - alpha: 0.58 - bd: 0.0 - beta: 0.077 - dm: 1.0 - ka: 0.38 - kb: 0.004 - jimenez: - ad: 0.0 - bd: 0.0 - kd: 0.05 - - ### - # Configure the parameters for the wake velocity deficit model - # selected above. - # Additional blocks can be provided for - # models that are not enabled, but the enabled model - # must have a corresponding parameter block. - wake_velocity_parameters: - cc: - a_s: 0.179367259 - b_s: 0.0118889215 - c_s1: 0.0563691592 - c_s2: 0.13290157 - a_f: 3.11 - b_f: -0.68 - c_f: 2.41 - alpha_mod: 1.0 - gauss: - alpha: 0.58 - beta: 0.077 - ka: 0.38 - kb: 0.004 - jensen: - we: 0.05 - - ### - # Configure the parameters for the wake turbulence model - # selected above. - # Additional blocks can be provided for - # models that are not enabled, but the enabled model - # must have a corresponding parameter block. - wake_turbulence_parameters: - crespo_hernandez: - initial: 0.1 - constant: 0.5 - ai: 0.8 - downstream: -0.32 diff --git a/tests/v3_to_v4_convert_test/nrel_5MW_v3.yaml b/tests/v3_to_v4_convert_test/nrel_5MW_v3.yaml deleted file mode 100644 index 653ef14c78..0000000000 --- a/tests/v3_to_v4_convert_test/nrel_5MW_v3.yaml +++ /dev/null @@ -1,212 +0,0 @@ - -### -# An ID for this type of turbine definition. -# This is not currently used, but it will be enabled in the future. This should typically -# match the root name of the file. -turbine_type: 'nrel_5MW' - -### -# Setting for generator losses to power. -generator_efficiency: 1.0 - -### -# Hub height. -hub_height: 90.0 - -### -# Cosine exponent for power loss due to yaw misalignment. -pP: 1.88 - -### -# Cosine exponent for power loss due to tilt. -pT: 1.88 - -### -# Rotor diameter. -rotor_diameter: 126.0 - -### -# Tip speed ratio defined as linear blade tip speed normalized by the incoming wind speed. -TSR: 8.0 - -### -# The air density at which the Cp and Ct curves are defined. -ref_density_cp_ct: 1.225 - -### -# The tilt angle at which the Cp and Ct curves are defined. This is used to capture -# the effects of a floating platform on a turbine's power and wake. -ref_tilt_cp_ct: 5.0 - -### -# Cp and Ct as a function of wind speed for the turbine's full range of operating conditions. -power_thrust_table: - power: - - 0.0 - - 0.000000 - - 0.000000 - - 0.178085 - - 0.289075 - - 0.349022 - - 0.384728 - - 0.406059 - - 0.420228 - - 0.428823 - - 0.433873 - - 0.436223 - - 0.436845 - - 0.436575 - - 0.436511 - - 0.436561 - - 0.436517 - - 0.435903 - - 0.434673 - - 0.433230 - - 0.430466 - - 0.378869 - - 0.335199 - - 0.297991 - - 0.266092 - - 0.238588 - - 0.214748 - - 0.193981 - - 0.175808 - - 0.159835 - - 0.145741 - - 0.133256 - - 0.122157 - - 0.112257 - - 0.103399 - - 0.095449 - - 0.088294 - - 0.081836 - - 0.075993 - - 0.070692 - - 0.065875 - - 0.061484 - - 0.057476 - - 0.053809 - - 0.050447 - - 0.047358 - - 0.044518 - - 0.041900 - - 0.039483 - - 0.0 - - 0.0 - thrust: - - 0.0 - - 0.0 - - 0.0 - - 0.99 - - 0.99 - - 0.97373036 - - 0.92826162 - - 0.89210543 - - 0.86100905 - - 0.835423 - - 0.81237673 - - 0.79225789 - - 0.77584769 - - 0.7629228 - - 0.76156073 - - 0.76261984 - - 0.76169723 - - 0.75232027 - - 0.74026851 - - 0.72987175 - - 0.70701647 - - 0.54054532 - - 0.45509459 - - 0.39343381 - - 0.34250785 - - 0.30487242 - - 0.27164979 - - 0.24361964 - - 0.21973831 - - 0.19918151 - - 0.18131868 - - 0.16537679 - - 0.15103727 - - 0.13998636 - - 0.1289037 - - 0.11970413 - - 0.11087113 - - 0.10339901 - - 0.09617888 - - 0.09009926 - - 0.08395078 - - 0.0791188 - - 0.07448356 - - 0.07050731 - - 0.06684119 - - 0.06345518 - - 0.06032267 - - 0.05741999 - - 0.05472609 - - 0.0 - - 0.0 - wind_speed: - - 0.0 - - 2.0 - - 2.5 - - 3.0 - - 3.5 - - 4.0 - - 4.5 - - 5.0 - - 5.5 - - 6.0 - - 6.5 - - 7.0 - - 7.5 - - 8.0 - - 8.5 - - 9.0 - - 9.5 - - 10.0 - - 10.5 - - 11.0 - - 11.5 - - 12.0 - - 12.5 - - 13.0 - - 13.5 - - 14.0 - - 14.5 - - 15.0 - - 15.5 - - 16.0 - - 16.5 - - 17.0 - - 17.5 - - 18.0 - - 18.5 - - 19.0 - - 19.5 - - 20.0 - - 20.5 - - 21.0 - - 21.5 - - 22.0 - - 22.5 - - 23.0 - - 23.5 - - 24.0 - - 24.5 - - 25.0 - - 25.01 - - 25.02 - - 50.0 - -### -# A boolean flag used when the user wants FLORIS to use the user-supplied multi-dimensional -# Cp/Ct information. -multi_dimensional_cp_ct: False - -### -# The path to the .csv file that contains the multi-dimensional Cp/Ct data. The format of this -# file is such that any external conditions, such as wave height or wave period, that the -# Cp/Ct data is dependent on come first, in column format. The last three columns of the .csv -# file must be ``ws``, ``Cp``, and ``Ct``, in that order. An example of fictional data is given -# in ``floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv``. -power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv' diff --git a/tests/v4_to_v5_converter_test/gch.yaml b/tests/v4_to_v5_converter_test/gch.yaml new file mode 100644 index 0000000000..1545c3fa76 --- /dev/null +++ b/tests/v4_to_v5_converter_test/gch.yaml @@ -0,0 +1,243 @@ + +### +# Name for the input file. +# This is not used by FLORIS and is simply for the user's reference. +# String type. +name: GCH + +### +# Description of the contents of this input file. +# This is not used by FLORIS and is simply for the user's reference. +# String type. +description: Three turbines using Gauss Curl Hybrid model + +### +# The FLORIS version that the file is defined for. +# This is not used by FLORIS and is simply for the user's reference. +# String type. +floris_version: v4 + +### +# Upper-level group of options for configuring logging. +logging: + + ### + # Group of settings for logging to the console (i.e. terminal). + console: + + ### + # Flag to enable console logging. Boolean type. + enable: true + + ### + # Severity to show output in console. Messages at this level or higher will be shown. + # String type. Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". + level: WARNING + + ### + # Group of settings for logging to a file. + file: + + ### + # Flag to enable file logging. Boolean type. + enable: false + + ### + # Severity to show output in file. Messages at this level or higher will be shown. + # String type. Can be one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG". + level: WARNING + +### +# Upper-level group of options for configuring solution grid. +solver: + + ### + # Grid type for solving flow values at the turbines. + # String type. Can be one of: "turbine_grid", "turbine_cubature_grid". + type: turbine_grid + + ### + # Number of grid points per turbine for solve. For turbine_grid type solve, represents the number + # of points along each of the two axes. For turbine_cubature_grid type solve, represents the + # total number of points used in the cubature solve. Integer type. + turbine_grid_points: 3 + +### +# Group for setting the wind farm configuration. +farm: + + ### + # x-coordinates for the turbine locations, with the x axis corresponding to the + # "west to east" direction. The order of the coordinates corresponds to the index of the + # turbine in the primary data structures. + # List of float type. + layout_x: + - 0.0 + - 630.0 + - 1260.0 + + ### + # y-coordinates for the turbine locations, with the y axis corresponding to the + # "south to north" direction. The order of the coordinates corresponds to the index of the + # turbine in the primary data structures. + # List of float type. + layout_y: + - 0.0 + - 0.0 + - 0.0 + + ### + # Listing of turbine types for placement at the x and y coordinates. + # The list length must be 1 or the same as ``layout_x`` and ``layout_y``. If it is a + # single value, all turbines are of the same type. Otherwise, the turbine type + # is mapped to the location at the same index in ``layout_x`` and ``layout_y``. + # The types can be either a string for a turbine included in the turbine_library, + # a string beginning with !include for a path to the user's turbine; or a full + # definition of a wind turbine (as a nested dictionary). + turbine_type: + - !include nrel_5MW_local.yaml + +### +# Group for defining the atmospheric conditions. +flow_field: + + ### + # Air density. Float type. + air_density: 1.225 + + ### + # The height in meters to consider the "center" of the vertical wind speed profile + # due to shear. With a shear exponent not 1, the wind speed at this height + # will be the value given in ``wind_speeds``. Above and below this height, + # the wind speed will change according to the shear profile; see + # :py:meth:`.FlowField.initialize_velocity_field`. + # For farms consisting of one wind turbine type, use ``reference_wind_height: -1`` + # to use the hub height of the wind turbine definition. For multiple wind turbine + # types, the reference wind height must be given explicitly. Float type (or -1). + reference_wind_height: -1 + + ### + # Turbulence intensities for the simulation, specified as a decimal value. Type list of floats. + turbulence_intensities: + - 0.06 + + ### + # Wind directions for the simulation, specified in degrees according to compass directions + # (0 is northerly, 90 is easterly, etc). Type list of floats. + wind_directions: + - 270.0 + + ### + # The exponent used to model the wind shear profile; see + # :py:meth:`.FlowField.initialize_velocity_field`. Float type. + wind_shear: 0.12 + + ### + # The wind speeds for the simulation, specified in m/s at the ``reference_wind_height``. + # Type list of floats. + wind_speeds: + - 8.0 + + ### + # The wind veer (in degrees) as a constant value for all points in the grid. Only used in + # certain models. Float type. + wind_veer: 0.0 + + ### + # Conditions that are specified for use with the multi-dimensional power/thrust capability. + # These conditions are external to FLORIS and specified by the user. They are used internally + # through a nearest-neighbor selection process to choose the correct Cp/Ct interpolants + # to use. Type dictionary of string:float pairs. + multidim_conditions: + Tp: 2.5 + Hs: 3.01 + +### +# Group for defining the wake model parameters. +wake: + + ### + # Group for selecting the model elements for the simulation. + # See :py:mod:`~.wake` for a list of available models and their descriptions. + model_strings: + + ### + # Wake combination model. String type.. + combination_model: sosfs + + ### + # Wake deflection model. String type. + deflection_model: gauss + + ### + # Wake turbulence model. String type.. + turbulence_model: crespo_hernandez + + ### + # Wake velocity deficit model. String type. + velocity_model: gauss + + ### + # Flag to include secondary steering effects. Only used in some models. Boolean type. + enable_secondary_steering: true + + ### + # Flag to include yaw added recovery effects. Only used in some models. Boolean type. + enable_yaw_added_recovery: true + + ### + # Flag to include active wake mixing effects. Only used in Empirical Guassian model. Boolean type. + enable_active_wake_mixing: false + + ### + # Flag to compute transverse velocities across turbine rotors. Only used in some models. + # Boolean type. + enable_transverse_velocities: true + + ### + # Parameters for the wake deflection model. See model descriptions and implementations for + # details of each parameter and its use. + wake_deflection_parameters: + gauss: + ad: 0.0 + alpha: 0.58 + bd: 0.0 + beta: 0.077 + dm: 1.0 + ka: 0.38 + kb: 0.004 + jimenez: + ad: 0.0 + bd: 0.0 + kd: 0.05 + + ### + # Parameters for the wake velocity deficit model. See model descriptions and implementations for + # details of each parameter and its use. + wake_velocity_parameters: + cc: + a_s: 0.179367259 + b_s: 0.0118889215 + c_s1: 0.0563691592 + c_s2: 0.13290157 + a_f: 3.11 + b_f: -0.68 + c_f: 2.41 + alpha_mod: 1.0 + gauss: + alpha: 0.58 + beta: 0.077 + ka: 0.38 + kb: 0.004 + jensen: + we: 0.05 + + ### + # Parameters for the wake turbulence model. See model descriptions and implementations for + # details of each parameter and its use. + wake_turbulence_parameters: + crespo_hernandez: + initial: 0.1 + constant: 0.5 + ai: 0.8 + downstream: -0.32 diff --git a/tests/v4_to_v5_converter_test/nrel_5MW_local.yaml b/tests/v4_to_v5_converter_test/nrel_5MW_local.yaml new file mode 100644 index 0000000000..ff760ee79b --- /dev/null +++ b/tests/v4_to_v5_converter_test/nrel_5MW_local.yaml @@ -0,0 +1,299 @@ +# NREL 5MW reference wind turbine. +# Data based on: +# https://github.com/NREL/turbine-models/blob/master/Offshore/NREL_5MW_126_RWT_corrected.csv +# Note: Small power variations above rated removed. Rotor diameter includes coning angle. +# Note: generator efficiency of 94.4% is assumed for the NREL 5MW turbine. + +### +# An ID for this type of turbine definition. +# This is used to uniquely identify different turbines in the simulation, so should be different +# for each different turbine definition being used in the same simulation. +# String type. +turbine_type: 'nrel_5MW' + +### +# Turbine hub height in meters. Float type. +hub_height: 90.0 + +### +# Turbine rotor diameter in meters. Float type. +rotor_diameter: 125.88 + +### +# Nominal wind turbine tip-speed ratio for below-rated operation. Only used in some wake models. +# Float type. +TSR: 8.0 + +### +# Model for power and thrust curve interpretation. See floris.core.turbine.operation_models for +# details. String type. +operation_model: 'cosine-loss' + +### +# Group of parameters needed to evaluate the power and thrust produced by the turbine. +power_thrust_table: + ### + # Air density at which the power and thrust_coefficient curves are defined (kg / m^3). Float type. + ref_air_density: 1.225 + ### + # Tilt angle at which the power and thrust_coefficient curves are defined (degrees). + # Used to capture the effects of a floating platform on a turbine's power and wake. + # Float type. + ref_tilt: 5.0 + ### + # Cosine exponent for power loss due to tilt. Float type. + cosine_loss_exponent_tilt: 1.88 + ### + # Cosine exponent for power loss due to yaw misalignment. Float type. + cosine_loss_exponent_yaw: 1.88 + ### + # Helix parameter a. See documentation for details. Float type. + helix_a: 1.802 + ### + # Helix parameter b for power calculation. See documentation for details. Float type. + helix_power_b: 4.568e-03 + ### + # Helix parameter c for power calculation. See documentation for details. Float type. + helix_power_c: 1.629e-10 + ### + # Helix parameter b for thrust calculation. See documentation for details. Float type. + helix_thrust_b: 1.027e-03 + ### + # Helix parameter c for thrust calculation. See documentation for details. Float type. + helix_thrust_c: 1.378e-06 + ### + # Fraction of peak thrust by which to reduce (specified as a decimal). Float type. + peak_shaving_fraction: 0.2 + ### + # Threshold turbulence intensity above which to apply peak shaving (specified as a decimal). + # Float tpe. + peak_shaving_TI_threshold: 0.1 + + ### + # Parameters for the 'controller-dependenter-dependent' operation model. See + # floris.core.turbine.controller_dependent_operation_model and documentation for details. + controller_dependent_turbine_parameters: + rated_rpm: 12.1 + rotor_solidity: 0.05132 + generator_efficiency: 0.944 + rated_power: 5000.0 + rotor_diameter: 126 + beta: -0.45891 + cd: 0.0040638 + cl_alfa: 4.275049 + cp_ct_data_file: "demo_cp_ct_surfaces/nrel_5MW_demo_cp_ct_surface.npz" + + ### + # Wind speeds for look-up tables of power and thrust_coefficient. List of float type. + wind_speed: + - 0.0 + - 2.9 + - 3.0 + - 4.0 + - 5.0 + - 6.0 + - 7.0 + - 7.1 + - 7.2 + - 7.3 + - 7.4 + - 7.5 + - 7.6 + - 7.7 + - 7.8 + - 7.9 + - 8.0 + - 9.0 + - 10.0 + - 10.1 + - 10.2 + - 10.3 + - 10.4 + - 10.5 + - 10.6 + - 10.7 + - 10.8 + - 10.9 + - 11.0 + - 11.1 + - 11.2 + - 11.3 + - 11.4 + - 11.5 + - 11.6 + - 11.7 + - 11.8 + - 11.9 + - 12.0 + - 13.0 + - 14.0 + - 15.0 + - 16.0 + - 17.0 + - 18.0 + - 19.0 + - 20.0 + - 21.0 + - 22.0 + - 23.0 + - 24.0 + - 25.0 + - 25.1 + - 50.0 + ### + # Power values (specified in kW) for lookup by wind speed. List of float type. + power: + - 0.0 + - 0.0 + - 40.518011517569214 + - 177.67162506419703 + - 403.900880943964 + - 737.5889584824021 + - 1187.1774030611875 + - 1239.245945375778 + - 1292.5184293723503 + - 1347.3213147477102 + - 1403.2573725578948 + - 1460.7011898730707 + - 1519.6419125979983 + - 1580.174365096404 + - 1642.1103166918167 + - 1705.758292831 + - 1771.1659528893977 + - 2518.553107505315 + - 3448.381605840943 + - 3552.140809000129 + - 3657.9545431794127 + - 3765.121299313842 + - 3873.928844315059 + - 3984.4800226955504 + - 4096.582833096852 + - 4210.721306623712 + - 4326.154305853405 + - 4443.395565353604 + - 4562.497934188341 + - 4683.419890251577 + - 4806.164748311019 + - 4929.931918769215 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 5000.00 + - 0.0 + - 0.0 + ### + # Thrust coefficient values (unitless) for lookup by wind speed. List of float type. + thrust_coefficient: + - 0.0 + - 0.0 + - 1.132034888 + - 0.999470963 + - 0.917697381 + - 0.860849503 + - 0.815371198 + - 0.811614904 + - 0.807939328 + - 0.80443352 + - 0.800993851 + - 0.79768116 + - 0.794529244 + - 0.791495834 + - 0.788560434 + - 0.787217182 + - 0.787127977 + - 0.785839257 + - 0.783812219 + - 0.783568108 + - 0.783328285 + - 0.781194418 + - 0.777292539 + - 0.773464375 + - 0.769690236 + - 0.766001924 + - 0.762348072 + - 0.758760824 + - 0.755242872 + - 0.751792927 + - 0.748434131 + - 0.745113997 + - 0.717806682 + - 0.672204789 + - 0.63831272 + - 0.610176496 + - 0.585456847 + - 0.563222111 + - 0.542912273 + - 0.399312061 + - 0.310517829 + - 0.248633226 + - 0.203543725 + - 0.169616419 + - 0.143478955 + - 0.122938861 + - 0.106515296 + - 0.093026095 + - 0.081648606 + - 0.072197368 + - 0.064388275 + - 0.057782745 + - 0.0 + - 0.0 + +### +# Boolean flag used when the user wants FLORIS to use the user-supplied multi-dimensional +# power/thrust coefficient information information. Boolean type. +multi_dimensional_cp_ct: False + +### +# Path to the .csv file that contains the multi-dimensional power/thrust coefficient data. +# The format of this file is such that any external conditions, such as wave height or wave period, +# that the power/thrust data is dependent on come first, in column format. The last three columns +# of the .csv file must be ``ws``, ``power``, and ``thrust_coefficient``, in that order. An example +# of fictional data is given in ``floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv``. +# String type. +power_thrust_data_file: '../floris/turbine_library/iea_15MW_multi_dim_Tp_Hs.csv' + +### +# Group of parameters needed to evaluate the tilt angle of a floating turbine across wind speeds. +floating_tilt_table: + ### + # Wind speeds at which steady tilt angles are defined (m/s). List of float type. + wind_speed: + - 4.0 + - 6.0 + - 8.0 + - 10.0 + - 12.0 + - 14.0 + - 16.0 + ### + # Tilt angle for each wind speed (degrees, positive "tilted back"). List of float type. + tilt: + - 5.0 + - 5.0 + - 5.0 + - 5.0 + - 5.0 + - 5.0 + - 5.0 + +### +# Flag for whether to apply the floating tilt table to correct turbine power and thrust curves. +# Boolean type. +correct_cp_ct_for_tilt: false diff --git a/tests/wake_unit_test.py b/tests/wake_unit_test.py new file mode 100644 index 0000000000..3def48f6c7 --- /dev/null +++ b/tests/wake_unit_test.py @@ -0,0 +1,81 @@ + +import numpy as np + +from floris.core import ( + Core, + power, + WakeModelManager, +) +from tests.conftest import SampleInputs + + +def test_asdict(sample_inputs_fixture: SampleInputs): + + wake_model_manager = WakeModelManager.from_dict(sample_inputs_fixture.wake) + dict1 = wake_model_manager.as_dict() + + new_wake = WakeModelManager.from_dict(dict1) + dict2 = new_wake.as_dict() + + assert dict1 == dict2 + +def test_combination_model(sample_inputs_fixture): + """ + Tandem turbines + """ + sample_inputs_fixture.switch_wake_model("jensen") + + floris = Core.from_dict(sample_inputs_fixture.core) + floris.initialize_domain() + floris.solve_for_turbines() + + velocities = floris.flow_field.u + turbulence_intensities = floris.flow_field.turbulence_intensity_field + air_density = floris.flow_field.air_density + yaw_angles = floris.farm.yaw_angles + power_setpoints = floris.farm.power_setpoints + awc_modes = floris.farm.awc_modes + awc_amplitudes = floris.farm.awc_amplitudes + + farm_powers_sosfs = power( + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, + ) + + # Switch to a different combination model and rerun + sample_inputs_fixture.core["wake"]["combination_model"] = "fls" + + floris = Core.from_dict(sample_inputs_fixture.core) + floris.initialize_domain() + floris.solve_for_turbines() + + velocities = floris.flow_field.u + turbulence_intensities = floris.flow_field.turbulence_intensity_field + air_density = floris.flow_field.air_density + yaw_angles = floris.farm.yaw_angles + power_setpoints = floris.farm.power_setpoints + awc_modes = floris.farm.awc_modes + awc_amplitudes = floris.farm.awc_amplitudes + + farm_powers_fls = power( + turbines=floris.farm.turbines, + velocities=velocities, + turbulence_intensities=turbulence_intensities, + air_density=air_density, + yaw_angles=yaw_angles, + power_setpoints=power_setpoints, + awc_modes=awc_modes, + awc_amplitudes=awc_amplitudes, + turbine_type_map=floris.farm.turbine_type_map, + ) + + # First-row turbines should be the same. Downstream turbines should differ some + assert np.allclose(farm_powers_sosfs[0, 0], farm_powers_fls[0,0]) + assert not np.allclose(farm_powers_sosfs, farm_powers_fls) diff --git a/tests/wake_unit_tests.py b/tests/wake_unit_tests.py deleted file mode 100644 index 90f66057ea..0000000000 --- a/tests/wake_unit_tests.py +++ /dev/null @@ -1,14 +0,0 @@ - -from floris.core import WakeModelManager -from tests.conftest import SampleInputs - - -def test_asdict(sample_inputs_fixture: SampleInputs): - - wake_model_manager = WakeModelManager.from_dict(sample_inputs_fixture.wake) - dict1 = wake_model_manager.as_dict() - - new_wake = WakeModelManager.from_dict(dict1) - dict2 = new_wake.as_dict() - - assert dict1 == dict2 diff --git a/tests/yaw_optimization_integration_test.py b/tests/yaw_optimization_integration_test.py index a0c3011fc8..a2da00aa6d 100644 --- a/tests/yaw_optimization_integration_test.py +++ b/tests/yaw_optimization_integration_test.py @@ -1,5 +1,4 @@ import numpy as np -import pandas as pd import pytest from floris import FlorisModel @@ -7,8 +6,7 @@ DEBUG = False -VELOCITY_MODEL = "gauss" -DEFLECTION_MODEL = "gauss" +WAKE_MODEL = "gauss" def test_yaw_optimization_limits(sample_inputs_fixture): """ @@ -16,8 +14,7 @@ def test_yaw_optimization_limits(sample_inputs_fixture): optimization scheme. This test compares the optimization results from the SR method for a simple farm with a simple wind rose to stored baseline results. """ - sample_inputs_fixture.core["wake"]["model_strings"]["velocity_model"] = VELOCITY_MODEL - sample_inputs_fixture.core["wake"]["model_strings"]["deflection_model"] = DEFLECTION_MODEL + sample_inputs_fixture.switch_wake_model(WAKE_MODEL) fmodel = FlorisModel(sample_inputs_fixture.core) wd_array = np.arange(0.0, 360.0, 90.0)