Partial wakes verification¶
In the following we investigate the accuracy of various partial wakes models.
We do this by comparison with the approximation of the truth that is provided by the choice rotor_model=grid400
in combination with partial_wakes=rotor_points
.
Our playground is the following turbine and wind setup:
Uniform wind from wind direction 270 degrees
One turbine located at
x = y = 0
, causing a wakeAnother turbine located at
x = 4D
and 1000 differenty
positions betweeny = -500 m
andy = 500 m
, acting as a “probe” of the wind field.
This setup will lead to different partial wakes situations, and the partial wakes models will try to predict the correct effective wake effect at the probe turbine.
Let’s see how well they perform at this task!
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import foxes
import foxes.variables as FV
We start by creating a pandas DataFrame object that contains data for 1000 ambient states, including the different y
positions of the probe turbine:
In [2]:
Ny = 1000
ws0 = 9.0
sdata = pd.DataFrame(index=range(Ny + 1))
sdata.index.name = "state"
sdata["ws"] = ws0
sdata["wd"] = 270
sdata["ti"] = 0.05
sdata["rho"] = 1.225
sdata["y"] = np.linspace(-500, 500, Ny + 1)
sdata
Out[2]:
ws | wd | ti | rho | y | |
---|---|---|---|---|---|
state | |||||
0 | 9.0 | 270 | 0.05 | 1.225 | -500.0 |
1 | 9.0 | 270 | 0.05 | 1.225 | -499.0 |
2 | 9.0 | 270 | 0.05 | 1.225 | -498.0 |
3 | 9.0 | 270 | 0.05 | 1.225 | -497.0 |
4 | 9.0 | 270 | 0.05 | 1.225 | -496.0 |
... | ... | ... | ... | ... | ... |
996 | 9.0 | 270 | 0.05 | 1.225 | 496.0 |
997 | 9.0 | 270 | 0.05 | 1.225 | 497.0 |
998 | 9.0 | 270 | 0.05 | 1.225 | 498.0 |
999 | 9.0 | 270 | 0.05 | 1.225 | 499.0 |
1000 | 9.0 | 270 | 0.05 | 1.225 | 500.0 |
1001 rows × 5 columns
The main foxes calculation is carried out by the following function. Notice how the turbine model SetFarmVars
is used for initializing the different y
positions:
In [3]:
def calc(rotor, pwake, wake):
states = foxes.input.states.StatesTable(
sdata,
output_vars=[FV.WS, FV.WD, FV.TI, FV.RHO],
var2col={FV.WS: "ws", FV.WD: "wd", FV.TI: "ti", FV.RHO: "rho"},
)
ydata = np.full((len(sdata.index), 2), np.nan)
ydata[:, 1] = sdata["y"].to_numpy()
ttype="DTU10MW"
mbook = foxes.ModelBook()
mbook.turbine_models["sety"] = foxes.models.turbine_models.SetFarmVars(
pre_rotor=True
)
mbook.turbine_models["sety"].add_var(FV.Y, ydata)
D = mbook.turbine_types[ttype].D
farm = foxes.WindFarm()
farm.add_turbine(
foxes.Turbine(
xy=np.array([0.0, 0.0]),
turbine_models=[ttype]
),
verbosity=0,
)
farm.add_turbine(
foxes.Turbine(
xy=np.array([4*D, 0.0]),
turbine_models=["sety", ttype],
),
verbosity=0,
)
algo = foxes.algorithms.Downwind(
farm,
states,
wake_models=[wake],
rotor_model=rotor,
partial_wakes=pwake,
mbook=mbook,
verbosity=0,
)
print(f"Calculating rotor = {rotor}, pwake = {pwake}")
farm_results = algo.calc_farm()
return farm_results, D
This function calls the above function for a given list of partial wakes models, called pwakes
:
In [4]:
def run_calc(rotor, pwakes, wake):
results = {}
for pwake in pwakes:
farm_results, D = calc(rotor, pwake, wake)
results[pwake] = farm_results
return results, D
Another function will be used for evaluating the calculation results dictionary, creating a plot of results:
In [5]:
def get_plot(results, D, rotor, figsize=(10, 4)):
fig, ax = plt.subplots(figsize=figsize)
for pwake, fres in results.items():
ls = "--" if pwake=="rotor_points" else "-"
lw = 3 if pwake=="rotor_points" else 2
ax.plot(
fres[FV.Y][:, 1]/D,
fres[FV.REWS][:, 1]/ws0,
linestyle=ls,
linewidth=lw,
alpha=0.8,
label=pwake,
)
title = f"4D behind the rotor, ws0 = {ws0} m/s, rotor = {rotor}"
ax.set_title(title)
ax.set_xlabel("y/D")
ax.legend()
return ax
Now we can start the evaluation. First, let’s look at partial wakes of the grid
type, for a Gaussian wake model:
In [6]:
rotor = "grid400"
wake = "Bastankhah2014_linear_k004"
pwakes = ["rotor_points", "centre", "grid4", "grid9", "grid16", "grid100"]
results, D = run_calc(rotor, pwakes, wake)
ax = get_plot(results, D, rotor)
plt.show()
ax = get_plot(results, D, rotor, figsize=(7, 6))
ax.set_xlim(-0.2, 0.2)
ax.set_ylim(0.73, 0.78)
ax.legend(loc="upper center")
plt.show()
Calculating rotor = grid400, pwake = rotor_points
Calculating rotor = grid400, pwake = centre
Calculating rotor = grid400, pwake = grid4
Calculating rotor = grid400, pwake = grid9
Calculating rotor = grid400, pwake = grid16
Calculating rotor = grid400, pwake = grid100
Next, we repeat the exercise with axiwake
partial wakes, which approach the “true” result with much less wake evaluation points:
In [7]:
rotor = "grid400"
wake = "Bastankhah2014_linear_k004"
pwakes = ["rotor_points", "centre", "axiwake4", "axiwake6", "axiwake9"]
results, D = run_calc(rotor, pwakes, wake)
ax = get_plot(results, D, rotor)
plt.show()
ax = get_plot(results, D, rotor, figsize=(7, 6))
ax.set_xlim(-0.2, 0.2)
ax.set_ylim(0.73, 0.78)
ax.legend(loc="upper center")
plt.show()
Calculating rotor = grid400, pwake = rotor_points
Calculating rotor = grid400, pwake = centre
Calculating rotor = grid400, pwake = axiwake4
Calculating rotor = grid400, pwake = axiwake6
Calculating rotor = grid400, pwake = axiwake9
For top-hat models, the top_hat
partial wakes are exact and fast:
In [8]:
rotor = "grid400"
wake = "Jensen_linear_k004"
pwakes = ["rotor_points", "centre", "grid16", "axiwake9", "top_hat"]
results, D = run_calc(rotor, pwakes, wake)
ax = get_plot(results, D, rotor)
plt.show()
ax = get_plot(results, D, rotor, figsize=(7, 6))
ax.set_xlim(-0.5, 0.5)
ax.set_ylim(0.65, 0.78)
ax.legend(loc="upper center")
plt.show()
Calculating rotor = grid400, pwake = rotor_points
Calculating rotor = grid400, pwake = centre
Calculating rotor = grid400, pwake = grid16
Calculating rotor = grid400, pwake = axiwake9
Calculating rotor = grid400, pwake = top_hat
Clearly the top_hat
choice is the only one that can handle wake models with top_hat
shape.