diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8ca0b1cb59..fe36342396 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,5 +1,7 @@ # This workflows will upload a Python Package using Twine when a release is created +# Published via GitHub Actions as a PyPI Trusted Publisher. # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +# and: https://docs.pypi.org/trusted-publishers/ name: Upload Python Package @@ -9,22 +11,22 @@ on: jobs: deploy: + environment: release-pypi if: github.repository_owner == 'NatLabRockies' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python -m build - twine upload dist/* + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + - name: Install dependencies and build package + run: | + python -m pip install --upgrade pip + pip install build twine + python -m build + twine check --strict dist/* + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: True diff --git a/README.md b/README.md index d07a19a382..8b38cabdec 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.2](https://github.com/NatLabRockies/floris/releases/latest). +release is [FLORIS v.4.6.3](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.2 + 4.6.3 FILE ~/floris/floris/__init__.py diff --git a/docs/dev_guide.md b/docs/dev_guide.md index 2a7cfa5c0b..890d52e6da 100644 --- a/docs/dev_guide.md +++ b/docs/dev_guide.md @@ -174,7 +174,7 @@ git commit -m "Update so and so" In order to maintain a level of confidence in the software, FLORIS is expected to maintain a reasonable level of test coverage. To that end, unit -tests for a the low-level code in the `floris.simulation` package are included. +tests for a the low-level code in the `floris.core` package are included. The full testing suite can by executed by running the command ``pytest`` from the highest directory in the repository. A testing-only class is included @@ -404,7 +404,7 @@ def function( ``` Some models require a special grid and/or solver, and that mapping happens in -[floris.simulation.Floris](https://github.com/NatLabRockies/floris/blob/main/floris/simulation/floris.py#L145). +[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`. diff --git a/docs/docs_image.png b/docs/docs_image.png index c0c029344c..140a1c626c 100644 Binary files a/docs/docs_image.png and b/docs/docs_image.png differ diff --git a/docs/installation.md b/docs/installation.md index b6c6567fa0..7ba09ba5c9 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -91,7 +91,7 @@ PACKAGE CONTENTS wind_data VERSION - 4.6.2 + 4.6.3 FILE ~/floris/floris/__init__.py diff --git a/docs/references.bib b/docs/references.bib index 9bf5f5b8d6..07c6cb7f63 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -146,7 +146,7 @@ @Article{annoni2018analysis @article{crespo1996turbulence, title={Turbulence characteristics in wind-turbine wakes}, - author={Crespo, A and Hernandez, J}, + author={Crespo, A. and Hernández, J.}, journal={Journal of wind engineering and industrial aerodynamics}, volume={61}, number={1}, @@ -369,3 +369,36 @@ @techreport{zahle_IEA22MW_2024 author = {Zahle, Frederik and Barlas, Athanasios and Loenbaek, Kenneth and Bortolotti, Pietro and Zalkind, Daniel and Wang, Lu and Labuschagne, Casper and Sethuraman, Latha and Barter, Garrett}, year = {2024}, } + +@article{fleming_sr_2022, + title = {Serial-Refine Method for Fast Wake-Steering Yaw Optimization}, + volume = {2265}, + issn = {1742-6588, 1742-6596}, + url = {https://iopscience.iop.org/article/10.1088/1742-6596/2265/3/032109}, + doi = {10.1088/1742-6596/2265/3/032109}, + number = {3}, + journal = {Journal of Physics: Conference Series (TORQUE)}, + author = {Fleming, Paul A. and Stanley, Andrew P. J. and Bay, Christopher J. and King, Jennifer and Simley, Eric and Doekemeijer, Bart M. and Mudafort, Rafael}, + year = {2022}, + pages = {032109}, +} + +@inproceedings{katic_sos_1986, + address = {Rome, Italy}, + title = {A simple model for cluster efficiency}, + volume = {1}, + author = {Katic, I and Højstrup, J and Jensen, N O}, + year = {1986}, + pages = {407--410}, +} + +@article{zehtabiyan_rezaie_CH_2023, + title = {A short note on turbulence characteristics in wind-turbine wakes}, + volume = {240}, + issn = {0167-6105}, + doi = {10.1016/j.jweia.2023.105504}, + journal = {Journal of Wind Engineering and Industrial Aerodynamics}, + author = {Zehtabiyan-Rezaie, Navid and Abkar, Mahdi}, + year = {2023}, + pages = {105504}, +} diff --git a/docs/wake_models.ipynb b/docs/wake_models.ipynb index 78919191a6..4a04091177 100644 --- a/docs/wake_models.ipynb +++ b/docs/wake_models.ipynb @@ -213,7 +213,9 @@ "\n", "CrespoHernandez is a wake-turbulence model that is used to compute additional variability introduced\n", "to the flow field by operation of a wind turbine. Implementation of the model follows the original\n", - "formulation and limitations outlined in {cite:t}`crespo1996turbulence`." + "formulation and limitations outlined in {cite:t}`crespo1996turbulence`.\n", + "\n", + "The default parameter values used in FLORIS for CrespoHernandez differ from those reported in {cite:t}`crespo1996turbulence` following subsequent calibration. However, {cite:t}`zehtabiyan_rezaie_CH_2023` argue that the sign of certain parameters are not physically consistent (and also misreported in subsequent literature). See the `CrespoHernandez` class docstring for more details." ] }, { @@ -347,8 +349,7 @@ "source": [ "### Sum of Squares Freestream Superposition (SOSFS)\n", "\n", - "This model combines the wakes via a sum of squares of the new wake to add and the existing\n", - "flow field." + "This model combines the wakes via a sum of squares of the new wake to add and the existing flow field. For more information, refer to :cite:`katic_sos_1986`." ] }, { diff --git a/examples/examples_turbine/001_reference_turbines.py b/examples/examples_turbine/001_reference_turbines.py index 75698dda2c..1ca53e1e81 100644 --- a/examples/examples_turbine/001_reference_turbines.py +++ b/examples/examples_turbine/001_reference_turbines.py @@ -1,7 +1,8 @@ -"""Example: Check turbine power curves +"""Example: Reference turbines -For each turbine in the turbine library, make a small figure showing that its power -curve and power loss to yaw are reasonable and reasonably smooth +For each reference wind turbine in the turbine library, make a small figure +showing its power and thrust coefficient curves and demonstrate its power loss +to yaw. """ diff --git a/floris/core/solver.py b/floris/core/solver.py index 47e0962719..a785dbee9b 100644 --- a/floris/core/solver.py +++ b/floris/core/solver.py @@ -529,7 +529,7 @@ def cc_solver( + (flow_field.u_initial_sorted - turb_u_wake) * mask2 ) - turb_avg_vels = average_velocity(turb_inflow_field) + turb_avg_vels = average_velocity(turb_inflow_field)[:, :, None, None] turb_Cts = thrust_coefficient( turb_avg_vels, flow_field.turbulence_intensity_field_sorted, diff --git a/floris/core/wake_combination/sosfs.py b/floris/core/wake_combination/sosfs.py index c277e21bba..305ff68035 100644 --- a/floris/core/wake_combination/sosfs.py +++ b/floris/core/wake_combination/sosfs.py @@ -10,6 +10,8 @@ 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: diff --git a/floris/core/wake_deflection/gauss.py b/floris/core/wake_deflection/gauss.py index 11cdee7fba..a210ef95e7 100644 --- a/floris/core/wake_deflection/gauss.py +++ b/floris/core/wake_deflection/gauss.py @@ -297,29 +297,31 @@ def wake_added_yaw( # top vortex # NOTE: this is the top of the grid, not the top of the rotor zT = z_i - (HH + D / 2) + NUM_EPS # distance from the top of the grid - rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is (-) in the paper + # NOTE: This is (-) in the paper, but (+) is consistent with the + # Martínez-Tossas et al. (2019) source. + rT_squared = ne.evaluate("yLocs ** 2 + zT ** 2") # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") - v_top = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape") + core_shape = ne.evaluate("1 - exp(-rT_squared / (eps ** 2))") + v_top = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT_squared) * core_shape") v_top = np.mean( v_top, axis=(2,3) ) # w_top = (-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay # bottom vortex zB = z_i - (HH - D / 2) + NUM_EPS - rB = ne.evaluate("yLocs ** 2 + zB ** 2") - core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") - v_bottom = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape") + rB_squared = ne.evaluate("yLocs ** 2 + zB ** 2") + core_shape = ne.evaluate("1 - exp(-rB_squared / (eps ** 2))") + v_bottom = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB_squared) * core_shape") v_bottom = np.mean( v_bottom, axis=(2,3) ) # w_bottom = (-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay # wake rotation vortex zC = z_i - HH + NUM_EPS - rC = ne.evaluate("yLocs ** 2 + zC ** 2") - core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") - v_core = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape") + rC_squared = ne.evaluate("yLocs ** 2 + zC ** 2") + core_shape = ne.evaluate("1 - exp(-rC_squared / (eps ** 2))") + v_core = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC_squared) * core_shape") v_core = np.mean( v_core, axis=(2,3) ) - # w_core = (-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay + # w_core = (-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC_squared) * core_shape * decay # Cap the effective yaw values between -45 and 45 degrees val = 2 * (avg_v - v_core) / (v_top + v_bottom) @@ -398,51 +400,59 @@ def calculate_transverse_velocity( # top vortex zT = z - (HH + D / 2) + NUM_EPS - rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is - in the paper + # NOTE: This is (-) in the paper, but (+) is consistent with the + # Martínez-Tossas et al. (2019) source. + rT_squared = ne.evaluate("yLocs ** 2 + zT ** 2") # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") - V1 = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape * decay") - W1 = ne.evaluate("(-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay") + core_shape = ne.evaluate("1 - exp(-rT_squared / (eps ** 2))") + V1 = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT_squared) * core_shape * decay") + W1 = ne.evaluate("(-1 * Gamma_top * yLocs) / (2 * pi * rT_squared) * core_shape * decay") # bottom vortex zB = z - (HH - D / 2) + NUM_EPS - rB = ne.evaluate("yLocs ** 2 + zB ** 2") - core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") - V2 = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape * decay") - W2 = ne.evaluate("(-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay") + rB_squared = ne.evaluate("yLocs ** 2 + zB ** 2") + core_shape = ne.evaluate("1 - exp(-rB_squared / (eps ** 2))") + V2 = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB_squared) * core_shape * decay") + W2 = ne.evaluate("(-1 * Gamma_bottom * yLocs) / (2 * pi * rB_squared) * core_shape * decay") # wake rotation vortex zC = z - HH + NUM_EPS - rC = ne.evaluate("yLocs ** 2 + zC ** 2") - core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") - V5 = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape * decay") - W5 = ne.evaluate("(-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay") + rC_squared = ne.evaluate("yLocs ** 2 + zC ** 2") + core_shape = ne.evaluate("1 - exp(-rC_squared / (eps ** 2))") + V5 = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC_squared) * core_shape * decay") + W5 = ne.evaluate( + "(-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC_squared) * core_shape * decay" + ) ### Boundary condition - ground mirror vortex # top vortex - ground zTb = z + (HH + D / 2) + NUM_EPS - rTb = ne.evaluate("yLocs ** 2 + zTb ** 2") + rTb_squared = ne.evaluate("yLocs ** 2 + zTb ** 2") # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions - core_shape = ne.evaluate("1 - exp(-rTb / (eps ** 2))") - V3 = ne.evaluate("(-1 * Gamma_top * zTb) / (2 * pi * rTb) * core_shape * decay") - W3 = ne.evaluate("(Gamma_top * yLocs) / (2 * pi * rTb) * core_shape * decay") + core_shape = ne.evaluate("1 - exp(-rTb_squared / (eps ** 2))") + V3 = ne.evaluate("(-1 * Gamma_top * zTb) / (2 * pi * rTb_squared) * core_shape * decay") + W3 = ne.evaluate("(Gamma_top * yLocs) / (2 * pi * rTb_squared) * core_shape * decay") # bottom vortex - ground zBb = z + (HH - D / 2) + NUM_EPS - rBb = ne.evaluate("yLocs ** 2 + zBb ** 2") - core_shape = ne.evaluate("1 - exp(-rBb / (eps ** 2))") - V4 = ne.evaluate("(-1 * Gamma_bottom * zBb) / (2 * pi * rBb) * core_shape * decay") - W4 = ne.evaluate("(Gamma_bottom * yLocs) / (2 * pi * rBb) * core_shape * decay") + rBb_squared = ne.evaluate("yLocs ** 2 + zBb ** 2") + core_shape = ne.evaluate("1 - exp(-rBb_squared / (eps ** 2))") + V4 = ne.evaluate("(-1 * Gamma_bottom * zBb) / (2 * pi * rBb_squared) * core_shape * decay") + W4 = ne.evaluate("(Gamma_bottom * yLocs) / (2 * pi * rBb_squared) * core_shape * decay") # wake rotation vortex - ground effect zCb = z + HH + NUM_EPS - rCb = ne.evaluate("yLocs ** 2 + zCb ** 2") - core_shape = ne.evaluate("1 - exp(-rCb / (eps ** 2))") - V6 = ne.evaluate("(-1 * Gamma_wake_rotation * zCb) / (2 * pi * rCb) * core_shape * decay") - W6 = ne.evaluate("(Gamma_wake_rotation * yLocs) / (2 * pi * rCb) * core_shape * decay") + rCb_squared = ne.evaluate("yLocs ** 2 + zCb ** 2") + core_shape = ne.evaluate("1 - exp(-rCb_squared / (eps ** 2))") + V6 = ne.evaluate( + "(-1 * Gamma_wake_rotation * zCb) / (2 * pi * rCb_squared) * core_shape * decay" + ) + W6 = ne.evaluate( + "(Gamma_wake_rotation * yLocs) / (2 * pi * rCb_squared) * core_shape * decay" + ) # total spanwise velocity V = V1 + V2 + V3 + V4 + V5 + V6 diff --git a/floris/core/wake_turbulence/crespo_hernandez.py b/floris/core/wake_turbulence/crespo_hernandez.py index b5c623fe00..d87987d7ab 100644 --- a/floris/core/wake_turbulence/crespo_hernandez.py +++ b/floris/core/wake_turbulence/crespo_hernandez.py @@ -23,13 +23,29 @@ class CrespoHernandez(BaseModel): 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 initial ambient turbulence - intensity, expressed as a decimal fraction. + - **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 diff --git a/floris/core/wake_velocity/gauss.py b/floris/core/wake_velocity/gauss.py index bac3cf4150..1aa2e2f957 100644 --- a/floris/core/wake_velocity/gauss.py +++ b/floris/core/wake_velocity/gauss.py @@ -132,7 +132,7 @@ def function( sigma_z *= (x >= xR) sigma_z += np.ones_like(sigma_z) * (x < xR) * 0.5 * rotor_diameter_i - r, C = rC( + r_squared, C = rC( wind_veer, sigma_y, sigma_z, @@ -146,7 +146,7 @@ def function( rotor_diameter_i, ) - near_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) + near_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) near_wake_deficit *= near_wake_mask velocity_deficit += near_wake_deficit @@ -160,7 +160,7 @@ def function( 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, C = rC( + r_squared, C = rC( wind_veer, sigma_y, sigma_z, @@ -174,7 +174,7 @@ def function( rotor_diameter_i, ) - far_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) + far_wake_deficit = gaussian_function(C, r_squared, 1, np.sqrt(0.5)) far_wake_deficit *= far_wake_mask velocity_deficit += far_wake_deficit @@ -189,7 +189,7 @@ def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): # 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 = ( + # r_squared = ( # a * (y - y_i - delta) ** 2 # - 2 * b * (y - y_i - delta) * (z - HH) # + c * (z - HH) ** 2 @@ -204,7 +204,7 @@ def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): # 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 = (a * (delta_y ** 2) - 2 * b * (delta_y) * (delta_z) + c * (delta_z ** 2)) + # 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 @@ -218,12 +218,12 @@ def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): c = ne.evaluate( "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" ) - r = ne.evaluate( + 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, C + return r_squared, C def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine_yaw): @@ -232,6 +232,6 @@ def mask_upstream_wake(mesh_y_rotated, x_coord_rotated, y_coord_rotated, turbine return xR, yR -def gaussian_function(C, r, n, sigma): - result = ne.evaluate("C * exp(-1 * r ** n / (2 * sigma ** 2))") +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/floris_model.py b/floris/floris_model.py index 333ecde624..b475303c33 100644 --- a/floris/floris_model.py +++ b/floris/floris_model.py @@ -50,22 +50,27 @@ class FlorisModel(LoggingManager): underlying methods within the FLORIS framework. It is meant to act as a single entry-point for the majority of users, simplifying the calls to methods on objects within FLORIS. - - Args: - configuration (:py:obj:`dict`): The Floris configuration dictionary or YAML file. - The configuration should have the following inputs specified. - - **flow_field**: See `floris.simulation.flow_field.FlowField` for more details. - - **farm**: See `floris.simulation.farm.Farm` for more details. - - **turbine**: See `floris.simulation.turbine.Turbine` for more details. - - **wake**: See `floris.simulation.wake.WakeManager` for more details. - - **logging**: See `floris.simulation.core.Core` for more details. """ @staticmethod def get_defaults() -> dict: + """ + Load the default FLORIS configuration dictionary. + + Returns: + dict: The default FLORIS configuration dictionary. + """ return copy.deepcopy(load_yaml(Path(__file__).parent / "default_inputs.yaml")) def __init__(self, configuration: dict | str | Path): + """ + Initialize the FlorisModel object. + + Args: + configuration: The Floris configuration dictionary or path to the input YAML file. + See floris.default_inputs.yaml for an example of the configuration dictionary + or visit https://nrel.github.io/floris/input_reference_main.html. + """ if configuration == "defaults": configuration = FlorisModel.get_defaults() @@ -1105,11 +1110,11 @@ def calculate_cross_plane( ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` - object containing the velocity field in a horizontal plane cut through - the simulation domain at a specific height. + object containing the velocity field in a vertical plane cut through + the simulation domain at a specific downstream (x) distance. Args: - downstream_dist (float): Distance downstream of turbines to compute. + downstream_dist (float): Distance downstream to compute. y_resolution (float, optional): Output array resolution. Defaults to 200 points. z_resolution (float, optional): Output array resolution. @@ -1121,7 +1126,7 @@ def calculate_cross_plane( finder_for_viz (int, optional): Index of the condition to visualize. Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values - of x, y, u, v, w + of y, z, u, v, w """ if self.n_findex > 1 and findex_for_viz is None: self.logger.warning( @@ -1241,11 +1246,11 @@ def calculate_y_plane( ): """ Shortcut method to instantiate a :py:class:`~.tools.cut_plane.CutPlane` - object containing the velocity field in a horizontal plane cut through - the simulation domain at a specific height. + object containing the velocity field in a vertical plane cut through + the simulation domain at a specific cross-stream (y) distance. Args: - height (float): Height of cut plane. Defaults to Hub-height. + crossstream_dist (float): Cross-stream distance to compute. x_resolution (float, optional): Output array resolution. Defaults to 200 points. z_resolution (float, optional): Output array resolution. @@ -1259,7 +1264,7 @@ def calculate_y_plane( Returns: :py:class:`~.tools.cut_plane.CutPlane`: containing values - of x, y, u, v, w + of x, z, u, v, w """ if self.n_findex > 1 and findex_for_viz is None: self.logger.warning( @@ -1286,7 +1291,7 @@ def calculate_y_plane( # Get the points of data in a dataframe # TODO this just seems to be flattening and storing the data in a df; is this necessary? - # It seems the biggest depenedcy is on CutPlane and the subsequent visualization tools. + # It seems the biggest dependency is on CutPlane and the subsequent visualization tools. df = fmodel_viz.get_plane_of_points( normal_vector="y", planar_coordinate=crossstream_dist, diff --git a/floris/flow_visualization.py b/floris/flow_visualization.py index 0fdfe64518..6fa5db9522 100644 --- a/floris/flow_visualization.py +++ b/floris/flow_visualization.py @@ -97,6 +97,14 @@ def visualize_cut_plane( """ Generate pseudocolor mesh plot of the cut_plane. + Horizontal cut planes are plotted with "North" pointing up the page (assuming the + layout is defined with x pointing East and y pointing North). + + y planes are plotted with flows coming from the left (that is, the layout is rotated + such that the wind is coming from the left). + + Cross planes are plotted "looking downstream". + Args: cut_plane (:py:class:`~.tools.cut_plane.CutPlane`): 2D plane through wind plant. diff --git a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py index 127cca01b5..bdb53dcca1 100644 --- a/floris/optimization/yaw_optimization/yaw_optimizer_sr.py +++ b/floris/optimization/yaw_optimization/yaw_optimizer_sr.py @@ -13,6 +13,11 @@ class YawOptimizationSR(YawOptimization, LoggingManager): + """ + Optimize yaw angles using the Serial Refine (SR) optimization method. + + See :cite:`fleming_sr_2022` for full details on the SR method. + """ def __init__( self, fmodel, @@ -28,8 +33,37 @@ def __init__( """ Instantiate YawOptimizationSR object with a FlorisModel object and assign parameter values. + + Args: + fmodel: An instantiated FlorisModel object. + minimum_yaw_angle: Minimum yaw angle for all turbines [degrees]. Default is 0.0. + maximum_yaw_angle: Maximum yaw angle for all turbines [degrees]. Default is 25.0. + yaw_angles_baseline: Yaw angles to use as a baseline for comparison to optimized + yaw angles [degrees]. If None, defaults to 0.0 for all turbines. + x0: Not used in this optimizer. Included for compatibility with base class. Defaults to + None. + Ny_passes: List of integers defining the number of yaw angles to evaluate + per turbine in each pass of the SR algorithm. The length of the list + defines the number of passes. The first entry can be even or odd, + but all further entries must be even. Default is [5, 4]. + turbine_weights: Weights for each turbine when calculating the + weighted power output during optimization. If None, all turbines + are weighted equally. Default is None. + exclude_downstream_turbines: Not used in this optimizer. Included for compatibility with + base class. Default is True. + verify_convergence: If True, the optimizer will perform additional checks to verify + that the optimal yaw angles have been found. See + YawOptimization._verify_solutions_for_convergence() for more details. """ + # Warn if non-default values are provided for unused inputs + if x0 is not None: + warnings.warn( + "The 'x0' argument is not used in the Serial Refine optimization method " + "and will be ignored.", + UserWarning + ) + # Initialize base class super().__init__( fmodel=fmodel, @@ -58,14 +92,6 @@ def __init__( "This is to ensure the same yaw angles are not evaluated twice between passes." ) - # # Set baseline and optimization settings - # if reduce_ngrid: - # for ti in range(self.nturbs): - # # Force number of grid points to 2 - # self.fmodel.core.farm.turbines[ti].ngrid = 2 - # self.fmodel.core.farm.turbines[ti].initialize_turbine() - # print("Reducing ngrid. Unsure if this functionality works!") - # Save optimization choices to self self.Ny_passes = Ny_passes @@ -178,13 +204,6 @@ def _generate_evaluation_grid(self, pass_depth, turbine_depth): for iw in range(self._n_findex_subset): turbid = self.turbines_ordered_array_subset[iw, turbine_depth] # Turbine to manipulate - # # Check if this turbine needs to be optimized. If not, continue - # if not self._turbs_to_opt_subset[iw, 0, turbid]: - # continue - - # # Remove turbines that need not be optimized - # turbines_ordered = [ti for ti in turbines_ordered if ti in self.turbs_to_opt] - # Grab yaw bounds from self yaw_lb = self._yaw_lbs[iw, turbid] yaw_ub = self._yaw_ubs[iw, turbid] @@ -224,7 +243,7 @@ def _process_evaluation_grid(self): def optimize(self, print_progress=True): """ Find the yaw angles that maximize the power production for every wind direction, - wind speed and turbulence intensity. + wind speed and turbulence intensity using the SR optimization algorithm. """ self.print_progress = print_progress diff --git a/floris/par_floris_model.py b/floris/par_floris_model.py index 83732a5025..a13b9b8cfb 100644 --- a/floris/par_floris_model.py +++ b/floris/par_floris_model.py @@ -31,13 +31,9 @@ def __init__( Initialize the ParFlorisModel object. Args: - configuration: The Floris configuration dictionary or YAML file, or an instantiated - FlorisModel object. The configuration should have the following inputs specified. - - **flow_field**: See `floris.simulation.flow_field.FlowField` for more details. - - **farm**: See `floris.simulation.farm.Farm` for more details. - - **turbine**: See `floris.simulation.turbine.Turbine` for more details. - - **wake**: See `floris.simulation.wake.WakeManager` for more details. - - **logging**: See `floris.simulation.core.Core` for more details. + configuration (:py:obj:`dict`): The Floris configuration dictionary or YAML file. + See floris.default_inputs.yaml for an example of the configuration dictionary + or visit https://nrel.github.io/floris/input_reference_main.html. interface: The parallelization interface to use. Options are "multiprocessing", "pathos", and "concurrent", with possible future support for "mpi4py" max_workers: The maximum number of workers to use. Defaults to -1, which then diff --git a/pyproject.toml b/pyproject.toml index ce55df0f55..b2ae8915f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta" [project] name = "floris" -version = "4.6.2" +version = "4.6.3" description = "A controls-oriented engineering wake model." readme = "README.md" requires-python = ">=3.10, <3.15" authors = [ - { name = "Rafael Mudafort", email = "Rafael.Mudafort@nrel.gov" }, - { name = "Paul Fleming", email = "Paul.Fleming@nrel.gov" }, - { name = "Michael (Misha) Sinner", email = "Michael.Sinner@nrel.gov" }, - { name = "Eric Simley", email = "Eric.Simley@nrel.gov" }, - { name = "Christopher Bay", email = "Christopher.Bay@nrel.gov" }, + { name = "Rafael Mudafort", email = "Rafael.Mudafort@nlr.gov" }, + { name = "Paul Fleming", email = "Paul.Fleming@nlr.gov" }, + { name = "Michael (Misha) Sinner", email = "Michael.Sinner@nlr.gov" }, + { name = "Eric Simley", email = "Eric.Simley@nlr.gov" }, + { name = "Christopher Bay", email = "Christopher.Bay@nlr.gov" }, ] license = { file = "LICENSE.txt" } keywords = ["floris"] @@ -48,7 +48,7 @@ docs = [ "sphinx-book-theme~=1.0", "sphinx-autodoc-typehints>=2,<4", "sphinxcontrib-autoyaml~=1.0", - "sphinxcontrib.mermaid~=1.0", + "sphinxcontrib.mermaid>=1,<3", "bokeh~=3.7", "ruamel.yaml~=0.18.0", ]