Dynamic Wakes 2¶
Horizontally homogeneous wake propagation¶
For horizontally homogeneous timeseries input data (i.e., dependency on time and optionally also on z, but not on x, y coordinates), foxes offers a simplified way to compute dynamic wake propagation. This in principle works by following a flow trace backwards in time from each point of interest, and identifying it with a wake trajectory if it approaches the vicinity of a rotor. For horizontally homogeneous inflow the steps of these traces are independent of the evaluation point.
Similarly to the prevously discussed DynamicWakes approach, this concept only works if
either all states fall into a single chunk,
or the
Iterativealgorithm is used for the calculation.
These are the inlcudes for this example:
%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt
import foxes
import foxes.variables as FV
import foxes.constants as FC
plt.rcParams["animation.html"] = "jshtml"
/home/runner/work/foxes/foxes/foxes/core/engine.py:4: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
from tqdm.autonotebook import tqdm
This is the engine that we are going to use throughout all calculations in this notebook:
engine = foxes.Engine.new(
"process", chunk_size_states=20, chunk_size_points=500, n_procs=8
)
We create a case with a regular 3 x 3 wind farm layout:
states = foxes.input.states.Timeseries(
data_source="timeseries_100.csv.gz",
output_vars=[FV.WS, FV.WD, FV.TI, FV.RHO],
var2col={FV.WS: "ws", FV.WD: "wd", FV.TI: "ti"},
fixed_vars={FV.RHO: 1.225, FV.TI: 0.07},
)
farm = foxes.WindFarm()
foxes.input.farm_layout.add_grid(
farm,
xy_base=np.array([0.0, 0.0]),
step_vectors=np.array([[1000.0, 0], [0, 800.0]]),
steps=(3, 3),
turbine_models=["DTU10MW"],
verbosity=0,
)
algo = foxes.algorithms.Iterative(
farm,
states,
wake_models=["Bastankhah2014_linear_lim_k004"],
wake_frame="timelines",
verbosity=1,
)
Notice the wake frame choice timelines, which is a pre-defined instance of the class Timelines from the model book.
Let’s run the wind farm calculation:
with engine:
farm_results = algo.calc_farm()
Algorithm Iterative: Iteration 0
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
Algorithm Iterative: Iteration 1
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
DefaultConv: Convergence check
REWS: delta = 2.446e-01, lim = 1.000e-06 -- FAILED
TI : delta = 0.000e+00, lim = 1.000e-07 -- OK
CT : delta = 3.180e-03, lim = 1.000e-07 -- FAILED
Converged states: 15/100
Algorithm Iterative: Iteration 2
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
DefaultConv: Convergence check
REWS: delta = 8.882e-16, lim = 1.000e-06 -- OK
TI : delta = 0.000e+00, lim = 1.000e-07 -- OK
CT : delta = 0.000e+00, lim = 1.000e-07 -- OK
Converged states: 100/100
Algorithm Iterative: Convergence reached.
Starting final run
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
Notice the iterations and the convergence behaviour, resulting in less iterations than the previous DynamicWakes example. Now the farm results are ready:
farm_df = farm_results.to_dataframe()
print("\nFarm results data:\n")
print(farm_df[[FV.AMB_REWS, FV.REWS, FV.P]])
Farm results data:
AMB_REWS REWS P
state turbine
2023-07-07 12:00:00 0 6.0 6.000000 1532.700000
1 6.0 6.000000 1532.700000
2 6.0 6.000000 1532.700000
3 6.0 6.000000 1532.700000
4 6.0 6.000000 1532.700000
... ... ... ...
2023-07-07 13:39:00 4 6.0 4.796724 693.620333
5 6.0 4.796724 693.620333
6 6.0 4.449712 513.555722
7 6.0 4.449712 513.555710
8 6.0 4.449712 513.555722
[900 rows x 3 columns]
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 1], label="Turbine 1")
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 4], label="Turbine 4")
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 7], label="Turbine 7")
ax.legend()
ax.set_xlabel("Time")
ax.set_ylabel("REWS [m/s]")
plt.show()
This timeseries has a time step of 1 minute. Let’s visualize the wake dynamics in an animation:
# compute data of all images in the animation:
with engine:
of = foxes.output.FlowPlots2D(algo, farm_results)
plot_data = of.get_states_data_xy(
FV.WS,
resolution=30,
xmax=5000,
ymax=5000,
vmin=0,
)
States 'Timeseries': Reading file /home/runner/work/foxes/foxes/foxes/data/states/timeseries_100.csv.gz
ProcessEngine: Calculating data at 33856 points for 100 states
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks and 68 targets chunks.
ProcessEngine: Completed all 340 chunks
# create animator object:
fig, axs = plt.subplots(2, 1, figsize=(5.2, 7), gridspec_kw={"height_ratios": [3, 1]})
anim = foxes.output.Animator(fig)
# this adds the flow anomation to the upper panel:
anim.add_generator(
of.gen_states_fig_xy(
plot_data,
quiver_pars=dict(scale=0.013),
quiver_n=35,
fig=fig,
ax=axs[0],
title=lambda si, s: f"t = {si / 6:3.2f} min",
rotor_color="red",
ret_im=True,
animated=True,
)
)
# This adds the REWS signal animation to the lower panel:
o = foxes.output.FarmResultsEval(farm_results)
anim.add_generator(
o.gen_stdata(
turbines=[1, 4, 7],
variable=FV.REWS,
fig=fig,
ax=axs[1],
ret_im=True,
legloc="upper right",
animated=True,
)
)
# This adds turbine indices at turbine positions:
lo = foxes.output.FarmLayoutOutput(farm)
lo.get_figure(
fig=fig,
ax=axs[0],
title="",
annotate=1,
anno_delx=-120,
anno_dely=-60,
alpha=0,
)
# evaluate the generators, yielding animation frames:
ani = anim.animate()
print("Done.")
Creating animation data
Done.
# display the animation:
ani
# cleanup
del ani, anim, farm_results, fig, axs, plot_data, of, o, lo
For the fun of it, let’s re-run this case assuming the time step was 10 s instead of 1 min. We can do so by using the wake frame Timelines(dt_min=1/6), which is called timelines_10s in the model book:
algo = foxes.algorithms.Iterative(
farm,
states,
wake_models=["Bastankhah2014_linear_lim_k004"],
wake_frame="timelines_10s",
verbosity=1,
)
with engine:
farm_results = algo.calc_farm()
# create the output object that creates flow images:
of = foxes.output.FlowPlots2D(algo, farm_results)
plot_data = of.get_states_data_xy(
FV.WS,
resolution=30,
xmax=5000,
ymax=5000,
vmin=0,
)
Algorithm Iterative: Iteration 0
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
Algorithm Iterative: Iteration 1
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
DefaultConv: Convergence check
REWS: delta = 1.824e-01, lim = 1.000e-06 -- FAILED
TI : delta = 0.000e+00, lim = 1.000e-07 -- OK
CT : delta = 2.371e-03, lim = 1.000e-07 -- FAILED
Converged states: 33/100
Algorithm Iterative: Iteration 2
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
DefaultConv: Convergence check
REWS: delta = 7.008e-08, lim = 1.000e-06 -- OK
TI : delta = 0.000e+00, lim = 1.000e-07 -- OK
CT : delta = 1.051e-09, lim = 1.000e-07 -- OK
Converged states: 100/100
Algorithm Iterative: Convergence reached.
Starting final run
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks.
ProcessEngine: Completed all 5 chunks
States 'Timeseries': Reading file /home/runner/work/foxes/foxes/foxes/data/states/timeseries_100.csv.gz
ProcessEngine: Calculating data at 33856 points for 100 states
ProcessEngine: Starting calculation using 7 workers, for 5 states chunks and 68 targets chunks.
ProcessEngine: Completed all 340 chunks
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 1], label="Turbine 1")
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 4], label="Turbine 4")
ax.plot(farm_results[FC.STATE], farm_results[FV.REWS][:, 7], label="Turbine 7")
ax.legend()
ax.set_xlabel("Time")
ax.set_ylabel("REWS [m/s]")
plt.show()
# create animator object:
fig, axs = plt.subplots(2, 1, figsize=(5.2, 7), gridspec_kw={"height_ratios": [3, 1]})
anim = foxes.output.Animator(fig)
# this adds the flow anomation to the upper panel:
anim.add_generator(
of.gen_states_fig_xy(
plot_data,
quiver_pars=dict(scale=0.013),
quiver_n=35,
fig=fig,
ax=axs[0],
title=lambda si, s: f"t = {si / 6:3.2f} min",
rotor_color="red",
ret_im=True,
animated=True,
)
)
# This adds the REWS signal animation to the lower panel:
o = foxes.output.FarmResultsEval(farm_results)
anim.add_generator(
o.gen_stdata(
turbines=[1, 4, 7],
variable=FV.REWS,
fig=fig,
ax=axs[1],
ret_im=True,
legloc="upper right",
animated=True,
)
)
# This adds turbine indices at turbine positions:
lo = foxes.output.FarmLayoutOutput(farm)
lo.get_figure(
fig=fig,
ax=axs[0],
title="",
annotate=1,
anno_delx=-120,
anno_dely=-60,
alpha=0,
)
# evaluate the generators, yielding animation frames:
ani = anim.animate()
print("Done.")
Creating animation data
Done.
# display the animation:
ani