Skip to content

Commit fd37e9b

Browse files
authored
Merge pull request #1029 from NREL/develop
FLORIS v4.2.1
2 parents 440549c + f6b9495 commit fd37e9b

30 files changed

+546
-169
lines changed

.github/workflows/check-working-examples.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ${{ matrix.os }}
99
strategy:
1010
matrix:
11-
python-version: ["3.9", "3.10", "3.11"]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1212
os: [ubuntu-latest] #, macos-latest, windows-latest]
1313
fail-fast: False
1414

.github/workflows/continuous-integration-workflow.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ${{ matrix.os }}
99
strategy:
1010
matrix:
11-
python-version: ["3.8", "3.9", "3.10", "3.11"]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1212
os: [ubuntu-latest] #, macos-latest, windows-latest]
1313
fail-fast: False
1414
env:

.github/workflows/deploy-pages.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Python
1919
uses: actions/setup-python@v4
2020
with:
21-
python-version: "3.10"
21+
python-version: "3.13"
2222

2323
- name: Install dependencies
2424
run: |

.github/workflows/python-publish.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ jobs:
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip
23-
pip install setuptools wheel twine
23+
pip install build twine
2424
- name: Build and publish
2525
env:
2626
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
2727
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
2828
run: |
29-
python setup.py sdist bdist_wheel
29+
python -m build
3030
twine upload dist/*

.github/workflows/quality-metrics-workflow.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ${{ matrix.os }}
99
strategy:
1010
matrix:
11-
python-version: ["3.10"]
11+
python-version: ["3.13"]
1212
os: [ubuntu-latest]
1313
fail-fast: False
1414

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
FLORIS is a controls-focused wind farm simulation software incorporating
44
steady-state engineering wake models into a performance-focused Python
55
framework. It has been in active development at NREL since 2013 and the latest
6-
release is [FLORIS v4.1.1](https://github.com/NREL/floris/releases/latest).
6+
release is [FLORIS v4.2.1](https://github.com/NREL/floris/releases/latest).
77
Online documentation is available at https://nrel.github.io/floris.
88

99
The software is in active development and engagement with the development team
@@ -13,6 +13,9 @@ the conversation in [GitHub Discussions](https://github.com/NREL/floris/discussi
1313

1414
## Installation
1515

16+
**WARNING:**
17+
Support for python version 3.8 will be dropped in FLORIS v4.3. See [Installation documentation](https://nrel.github.io/floris/installation.html#installation) for details.
18+
1619
**If upgrading from a previous version, it is recommended to install FLORIS v4 into a new virtual environment**.
1720
If you intend to use [pyOptSparse](https://mdolab-pyoptsparse.readthedocs-hosted.com/en/latest/) with FLORIS,
1821
it is recommended to install that package first before installing FLORIS.
@@ -79,7 +82,7 @@ PACKAGE CONTENTS
7982
wind_data
8083

8184
VERSION
82-
4.2
85+
4.2.1
8386

8487
FILE
8588
~/floris/floris/__init__.py

docs/dev_guide.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ is located at `floris/.github/workflows/continuous-integration-workflow.yaml`.
210210
The online documentation is built with Jupyter Book which uses Sphinx
211211
as a framework. It is automatically built and hosted by GitHub, but it
212212
can also be compiled locally. Additional dependencies are required
213-
for the documentation, and they are listed in the `EXTRAS` of `setup.py`.
213+
for the documentation, and they are listed in the `project.optional-dependencies` of `pyproject.toml`.
214214
The commands to build the docs are given below. After successfully
215215
compiling, a file should be located at ``docs/_build/html/index.html``.
216216
This file can be opened in any browser.
@@ -246,7 +246,7 @@ Be sure to complete each step in the sequence as described.
246246
with a commit message such as "Update version to vN.M".
247247
The version number must be updated in the following two files:
248248
- [floris/README.md](https://github.com/NREL/floris/blob/main/README.md)
249-
- [floris/floris/version.py](https://github.com/NREL/floris/blob/main/floris/version.py)
249+
- [pyproject.toml](https://github.com/NREL/floris/blob/main/pyproject.toml)
250250
Note that a `.0` version number is left off meaning that valid versions
251251
are `v3`, `v3.1`, `v3.1.1`, etc.
252252

docs/index.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ is highly encouraged. If you are interested in using FLORIS to conduct studies
88
of a wind farm or extending FLORIS to include your own wake model, please join
99
the conversation in [GitHub Discussions](https://github.com/NREL/floris/discussions/)!
1010

11+
```{note}
12+
Support for python version 3.8 will be dropped in FLORIS v4.3. See {ref}`installation` for details.
13+
```
14+
1115
## Quick Start
1216

1317
FLORIS is a Python package run on the command line typically by providing

docs/installation.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ The following sections detail how download and install FLORIS for each use case.
77
(requirements)=
88
## Requirements
99

10-
FLORIS is intended to be used with Python 3.8 and up, and it is highly recommended that users
10+
FLORIS is a python package. FLORIS is intended to work with all [active versions of python](https://devguide.python.org/versions/). Support will drop for python versions once they reach end-of-life.
11+
It is highly recommended that users
1112
work within a virtual environment for both working with and working on FLORIS, to maintain a clean
1213
and sandboxed environment. The simplest way to get started with virtual environments is through
1314
[conda](https://docs.conda.io/en/latest/miniconda.html).
1415

16+
```{warning}
17+
Support for python version 3.8 will be dropped in FLORIS v4.3.
18+
```
19+
1520
Installing into a Python environment that contains a previous version of FLORIS may cause conflicts.
1621
If you intend to use [pyOptSparse](https://mdolab-pyoptsparse.readthedocs-hosted.com/en/latest/)
1722
with FLORIS, it is recommended to install that package first before installing FLORIS.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Example: Optimizing yaw angles with disabled turbines
2+
3+
This example demonstrates how to optimize yaw angles in FLORIS, when some turbines are disabled.
4+
The example optimization is run using both YawOptimizerSR and YawOptimizerGeometric, the two
5+
yaw optimizers that support disabling turbines.
6+
"""
7+
8+
import numpy as np
9+
10+
from floris import FlorisModel
11+
from floris.optimization.yaw_optimization.yaw_optimizer_geometric import YawOptimizationGeometric
12+
from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR
13+
14+
15+
# Load a 3-turbine model
16+
fmodel = FlorisModel("../inputs/gch.yaml")
17+
18+
# Set wind conditions to be the same for two cases
19+
fmodel.set(wind_directions=[270.]*2, wind_speeds=[8.]*2, turbulence_intensities=[.06]*2)
20+
21+
# First run the case where all turbines are active and print results
22+
yaw_opt = YawOptimizationSR(fmodel)
23+
df_opt = yaw_opt.optimize()
24+
print("Serial Refine optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt)
25+
26+
yaw_opt = YawOptimizationGeometric(fmodel)
27+
df_opt = yaw_opt.optimize()
28+
print("\nGeometric optimized yaw angles (all turbines active) [deg]:\n", df_opt.yaw_angles_opt)
29+
30+
# Disable turbines (different pattern for each of the two cases)
31+
# First case: disable the middle turbine
32+
# Second case: disable the front turbine
33+
fmodel.set_operation_model('mixed')
34+
fmodel.set(disable_turbines=np.array([[False, True, False], [True, False, False]]))
35+
36+
# Rerun optimizations and print results
37+
yaw_opt = YawOptimizationSR(fmodel)
38+
df_opt = yaw_opt.optimize()
39+
print(
40+
"\nSerial Refine optimized yaw angles (some turbines disabled) [deg]:\n",
41+
df_opt.yaw_angles_opt
42+
)
43+
# Note that disabled turbines are assigned a zero yaw angle, but their yaw angle is arbitrary as it
44+
# does not affect the total power output.
45+
46+
yaw_opt = YawOptimizationGeometric(fmodel)
47+
df_opt = yaw_opt.optimize()
48+
print("\nGeometric optimized yaw angles (some turbines disabled) [deg]:\n", df_opt.yaw_angles_opt)

examples/examples_turbine/003_specify_turbine_power_curve.py

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
wind_speeds=wind_speeds,
5252
turbulence_intensities=turbulence_intensities,
5353
turbine_type=[turbine_dict],
54+
reference_wind_height=fmodel.reference_wind_height
5455
)
5556
fmodel.run()
5657

examples/examples_turbopark/001_compare_turbopark_implementations.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
### Start by visualizing a single turbine in and its wake with the new model
3636
# Load the new TurboPark implementation and switch to constant CT turbine
3737
fmodel_new = FlorisModel("../inputs/turboparkgauss_cubature.yaml")
38-
fmodel_new.set(turbine_type=[const_CT_turb])
38+
fmodel_new.set(
39+
turbine_type=[const_CT_turb],
40+
reference_wind_height=fmodel_new.reference_wind_height
41+
)
3942
fmodel_new.run()
4043
u0 = fmodel_new.wind_speeds[0]
4144

@@ -94,7 +97,10 @@
9497
### Look at the wake profile at a single downstream distance for a range of wind directions
9598
# Load the original TurboPark implementation and switch to constant CT turbine
9699
fmodel_orig = FlorisModel("../inputs/turbopark_cubature.yaml")
97-
fmodel_orig.set(turbine_type=[const_CT_turb])
100+
fmodel_orig.set(
101+
turbine_type=[const_CT_turb],
102+
reference_wind_height=fmodel_orig.reference_wind_height
103+
)
98104

99105
# Set up and solve flows
100106
wd_array = np.arange(225,315,0.1)

floris/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

2+
from importlib.metadata import version
23
from pathlib import Path
34

45

5-
with open(Path(__file__).parent / "version.py") as _version_file:
6-
__version__ = _version_file.read().strip()
6+
__version__ = version("floris")
77

88

99
from .floris_model import FlorisModel

floris/core/turbine/operation_models.py

+61-31
Original file line numberDiff line numberDiff line change
@@ -382,22 +382,22 @@ def axial_induction(
382382
@define
383383
class MixedOperationTurbine(BaseOperationModel):
384384

385+
@staticmethod
385386
def power(
386387
yaw_angles: NDArrayFloat,
387388
power_setpoints: NDArrayFloat,
388389
**kwargs
389390
):
390-
# Yaw angles mask all yaw_angles not equal to zero
391-
yaw_angles_mask = yaw_angles != 0.0
392-
power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT
393-
neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask)
394-
395-
if (power_setpoints_mask & yaw_angles_mask).any():
396-
raise ValueError((
397-
"Power setpoints and yaw angles are incompatible."
398-
"If yaw_angles entry is nonzero, power_setpoints must be greater than"
399-
" or equal to {0}.".format(POWER_SETPOINT_DEFAULT)
400-
))
391+
(
392+
yaw_angles,
393+
power_setpoints,
394+
yaw_angles_mask,
395+
power_setpoints_mask,
396+
neither_mask
397+
) = MixedOperationTurbine._handle_mixed_operation_setpoints(
398+
yaw_angles=yaw_angles,
399+
power_setpoints=power_setpoints
400+
)
401401

402402
powers = np.zeros_like(power_setpoints)
403403
powers[yaw_angles_mask] += CosineLossTurbine.power(
@@ -414,21 +414,22 @@ def power(
414414

415415
return powers
416416

417+
@staticmethod
417418
def thrust_coefficient(
418419
yaw_angles: NDArrayFloat,
419420
power_setpoints: NDArrayFloat,
420421
**kwargs
421422
):
422-
yaw_angles_mask = yaw_angles != 0.0
423-
power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT
424-
neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask)
425-
426-
if (power_setpoints_mask & yaw_angles_mask).any():
427-
raise ValueError((
428-
"Power setpoints and yaw angles are incompatible."
429-
"If yaw_angles entry is nonzero, power_setpoints must be greater than"
430-
" or equal to {0}.".format(POWER_SETPOINT_DEFAULT)
431-
))
423+
(
424+
yaw_angles,
425+
power_setpoints,
426+
yaw_angles_mask,
427+
power_setpoints_mask,
428+
neither_mask
429+
) = MixedOperationTurbine._handle_mixed_operation_setpoints(
430+
yaw_angles=yaw_angles,
431+
power_setpoints=power_setpoints
432+
)
432433

433434
thrust_coefficients = np.zeros_like(power_setpoints)
434435
thrust_coefficients[yaw_angles_mask] += CosineLossTurbine.thrust_coefficient(
@@ -445,21 +446,22 @@ def thrust_coefficient(
445446

446447
return thrust_coefficients
447448

449+
@staticmethod
448450
def axial_induction(
449451
yaw_angles: NDArrayFloat,
450452
power_setpoints: NDArrayFloat,
451453
**kwargs
452454
):
453-
yaw_angles_mask = yaw_angles != 0.0
454-
power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT
455-
neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask)
456-
457-
if (power_setpoints_mask & yaw_angles_mask).any():
458-
raise ValueError((
459-
"Power setpoints and yaw angles are incompatible."
460-
"If yaw_angles entry is nonzero, power_setpoints must be greater than"
461-
" or equal to {0}.".format(POWER_SETPOINT_DEFAULT)
462-
))
455+
(
456+
yaw_angles,
457+
power_setpoints,
458+
yaw_angles_mask,
459+
power_setpoints_mask,
460+
neither_mask
461+
) = MixedOperationTurbine._handle_mixed_operation_setpoints(
462+
yaw_angles=yaw_angles,
463+
power_setpoints=power_setpoints
464+
)
463465

464466
axial_inductions = np.zeros_like(power_setpoints)
465467
axial_inductions[yaw_angles_mask] += CosineLossTurbine.axial_induction(
@@ -476,6 +478,34 @@ def axial_induction(
476478

477479
return axial_inductions
478480

481+
@staticmethod
482+
def _handle_mixed_operation_setpoints(
483+
yaw_angles: NDArrayFloat,
484+
power_setpoints: NDArrayFloat,
485+
):
486+
"""
487+
Check for incompatible yaw angles and power setpoints and raise an error if found.
488+
Return masks and updated setpoints.
489+
"""
490+
# If any turbines are disabled, set their yaw angles to zero
491+
yaw_angles[power_setpoints <= POWER_SETPOINT_DISABLED] = 0.0
492+
493+
# Create masks for whether yaw angles and power setpoints are set
494+
yaw_angles_mask = yaw_angles != 0.0
495+
power_setpoints_mask = power_setpoints < POWER_SETPOINT_DEFAULT
496+
neither_mask = np.logical_not(yaw_angles_mask) & np.logical_not(power_setpoints_mask)
497+
498+
# Check for incompatibility and raise error if found.
499+
if (power_setpoints_mask & yaw_angles_mask).any():
500+
raise ValueError((
501+
"Power setpoints and yaw angles are incompatible."
502+
"If yaw_angles entry is nonzero, power_setpoints must be greater than"
503+
" or equal to {0}.".format(POWER_SETPOINT_DEFAULT)
504+
))
505+
506+
# Return updated setpoints as well as masks
507+
return yaw_angles, power_setpoints, yaw_angles_mask, power_setpoints_mask, neither_mask
508+
479509
@define
480510
class AWCTurbine(BaseOperationModel):
481511
"""

0 commit comments

Comments
 (0)