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 Iterative algorithm 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