Skip to content

Commit 01115bf

Browse files
authored
Merge pull request #921 from NREL/develop
FLORIS v4.1
2 parents 7d7c0c4 + e6f1c16 commit 01115bf

File tree

53 files changed

+4354
-364
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+4354
-364
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,9 @@ examples/SLSQP.out
4040

4141
# Log files
4242
*.log
43+
44+
# Temp files in convert test
45+
tests/v3_to_v4_convert_test/convert_turbine_v3_to_v4.py
46+
tests/v3_to_v4_convert_test/convert_floris_input_v3_to_v4.py
47+
tests/v3_to_v4_convert_test/gch_v4.yaml
48+
tests/v3_to_v4_convert_test/nrel_5MW_v3_v4.yaml

README.md

+2-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.0.1](https://github.com/NREL/floris/releases/latest).
6+
release is [FLORIS v4.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
@@ -79,7 +79,7 @@ PACKAGE CONTENTS
7979
wind_data
8080

8181
VERSION
82-
4
82+
4.1
8383

8484
FILE
8585
~/floris/floris/__init__.py

docs/_toc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ parts:
1414
- file: intro_concepts
1515
- file: advanced_concepts
1616
- file: wind_data_user
17+
- file: heterogeneous_map
1718
- file: floating_wind_turbine
1819
- file: turbine_interaction
1920
- file: operation_models_user
21+
- file: layout_optimization
2022
- file: input_reference_main
2123
- file: input_reference_turbine
2224
- file: examples

docs/empirical_gauss_model.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ The effect of AWC is represented by updating the
172172
wake-induced mixing term as follows:
173173

174174
$$ \text{WIM}_j = \sum_{i \in T^{\text{up}}(j)} \frac{A_{ij} a_i} {(x_j - x_i)/D_i} +
175-
\frac{\beta_{j}^{p}{d}$$
175+
\frac{\beta_{j}^{p}}{d}$$
176176

177177
where $\beta_{j}$ is the AWC amplitude of turbine $j$, and the exponent $p$ and
178178
denominator $d$ are tuning parameters that can be set in the `emgauss.yaml` file with

docs/heterogeneous_map.ipynb

+507
Large diffs are not rendered by default.

docs/layout_optimization.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
(layout_optimization)=
3+
# Layout optimization
4+
5+
The FLORIS package provides layout optimization tools to place turbines within a specified
6+
boundary area to optimize annual energy production (AEP) or wind plant value. Layout
7+
optimizers accept an instantiated `FlorisModel` and alter the turbine layouts in order to
8+
improve the objective function value (AEP or value).
9+
10+
## Background
11+
12+
Layout optimization entails placing turbines in a wind farm in a configuration that maximizes
13+
an objective function, usually the AEP. Turbines are moved to minimize their wake interactions
14+
in the most dominant wind directions, while respecting the boundaries of the area for turbine
15+
placement as well as minimum distance requirements between neighboring turbines.
16+
17+
Mathematically, we represent this as a (nonconvex) optimization problem.
18+
Let $x = \{x_i\}_{i=1,\dots,N}$, $x_i \in \mathbb{R}^2$ represent the set of
19+
coordinates of turbines within a farm (that is, $x_i$ represents the $(X, Y)$
20+
location of turbine $i$). Further, let $R \subset \mathbb{R}^2$ be a closed
21+
region in which to place turbines. Finally, let $d$ represent the minimum
22+
allowable distance between two turbines. Then, the layout optimization problem
23+
is expressed as
24+
25+
$$
26+
\begin{aligned}
27+
\underset{x}{\text{maximize}} & \:\: f(x)\\
28+
\text{subject to} & \:\: x \subset R \\
29+
& \:\: ||x_i - x_j|| \geq d \:\: \forall j = 1,\dots,N, j\neq i
30+
\end{aligned}
31+
$$
32+
33+
Here, $||\cdot||$ denotes the Euclidean norm, and $f(x)$ is the cost function to be maximized.
34+
35+
When maximizing the AEP, $f = \sum_w P(w, x)p_W(w)$, where $w$ is the wind condition bin
36+
(e.g., wind speed, wind direction pair); $P(w, x)$ is the power produced by the wind farm in
37+
condition $w$ with layout $x$; and $p_W(w)$ is the annual frequency of occurrence of
38+
condition $w$.
39+
40+
Layout optimizers take iterative approaches to solving the layout optimization problem
41+
specified above. Optimization routines available in FLORIS are described below.
42+
43+
## Scipy layout optimization
44+
The `LayoutOptimizationScipy` class is built around `scipy.optimize`s `minimize`
45+
routine, using the `SLSQP` solver by default. Options for adjusting
46+
`minimize`'s behavior are exposed to the user with the `optOptions` argument.
47+
Other options include enabling fast wake steering at each layout optimizer
48+
iteration with the `enable_geometric_yaw` argument, and switch from AEP
49+
optimization to value optimization with the `use_value` argument.
50+
51+
## Genetic random search layout optimization
52+
The `LayoutOptimizationRandomSearch` class is a custom optimizer designed specifically for
53+
layout optimization via random perturbations of the turbine locations. It is designed to have
54+
the following features:
55+
- Robust to complex wind conditions and complex boundaries, including disjoint regions for
56+
turbine placement
57+
- Easy to parallelize and wrapped in a genetic algorithm for propagating candidate solutions
58+
- Simple to set up and tune for non-optimization experts
59+
- Set up to run cheap constraint checks prior to more expensive objective function evaluations
60+
to accelerate optimization
61+
62+
The algorithm, described in full in an upcoming paper that will be linked here when it is
63+
publicly available, moves a random turbine and random distance in a random direction; checks
64+
that constraints are satisfied; evaluates the objective function (AEP or value); and then
65+
commits to the move if there is an objective function improvement. The main tuning parameter
66+
is the probability mass function for the random movement distance, which is a dictionary to be
67+
passed to the `distance_pmf` argument.
68+
69+
The `distance_pmf` dictionary should contain two keys, each containing a 1D array of equal
70+
length: `"d"`, which specifies the perturbation distance _in units of the rotor diameter_,
71+
and `"p"`, which specifies the probability that the corresponding perturbation distance is
72+
chosen at any iteration of the random search algorithm. The `distance_pmf` can therefore be
73+
used to encourage or discourage more aggressive or more conservative movements, and to enable
74+
or disable jumps between disjoint regions for turbine placement.
75+
76+
The figure below shows an example of the optimized layout of a farm using the GRS algorithm, with
77+
the black dots indicating the initial layout; red dots indicating the final layout; and blue
78+
shading indicating wind speed heterogeneity (lighter shade is lower wind speed, darker shade is
79+
higher wind speed). The progress of each of the genetic individuals in the optimization process is
80+
shown in the right-hand plot.
81+
![](plot_complex_docs.png)

docs/operation_models_user.ipynb

+86
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,92 @@
380380
"cell_type": "markdown",
381381
"id": "25f9c86c",
382382
"metadata": {},
383+
"source": [
384+
"### Peak shaving model\n",
385+
"\n",
386+
"User-level name: `\"peak-shaving\"`\n",
387+
"\n",
388+
"Underlying class: `PeakShavingTurbine`\n",
389+
"\n",
390+
"Required data on `power_thrust_table`:\n",
391+
"- `ref_air_density` (scalar)\n",
392+
"- `ref_tilt` (scalar)\n",
393+
"- `wind_speed` (list)\n",
394+
"- `power` (list)\n",
395+
"- `thrust_coefficient` (list)\n",
396+
"- `peak_shaving_fraction` (scalar)\n",
397+
"- `peak_shaving_TI_threshold` (scalar)\n",
398+
"\n",
399+
"The `\"peak-shaving\"` operation model allows users to implement peak shaving, where the thrust\n",
400+
"of the wind turbine is reduced from the nominal curve near rated to reduce unwanted structural\n",
401+
"loading. Peak shaving here is implemented here by reducing the thrust by a fixed fraction from\n",
402+
"the peak thrust on the nominal thrust curve, as specified by `peak_shaving_fraction`.This only\n",
403+
"affects wind speeds near the peak in the thrust\n",
404+
"curve (usually near rated wind speed), as thrust values away from the peak will be below the\n",
405+
"fraction regardless. Further, peak shaving is only applied if the turbulence intensity experienced\n",
406+
"by the turbine meets the `peak_shaving_TI_threshold`. To apply peak shaving in all wind conditions,\n",
407+
"`peak_shaving_TI_threshold` may be set to zero.\n",
408+
"\n",
409+
"When the turbine is peak shaving to reduce thrust, the power output is updated accordingly. Letting\n",
410+
"$C_{T}$ represent the thrust coefficient when peak shaving (at given wind speed), and $C_{T}'$\n",
411+
"represent the thrust coefficient that the turbine would be operating at under nominal control, then\n",
412+
"the power $P$ due to peak shaving (compared to the power $P'$ available under nominal control) is \n",
413+
"computed (based on actuator disk theory) as\n",
414+
"\n",
415+
"$$ P = \\frac{C_T (1 - a)}{C_T' (1 - a')} P'$$\n",
416+
"\n",
417+
"where $a$ (respectively, $a'$) is the axial induction factor corresponding to $C_T$\n",
418+
"(respectively, $C_T'$), computed using the usual relationship from actuator disk theory,\n",
419+
"i.e. the lesser solution to $C_T=4a(1-a)$.\n"
420+
]
421+
},
422+
{
423+
"cell_type": "code",
424+
"execution_count": null,
425+
"id": "1eff05f3",
426+
"metadata": {},
427+
"outputs": [],
428+
"source": [
429+
"# Set up the FlorisModel\n",
430+
"fmodel = FlorisModel(\"../examples/inputs/gch.yaml\")\n",
431+
"fmodel.set(\n",
432+
" layout_x=[0.0], layout_y=[0.0],\n",
433+
" wind_data=TimeSeries(\n",
434+
" wind_speeds=wind_speeds,\n",
435+
" wind_directions=np.ones(100) * 270.0,\n",
436+
" turbulence_intensities=0.2 # Higher than threshold value of 0.1\n",
437+
" )\n",
438+
")\n",
439+
"fmodel.reset_operation()\n",
440+
"fmodel.set_operation_model(\"simple\")\n",
441+
"fmodel.run()\n",
442+
"powers_base = fmodel.get_turbine_powers()/1000\n",
443+
"thrust_coefficients_base = fmodel.get_turbine_thrust_coefficients()\n",
444+
"fmodel.set_operation_model(\"peak-shaving\")\n",
445+
"fmodel.run()\n",
446+
"powers_peak_shaving = fmodel.get_turbine_powers()/1000\n",
447+
"thrust_coefficients_peak_shaving = fmodel.get_turbine_thrust_coefficients()\n",
448+
"\n",
449+
"fig, ax = plt.subplots(2,1,sharex=True)\n",
450+
"ax[0].plot(wind_speeds, thrust_coefficients_base, label=\"Without peak shaving\", color=\"black\")\n",
451+
"ax[0].plot(wind_speeds, thrust_coefficients_peak_shaving, label=\"With peak shaving\", color=\"C0\")\n",
452+
"ax[1].plot(wind_speeds, powers_base, label=\"Without peak shaving\", color=\"black\")\n",
453+
"ax[1].plot(wind_speeds, powers_peak_shaving, label=\"With peak shaving\", color=\"C0\")\n",
454+
"\n",
455+
"ax[1].grid()\n",
456+
"ax[0].grid()\n",
457+
"ax[0].legend()\n",
458+
"ax[0].set_ylabel(\"Thrust coefficient [-]\")\n",
459+
"ax[1].set_xlabel(\"Wind speed [m/s]\")\n",
460+
"ax[1].set_ylabel(\"Power [kW]\")"
461+
]
462+
},
463+
{
464+
"cell_type": "code",
465+
"execution_count": null,
466+
"id": "92912bf7",
467+
"metadata": {},
468+
"outputs": [],
383469
"source": []
384470
}
385471
],

docs/plot_complex_docs.png

144 KB
Loading

examples/002_visualizations.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@
6969
# we show the horizontal plane at hub height, further examples are provided within
7070
# the examples_visualizations folder
7171

72-
# For flow visualizations, the FlorisModel must be set to run a single condition
73-
# (n_findex = 1)
72+
# For flow visualizations, the FlorisModel may be set to run a single condition
73+
# (n_findex = 1). Otherwise, the user may set multiple conditions and then use
74+
# the findex_for_viz keyword argument to calculate_horizontal_plane to specify which
75+
# flow condition to visualize.
7476
fmodel.set(wind_speeds=[8.0], wind_directions=[290.0], turbulence_intensities=[0.06])
7577
horizontal_plane = fmodel.calculate_horizontal_plane(
7678
x_resolution=200,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Example of using the peak-shaving turbine operation model.
2+
3+
This example demonstrates how to use the peak-shaving operation model in FLORIS.
4+
The peak-shaving operation model allows the user to a thrust reduction near rated wind speed to
5+
reduce loads on the turbine. The power is reduced accordingly, and wind turbine wakes
6+
are shallower due to the reduced thrust.
7+
8+
"""
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
from floris import FlorisModel, TimeSeries
14+
15+
16+
fmodel = FlorisModel("../inputs/gch.yaml")
17+
fmodel.set(layout_x=[0, 1000.0], layout_y=[0.0, 0.0])
18+
wind_speeds = np.linspace(0, 30, 100)
19+
fmodel.set(
20+
wind_data=TimeSeries(
21+
wind_directions=270 * np.ones_like(wind_speeds),
22+
wind_speeds=wind_speeds,
23+
turbulence_intensities=0.10, # High enough to engage peak shaving
24+
)
25+
)
26+
27+
# Start with "normal" operation under the simple turbine operation model
28+
fmodel.set_operation_model("simple")
29+
fmodel.run()
30+
powers_base = fmodel.get_turbine_powers()/1000
31+
thrust_coefficients_base = fmodel.get_turbine_thrust_coefficients()
32+
33+
# Switch to the peak-shaving operation model
34+
fmodel.set_operation_model("peak-shaving")
35+
fmodel.run()
36+
powers_peak_shaving = fmodel.get_turbine_powers()/1000
37+
thrust_coefficients_peak_shaving = fmodel.get_turbine_thrust_coefficients()
38+
39+
# Compare the power and thrust coefficients of the upstream turbine
40+
fig, ax = plt.subplots(2,1,sharex=True)
41+
ax[0].plot(
42+
wind_speeds,
43+
thrust_coefficients_base[:,0],
44+
label="Without peak shaving",
45+
color="black"
46+
)
47+
ax[0].plot(
48+
wind_speeds,
49+
thrust_coefficients_peak_shaving[:,0],
50+
label="With peak shaving",
51+
color="C0"
52+
)
53+
ax[1].plot(
54+
wind_speeds,
55+
powers_base[:,0],
56+
label="Without peak shaving",
57+
color="black"
58+
)
59+
ax[1].plot(
60+
wind_speeds,
61+
powers_peak_shaving[:,0],
62+
label="With peak shaving",
63+
color="C0"
64+
)
65+
ax[1].grid()
66+
ax[0].grid()
67+
ax[0].legend()
68+
ax[0].set_ylabel("Thrust coefficient [-]")
69+
ax[1].set_xlabel("Wind speed [m/s]")
70+
ax[1].set_ylabel("Power [kW]")
71+
72+
# Look at the total power across the two turbines for each case
73+
fig, ax = plt.subplots(2,1,sharex=True,sharey=True)
74+
ax[0].fill_between(
75+
wind_speeds,
76+
0,
77+
powers_base[:, 0]/1e6,
78+
color='C0',
79+
label='Turbine 1'
80+
)
81+
ax[0].fill_between(
82+
wind_speeds,
83+
powers_base[:, 0]/1e6,
84+
powers_base[:, :2].sum(axis=1)/1e6,
85+
color='C1',
86+
label='Turbine 2'
87+
)
88+
ax[0].plot(
89+
wind_speeds,
90+
powers_base[:,:2].sum(axis=1)/1e6,
91+
color='k',
92+
label='Farm'
93+
)
94+
ax[1].fill_between(
95+
wind_speeds,
96+
0,
97+
powers_peak_shaving[:, 0]/1e6,
98+
color='C0',
99+
label='Turbine 1'
100+
)
101+
ax[1].fill_between(
102+
wind_speeds,
103+
powers_peak_shaving[:, 0]/1e6,
104+
powers_peak_shaving[:, :2].sum(axis=1)/1e6,
105+
color='C1',
106+
label='Turbine 2'
107+
)
108+
ax[1].plot(
109+
wind_speeds,
110+
powers_peak_shaving[:,:2].sum(axis=1)/1e6,
111+
color='k',
112+
label='Farm'
113+
)
114+
ax[0].legend()
115+
ax[0].set_title("Without peak shaving")
116+
ax[1].set_title("With peak shaving")
117+
ax[0].set_ylabel("Power [MW]")
118+
ax[1].set_ylabel("Power [MW]")
119+
ax[0].grid()
120+
ax[1].grid()
121+
122+
ax[1].set_xlabel("Free stream wind speed [m/s]")
123+
124+
plt.show()

examples/examples_heterogeneous/001_heterogeneous_inflow_single.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import numpy as np
1818

1919
from floris import FlorisModel, TimeSeries
20-
from floris.flow_visualization import visualize_cut_plane
20+
from floris.flow_visualization import visualize_heterogeneous_cut_plane
2121
from floris.layout_visualization import plot_turbine_labels
2222

2323

@@ -65,15 +65,20 @@
6565
x_resolution=200, y_resolution=100, height=90.0
6666
)
6767

68-
# Plot the horizontal plane
68+
# Plot the horizontal plane using the visualize_heterogeneous_cut_plane.
69+
# Note that this function is not very different than the standard
70+
# visualize_cut_plane except that it accepts the fmodel object in order to
71+
# visualize the boundary of the heterogeneous inflow region.
6972
fig, ax = plt.subplots()
70-
visualize_cut_plane(
73+
visualize_heterogeneous_cut_plane(
7174
horizontal_plane,
75+
fmodel=fmodel,
7276
ax=ax,
7377
title="Horizontal plane at hub height",
7478
color_bar=True,
7579
label_contours=True,
7680
)
7781
plot_turbine_labels(fmodel, ax)
82+
ax.legend()
7883

7984
plt.show()

0 commit comments

Comments
 (0)