import numpy as np
from foxes.core import WakeK
from foxes.models.wake_models.gaussian import GaussianWakeModel
from foxes.config import config
import foxes.variables as FV
import foxes.constants as FC
[docs]
class TurbOParkWake(GaussianWakeModel):
"""
The TurbOPark wake model
Notes
-----
Reference:
"Turbulence Optimized Park model with Gaussian wake profile"
J G Pedersen, E Svensson, L Poulsen and N G Nygaard
https://iopscience.iop.org/article/10.1088/1742-6596/2265/2/022063/pdf
Attributes
----------
sbeta_factor: float
Factor multiplying sbeta
c1: float
Factor from Frandsen turbulence model
c2: float
Factor from Frandsen turbulence model
induction: foxes.core.AxialInductionModel or str
The induction model
wake_k: foxes.core.WakeK
Handler for the wake growth parameter k
:group: models.wake_models.wind
"""
[docs]
def __init__(
self,
superposition,
sbeta_factor=0.25,
c1=1.5,
c2=0.8,
induction="Madsen",
**wake_k,
):
"""
Constructor.
Parameters
----------
superposition: str
The wind deficit superposition
sbeta_factor: float
Factor multiplying sbeta
c1: float
Factor from Frandsen turbulence model
c2: float
Factor from Frandsen turbulence model
induction: foxes.core.AxialInductionModel or str
The induction model
wake_k: dict, optional
Parameters for the WakeK class
"""
super().__init__(superpositions={FV.WS: superposition})
self.sbeta_factor = sbeta_factor
self.c1 = c1
self.c2 = c2
self.induction = induction
self.wake_k = WakeK(**wake_k)
[docs]
def __repr__(self):
iname = (
self.induction if isinstance(self.induction, str) else self.induction.name
)
s = f"{type(self).__name__}"
s += f"({self.superpositions[FV.WS]}, induction={iname}, "
s += self.wake_k.repr() + ")"
return s
[docs]
def sub_models(self):
"""
List of all sub-models
Returns
-------
smdls: list of foxes.core.Model
All sub models
"""
return [self.wake_k, self.induction]
[docs]
def initialize(self, algo, verbosity=0, force=False):
"""
Initializes the model.
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
verbosity: int
The verbosity level, 0 = silent
force: bool
Overwrite existing data
"""
if isinstance(self.induction, str):
self.induction = algo.mbook.axial_induction[self.induction]
super().initialize(algo, verbosity, force)
[docs]
def calc_amplitude_sigma(
self,
algo,
mdata,
fdata,
tdata,
downwind_index,
x,
):
"""
Calculate the amplitude and the sigma,
both depend only on x (not on r).
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
mdata: foxes.core.MData
The model data
fdata: foxes.core.FData
The farm data
tdata: foxes.core.TData
The target point data
downwind_index: int
The index in the downwind order
x: numpy.ndarray
The x values, shape: (n_states, n_targets)
Returns
-------
amsi: tuple
The amplitude and sigma, both numpy.ndarray
with shape (n_st_sel,)
st_sel: numpy.ndarray of bool
The state-target selection, for which the wake
is non-zero, shape: (n_states, n_targets)
"""
# get ct:
ct = self.get_data(
FV.CT,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=True,
)
# select targets:
st_sel = (x > 1e-8) & (ct > 1e-8)
if np.any(st_sel):
# apply selection:
x = x[st_sel]
ct = ct[st_sel]
# get D:
D = self.get_data(
FV.D,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=False,
selection=st_sel,
)
# get TI:
ati = self.get_data(
FV.AMB_TI,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=True,
)
# get k:
k = self.wake_k(
FC.STATE_TARGET,
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
amb_ti=ati,
upcast=False,
selection=st_sel,
)
# calculate sigma:
# beta = np.sqrt(0.5 * (1 + np.sqrt(1.0 - ct)) / np.sqrt(1.0 - ct))
a = self.induction.ct2a(ct)
beta = (1 - a) / (1 - 2 * a)
epsilon = self.sbeta_factor * np.sqrt(beta)
del a, beta
ati = ati[st_sel]
alpha = self.c1 * ati
beta = self.c2 * ati / np.sqrt(ct)
# calculate sigma (eqn 4)
sigma = D * (
epsilon
+ k
/ beta
* (
np.sqrt((alpha + beta * x / D) ** 2 + 1)
- np.sqrt(1 + alpha**2)
- np.log(
(np.sqrt((alpha + beta * x / D) ** 2 + 1) + 1)
* alpha
/ ((np.sqrt(1 + alpha**2) + 1) * (alpha + beta * x / D))
)
)
)
del (
x,
alpha,
beta,
epsilon,
)
# calculate amplitude, same as in Bastankhah model (eqn 7)
ct_eff = ct / (8 * (sigma / D) ** 2)
ampld = np.maximum(-2 * self.induction.ct2a(ct_eff), -1)
# case no targets:
else:
st_sel = np.zeros_like(x, dtype=bool)
n_sp = np.sum(st_sel)
ampld = np.zeros(n_sp, dtype=config.dtype_double)
sigma = np.zeros(n_sp, dtype=config.dtype_double)
return {FV.WS: (ampld, sigma)}, st_sel
[docs]
class TurbOParkWakeIX(GaussianWakeModel):
"""
The generalized TurbOPark wake model, integrating TI over the streamline.
https://iopscience.iop.org/article/10.1088/1742-6596/2265/2/022063/pdf
Attributes
----------
dx: float
The step size of the integral
sbeta_factor: float
Factor multiplying sbeta
self_wake: bool
Flag for considering only own wake in ti integral
induction: foxes.core.AxialInductionModel or str
The induction model
ipars: dict
Additional parameters for centreline integration
wake_k: foxes.core.WakeK
Handler for the wake growth parameter k
:group: models.wake_models.wind
"""
[docs]
def __init__(
self,
superposition,
dx,
sbeta_factor=0.25,
self_wake=True,
induction="Madsen",
ipars={},
**wake_k,
):
"""
Constructor.
Parameters
----------
superposition: str
The wind deficit superposition
dx: float
The step size of the integral
sbeta_factor: float
Factor multiplying sbeta
self_wake: bool
Flag for considering only own wake in ti integral
induction: foxes.core.AxialInductionModel or str
The induction model
ipars: dict
Additional parameters for centreline integration
wake_k: dict, optional
Parameters for the WakeK class
"""
super().__init__(superpositions={FV.WS: superposition})
self.dx = dx
self.sbeta_factor = sbeta_factor
self.ipars = ipars
self._tiwakes = None
self.self_wake = self_wake
self.induction = induction
self.wake_k = WakeK(**wake_k)
assert not self.wake_k.is_kTI, f"{self.name}: Cannot apply ka or ambka setup"
[docs]
def __repr__(self):
iname = (
self.induction if isinstance(self.induction, str) else self.induction.name
)
s = f"{type(self).__name__}"
s += f"({self.superpositions[FV.WS]}, induction={iname}, dx={self.dx}, "
s += self.wake_k.repr() + ")"
return s
[docs]
def sub_models(self):
"""
List of all sub-models
Returns
-------
smdls: list of foxes.core.Model
All sub models
"""
return [self.wake_k, self.induction]
[docs]
def initialize(self, algo, verbosity=0, force=False):
"""
Initializes the model.
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
verbosity: int
The verbosity level, 0 = silent
force: bool
Overwrite existing data
"""
if isinstance(self.induction, str):
self.induction = algo.mbook.axial_induction[self.induction]
super().initialize(algo, verbosity, force)
[docs]
def new_wake_deltas(self, algo, mdata, fdata, tdata):
"""
Creates new empty wake delta arrays.
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
mdata: foxes.core.MData
The model data
fdata: foxes.core.FData
The farm data
tdata: foxes.core.TData
The target point data
Returns
-------
wake_deltas: dict
Key: variable name, value: The zero filled
wake deltas, shape: (n_states, n_turbines, n_rpoints, ...)
"""
# find TI wake model:
self._tiwakes = []
for w in algo.wake_models.values():
if w is not self:
wdel = w.new_wake_deltas(algo, mdata, fdata, tdata)
if self.wake_k.ti_var in wdel:
self._tiwakes.append(w)
if self.wake_k.ti_var not in FV.amb2var and len(self._tiwakes) == 0:
raise KeyError(
f"Model '{self.name}': Missing wake model that computes wake delta for variable {self.wake_k.ti_var}"
)
return super().new_wake_deltas(algo, mdata, fdata, tdata)
[docs]
def calc_amplitude_sigma(
self,
algo,
mdata,
fdata,
tdata,
downwind_index,
x,
):
"""
Calculate the amplitude and the sigma,
both depend only on x (not on r).
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
mdata: foxes.core.MData
The model data
fdata: foxes.core.FData
The farm data
tdata: foxes.core.TData
The target point data
downwind_index: int
The index in the downwind order
x: numpy.ndarray
The x values, shape: (n_states, n_targets)
Returns
-------
amsi: tuple
The amplitude and sigma, both numpy.ndarray
with shape (n_st_sel,)
st_sel: numpy.ndarray of bool
The state-target selection, for which the wake
is non-zero, shape: (n_states, n_targets)
"""
# get ct:
ct = self.get_data(
FV.CT,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=True,
)
# select targets:
st_sel = (x > 1e-8) & (ct > 1e-8)
if np.any(st_sel):
# apply selection:
# x = x[st_sel]
ct = ct[st_sel]
# get D:
D = self.get_data(
FV.D,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=False,
selection=st_sel,
)
# get k:
k = self.wake_k(
FC.STATE_TARGET,
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=False,
selection=st_sel,
)
# calculate sigma:
# beta = np.sqrt(0.5 * (1 + np.sqrt(1.0 - ct)) / np.sqrt(1.0 - ct))
a = self.induction.ct2a(ct)
beta = (1 - a) / (1 - 2 * a)
epsilon = self.sbeta_factor * np.sqrt(beta)
del a, beta
# get TI by integration along centre line:
ti_ix = algo.wake_frame.calc_centreline_integral(
algo,
mdata,
fdata,
downwind_index,
[self.wake_k.ti_var],
x,
dx=self.dx,
wake_models=self._tiwakes,
self_wake=self.self_wake,
**self.ipars,
)[:, :, 0]
# calculate sigma (eqn 1, plus epsilon from eqn 4 for x = 0)
sigma = D * epsilon + k * ti_ix[st_sel]
del x, epsilon
# calculate amplitude, same as in Bastankhah model (eqn 7)
ct_eff = ct / (8 * (sigma / D) ** 2)
ampld = np.maximum(-2 * self.induction.ct2a(ct_eff), -1)
# case no targets:
else:
st_sel = np.zeros_like(x, dtype=bool)
n_sp = np.sum(st_sel)
ampld = np.zeros(n_sp, dtype=config.dtype_double)
sigma = np.zeros(n_sp, dtype=config.dtype_double)
return {FV.WS: (ampld, sigma)}, st_sel
[docs]
def finalize(self, algo, verbosity=0):
"""
Finalizes the model.
Parameters
----------
algo: foxes.core.Algorithm
The calculation algorithm
verbosity: int
The verbosity level, 0 = silent
"""
super().finalize(algo, verbosity)
self._tiwakes = None