import numpy as np
from foxes.models.wake_models.axisymmetric import AxisymmetricWakeModel
from foxes.utils.two_circles import calc_area
from foxes.config import config
import foxes.variables as FV
import foxes.constants as FC
from .centre import PartialCentre
[docs]
class PartialAxiwake(PartialCentre):
"""
Partial wake calculation for axial wake models.
The basic idea is that the x-dependent part of
the wake model is evaluated only once, and the radial
part then for `n` radii that cover the target rotor discs.
The latter results are then weighted according to the overlap
of radial wake circle area deltas and the target rotor disc area.
Attributes
----------
n: int
The number of radial evaluation points
:group: models.partial_wakes
"""
[docs]
def __init__(self, n=6):
"""
Constructor.
Parameters
----------
n: int
The number of radial evaluation points
"""
super().__init__()
self.n = n
[docs]
def __repr__(self):
return f"{type(self).__name__}(n={self.n})"
[docs]
def check_wmodel(self, wmodel, error=True):
"""
Checks the wake model type
Parameters
----------
wmodel: foxes.core.WakeModel
The wake model to be tested
error: bool
Flag for raising TypeError
Returns
-------
chk: bool
True if wake model is compatible
"""
if not isinstance(wmodel, AxisymmetricWakeModel):
if error:
raise TypeError(
f"Partial wakes '{self.name}': Cannot be applied to wake model '{wmodel.name}', since not an AxisymmetricWakeModel"
)
return False
return True
[docs]
def contribute(
self,
algo,
mdata,
fdata,
tdata,
downwind_index,
wake_deltas,
wmodel,
):
"""
Modifies wake deltas at target points by
contributions from the specified wake source turbines.
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 of the wake causing turbine
in the downwind order
wake_deltas: dict
The wake deltas. Key: variable name,
value: numpy.ndarray with shape
(n_states, n_targets, n_tpoints, ...)
wmodel: foxes.core.WakeModel
The wake model
"""
# check:
self.check_wmodel(wmodel, error=True)
# prepare:
n_states = mdata.n_states
n_targets = tdata.n_targets
# get D:
D = self.get_data(
FV.D,
FC.STATE_TARGET,
lookup="w",
algo=algo,
fdata=fdata,
tdata=tdata,
downwind_index=downwind_index,
upcast=True,
)
# calc coordinates to rotor centres:
wcoos = algo.wake_frame.get_wake_coos(algo, mdata, fdata, tdata, downwind_index)
# prepare x and r coordinates:
x = wcoos[..., 0, 0]
n = wcoos[..., 0, 1:3]
R = np.linalg.norm(n, axis=-1)
r = np.zeros((n_states, n_targets, self.n), dtype=config.dtype_double)
del wcoos
# prepare circle section area calculation:
A = np.zeros((n_states, n_targets, self.n), dtype=config.dtype_double)
weights = np.zeros_like(A)
# get normalized 2D vector between rotor and wake centres:
sel = R > 0
if np.any(sel):
n[sel] /= R[sel][:, None]
if np.any(~sel):
n[:, :, 0][~sel] = 1
# case wake centre outside rotor disk:
sel = (x > 1e-8) & (R > D / 2)
if np.any(sel):
n_sel = np.sum(sel)
Rsel = np.zeros((n_sel, self.n + 1), dtype=config.dtype_double)
Rsel[:] = R[sel][:, None]
Dsel = D[sel][:, None]
# equal delta R2:
R1 = np.zeros((n_sel, self.n + 1), dtype=config.dtype_double)
R1[:] = Dsel / 2
steps = np.linspace(0.0, 1.0, self.n + 1, endpoint=True) - 0.5
R2 = np.zeros_like(R1)
R2[:] = Rsel + Dsel * steps[None, :]
r[sel] = 0.5 * (R2[:, 1:] + R2[:, :-1])
hA = calc_area(R1, R2, Rsel)
hA = hA[:, 1:] - hA[:, :-1] + 1e-15
weights[sel] = hA / np.sum(hA, axis=-1)[:, None]
del hA, Rsel, Dsel, R1, R2
# case wake centre inside rotor disk:
sel = (x > 0) & (R < D / 2)
if np.any(sel):
n_sel = np.sum(sel)
Rsel = np.zeros((n_sel, self.n + 1), dtype=config.dtype_double)
Rsel[:] = R[sel][:, None]
Dsel = D[sel][:, None]
# equal delta R2:
R1 = np.zeros((n_sel, self.n + 1), dtype=config.dtype_double)
R1[:, 1:] = Dsel / 2
R2 = np.zeros_like(R1)
# R2[:, 1:] = Rsel[:, :-1] + Dsel/2
# R2[:] *= np.linspace(0., 1, self.n + 1, endpoint=True)[None, :]
R2[:, 1:] = (Rsel[:, :-1] + Dsel / 2) / (self.n - 0.5)
R2[:, 1:] *= (
0.5 + np.linspace(0.0, self.n - 1, self.n, endpoint=True)[None, :]
)
hr = 0.5 * (R2[:, 1:] + R2[:, :-1])
hr[:, 0] = 0.0
r[sel] = hr
hA = calc_area(R1, R2, Rsel)
hA = hA[:, 1:] - hA[:, :-1]
weights[sel] = hA / np.sum(hA, axis=-1)[:, None]
del hA, hr, Rsel, Dsel, R1, R2
# evaluate wake model:
wdeltas, st_sel = wmodel.calc_wakes_x_r(
algo, mdata, fdata, tdata, downwind_index, x, r
)
for v, wdel in wdeltas.items():
d = np.einsum("sn,sn->s", wdel, weights[st_sel])
try:
superp = wmodel.superp[v]
except KeyError:
raise KeyError(
f"Model '{self.name}': Missing wake superposition entry for variable '{v}' in wake model '{wmodel.name}', found {sorted(list(wmodel.superp.keys()))}"
)
wake_deltas[v] = superp.add_wake(
algo,
mdata,
fdata,
tdata,
downwind_index,
st_sel,
v,
wake_deltas[v],
d[:, None],
)