Dynamic Wakes 1

Wake propagation in heterogeneous backgrounds

foxes can model dynamic wake propagation for timeseries inflow states based on passive transport of the wake flow by the ambient wind vectors. This can be switched on by selecting a wake frame of DynamicWakes type.

Since all foxes computations are based on chunks of input states, wake propagation only works if

  • either all states fall into a single chunk,

  • or the Iterative algorithm is used for the calculation.

The reason is that in cases where the wake originates from a state that belongs to a chunk previous to the chunk of evaluation, data has to be passed between two different chunks. This is not possible with the default Downwind algorithm, since it does not allow cross-chunk communication. Hence iterative computations are required for muti-chunk cases with dynamic wakes.

These are the imports for this example:

In [1]:
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams["animation.html"] = "jshtml"

import foxes
import foxes.constants as FC
import foxes.variables as FV

First, we initialize the engine which is then used for all computations:

In [2]:
engine = foxes.Engine.new("process", chunk_size_states=20, chunk_size_points=4000)
engine.initialize()

We continue by defining timeseries inflow at a single point. For this example we modify the wind speed data of the underlying data file (which is shipped as part of foxes provided static data), in order to include oscillating time variations:

In [3]:
sdata = pd.read_csv(
    foxes.StaticData().get_file_path(foxes.STATES, "timeseries_100.csv.gz"),
    index_col=0,
    parse_dates=[0],
)
n_times = len(sdata.index)
sdata["ws"] = 5 + 0.3 * np.sin(np.arange(n_times) * 2 * np.pi / 20)

fig, axs = plt.subplots(2, 1, figsize=(9, 3), sharex=True)
axs[0].plot(sdata.index, sdata.ws)
axs[0].set_ylabel("ws [m/s]")
axs[1].plot(sdata.index, sdata.wd)
axs[1].set_xlabel("Time")
axs[1].set_ylabel("wd [deg]")
plt.show()
../_images/notebooks_dyn_wakes_6_0.png

The above data are defined at a single point. Since we are interested in inhomogeneous background flows, we plug this into the OnePointFlowTimeseries states class in the following. This inflow propagates the given local information from the given ref_xy location to any other point of evaluation. Similarly to the dynamic wakes approach, this is also based on passive transport by time dependent flow vectors and travel time. Note that this implies that this inflow requires the Iterative algorithm, also when combined with non-dynamic wake frames.

In [4]:
ref_xy = [2500, 2500]
states = foxes.input.states.OnePointFlowTimeseries(
    data_source=sdata,
    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},
    ref_xy=ref_xy,
)

We investigate the wakes of a simple 3 x 3 regular wind farm:

In [5]:
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,
)

ax = foxes.output.FarmLayoutOutput(farm).get_figure()
plt.show()
../_images/notebooks_dyn_wakes_10_0.png

As stressed above, the iterative approach is required for this case. Here we select the DynamicWakes wake frame with maximal wake length of 8 km, which is called dyn_wakes_8km in the model book. Similarly, any other maximal wake length can be specified. Note that the choice of maximal wake length, in combination with the number of chunks, determines the number of iterations during calculations.

In [6]:
algo = foxes.algorithms.Iterative(
    farm,
    states=states,
    rotor_model="centre",
    wake_models=["Bastankhah2014_linear_loc_k004"],
    wake_frame="dyn_wakes_8km",
    verbosity=1,
)

Notice the usage of the linear_loc wake superposition, which rescales the linearly added dimensionless wake deficits to the local ambient wind speed at any point of evaluation, in contrast to the rotor equivalent wind speed at the wake causing turbine as for the choice linear. Other choices with such rescaling are quadratic_loc, cubic_loc, quartic_loc, and also product superposition (note that it is not required to select a local superposition for dynamic wakes).

Now let’s run the distributed computations via the pool of parallel processes:

In [7]:
farm_results = algo.calc_farm()

Algorithm Iterative: Iteration 0

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00,  9.84it/s]
Iterative: Convergence blocked

Algorithm Iterative: Iteration 1

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00,  7.45it/s]
Iterative: Convergence blocked

Algorithm Iterative: Iteration 2

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00,  7.10it/s]
Iterative: Convergence blocked

Algorithm Iterative: Iteration 3

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 41.81it/s]
Iterative: Convergence blocked

Algorithm Iterative: Iteration 4

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 39.58it/s]
Iterative: Convergence blocked

Algorithm Iterative: Iteration 5

ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 74.10it/s]

DefaultConv: Convergence check
  REWS: delta = 0.000e+00, 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

Algorithm Iterative: Convergence reached.

Starting final run
ProcessEngine: Calculating 100 states for 9 turbines
ProcessEngine: Computing 5 chunks using 16 processes
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 179.34it/s]



Notice that the convergence is “blocked” until all wakes have reached the selected maximal wake length.

These are the resulting rotor effective wind speed results at the middle row of turbines (indices 1, 4, 7):

In [8]:
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()
../_images/notebooks_dyn_wakes_16_0.png

Now let’s have a look at the wind field dynamics. We do so by creating an output Animator to which we add a flow plot generator:

In [9]:
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:
of = foxes.output.FlowPlots2D(algo, farm_results)
anim.add_generator(
    of.gen_states_fig_xy(
        FV.WS,
        resolution=30,
        quiver_pars=dict(scale=0.013),
        quiver_n=35,
        xmax=5000,
        ymax=5000,
        fig=fig,
        ax=axs[0],
        vmin=0,
        title=lambda si, s: f"t = {si/6:3.2f} min",
        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,
)

ani = anim.animate()

# This adds a cross for the inflow measurement point
axs[0].scatter([ref_xy[0]], [ref_xy[1]], marker="x", color="red", s=80, animated=True)

plt.close()
print("done.")

print("Creating animation")
ani
Creating animation data
ProcessEngine: Calculating data at 33856 points for 100 states
ProcessEngine: Computing 45 chunks using 16 processes
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 45/45 [00:04<00:00, 10.10it/s]
done.
Creating animation
Out[9]:

The red cross in the animation marks the location at which the inflow data was given.

Finally, let’s close the engine, since our work is done here:

In [10]:
engine.finalize()