Skip to content
Snippets Groups Projects
engineering_models.py 36 KiB
Newer Older
from abc import abstractmethod
from numpy import newaxis as na
import numpy as np
from py_wake.deficit_models import DeficitModel
Mads M. Pedersen's avatar
Mads M. Pedersen committed
from py_wake.superposition_models import SuperpositionModel, LinearSum, WeightedSum
from py_wake.wind_farm_models.wind_farm_model import WindFarmModel
from py_wake.deflection_models.deflection_model import DeflectionModel
Mads M. Pedersen's avatar
Mads M. Pedersen committed
from py_wake.utils.gradients import use_autograd_in, autograd
from py_wake.rotor_avg_models.rotor_avg_model import RotorCenter, RotorAvgModel
Mads M. Pedersen's avatar
Mads M. Pedersen committed
from py_wake.turbulence_models.turbulence_model import TurbulenceModel
from py_wake.deficit_models.deficit_model import ConvectionDeficitModel
from py_wake.ground_models.ground_models import NoGround, GroundModel


class EngineeringWindFarmModel(WindFarmModel):
    """
    Base class for engineering wake models

    General suffixes:

    - i: turbines ordered by id
    - j: downstream points/turbines
    - k: wind speeds
    - l: wind directions

    Arguments available for calc_deficit (specifiy in args4deficit):

    - WS_ilk: Local wind speed without wake effects
    - TI_ilk: Local turbulence intensity without wake effects
    - TI_std_ilk: Standard deviation of local turbulence intensity
    - TI_eff_ilk: Local turbulence intensity with wake effects
    - D_src_il: Diameter of source turbine
    - D_dst_ijl: Diameter of destination turbine
Mads M. Pedersen's avatar
Mads M. Pedersen committed
    - dw_ijlk: Downwind distance from turbine i to point/turbine j
    - hcw_ijlk: Horizontal cross wind distance from turbine i to point/turbine j
    - dh_ijl: vertical distance from turbine i to point/turbine j
    - cw_ijlk: Cross wind(horizontal and vertical) distance from turbine i to point/turbine j
    def __init__(self, site, windTurbines, wake_deficitModel, rotorAvgModel, superpositionModel,
                 blockage_deficitModel=None, deflectionModel=None, turbulenceModel=None,
                 groundModel=None):

        WindFarmModel.__init__(self, site, windTurbines)

        assert isinstance(wake_deficitModel, DeficitModel)
        assert isinstance(rotorAvgModel, RotorAvgModel)
        assert isinstance(superpositionModel, SuperpositionModel)
        assert blockage_deficitModel is None or isinstance(blockage_deficitModel, DeficitModel)
        assert deflectionModel is None or isinstance(deflectionModel, DeflectionModel)
        assert turbulenceModel is None or isinstance(turbulenceModel, TurbulenceModel)
        if groundModel is None:
            groundModel = NoGround()
        assert isinstance(groundModel, GroundModel)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        if isinstance(superpositionModel, WeightedSum):
            assert isinstance(wake_deficitModel, ConvectionDeficitModel)
            assert rotorAvgModel.__class__ is RotorCenter, "Multiple rotor average points not implemented for WeightedSum"
        assert 'TI_eff_ilk' not in wake_deficitModel.args4deficit or turbulenceModel  # TI_eff requires a turbulence model
        self.rotorAvgModel = rotorAvgModel

        self.superpositionModel = superpositionModel
        self.blockage_deficitModel = blockage_deficitModel
        self.deflectionModel = deflectionModel
        self.turbulenceModel = turbulenceModel
        self.groundModel = groundModel

        self.wec = 1  # wake expansion continuation (wake-width scale factor) see
        # Thomas, J. J. and Ning, A., “A Method for Reducing Multi-Modality in the Wind Farm Layout Optimization Problem,”
        # Journal of Physics: Conference Series, Vol. 1037, The Science of Making
        # Torque from Wind, Milano, Italy, jun 2018, p. 10.
        self.deficit_initalized = False

        self.args4deficit = self.wake_deficitModel.args4deficit
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        self.args4deficit = set(self.args4deficit) | {'yaw_ilk'} | set(self.rotorAvgModel.args4rotor_avg_deficit)
        if self.blockage_deficitModel:
            self.args4deficit = set(self.args4deficit) | set(self.blockage_deficitModel.args4deficit)
        if self.groundModel:
            self.args4deficit = set(self.args4deficit) | set(self.groundModel.args4deficit)
        self.args4all = set(self.args4deficit)
        if self.turbulenceModel:
            if self.turbulenceModel.rotorAvgModel is None:
                self.turbulenceModel.rotorAvgModel = rotorAvgModel
            self.args4addturb = set(self.turbulenceModel.args4addturb) | set(
                self.turbulenceModel.rotorAvgModel.args4rotor_avg_deficit)
            self.args4all = self.args4all | set(self.turbulenceModel.args4addturb)
        if self.deflectionModel:
            self.args4all = self.args4all | set(self.deflectionModel.args4deflection)

    def __str__(self):
        def name(o):
            return o.__class__.__name__

        models = [self.__class__.__bases__[0].__name__,
                  "%s-wake" % name(self.wake_deficitModel)]
        if self.blockage_deficitModel:
            models.append("%s-blockage" % name(self.blockage_deficitModel))
        models.append("%s-rotor-average" % (name(self.rotorAvgModel)))
        models.append("%s-superposition" % (name(self.superpositionModel)))
        if self.deflectionModel:
            models.append("%s-deflection" % name(self.deflectionModel))
        if self.turbulenceModel:
            models.append("%s-turbulence" % name(self.turbulenceModel))
        return "%s(%s)" % (name(self), ", ".join(models))

    def _init_deficit(self, **kwargs):
        """Calculate layout dependent wake (and blockage) deficit terms"""
        self.rotorAvgModel._calc_layout_terms(self.wake_deficitModel, **kwargs)
        self.wake_deficitModel.deficit_initalized = True
        if self.blockage_deficitModel:
            if self.blockage_deficitModel != self.wake_deficitModel:
                self.blockage_deficitModel._calc_layout_terms(**kwargs)
            self.blockage_deficitModel.deficit_initalized = True

    def _reset_deficit(self):
        self.wake_deficitModel.deficit_initalized = False
        if self.blockage_deficitModel:
            self.blockage_deficitModel.deficit_initalized = False

Mads M. Pedersen's avatar
Mads M. Pedersen committed
    def _add_blockage(self, deficit, dw_ijlk, **kwargs):
        # the split line between wake and blockage is set slightly upstream to handle
        # numerical inaccuracy in the trigonometric functions that calculates dw_ijlk
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        rotor_pos = -1e-10
Alex's avatar
Alex committed
        blockage = np.zeros_like(deficit)
Alex's avatar
Alex committed
        elif (self.blockage_deficitModel != self.wake_deficitModel):
            blockage = self.groundModel(lambda **kwargs: self.rotorAvgModel(self.blockage_deficitModel.calc_blockage_deficit, **kwargs),
                                        dw_ijlk=dw_ijlk, **kwargs)
            deficit *= (dw_ijlk > rotor_pos)
        return deficit, blockage
Mads M. Pedersen's avatar
Mads M. Pedersen committed
    def _calc_deficit(self, dw_ijlk, **kwargs):
        """Calculate wake (and blockage) deficit"""
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        deficit = self.groundModel(lambda **kwargs: self.rotorAvgModel(self.wake_deficitModel.calc_deficit_downwind, **kwargs),
                                   dw_ijlk=dw_ijlk, **kwargs)
Alex's avatar
Alex committed
        deficit, blockage = self._add_blockage(deficit, dw_ijlk, **kwargs)
        return deficit + blockage
Mads M. Pedersen's avatar
Mads M. Pedersen committed

    def _calc_deficit_convection(self, dw_ijlk, **kwargs):
        """Calculate wake convection deficit (and blockage)"""
        deficit, uc, sigma_sqr = self.rotorAvgModel.calc_deficit_convection(
            self.wake_deficitModel, dw_ijlk=dw_ijlk, **kwargs)
Alex's avatar
Alex committed
        deficit, blockage = self._add_blockage(deficit, dw_ijlk, **kwargs)
        return deficit, uc, sigma_sqr, blockage
Mads M. Pedersen's avatar
Mads M. Pedersen committed
    def calc_wt_interaction(self, x_i, y_i, h_i=None, type_i=0, wd=None, ws=None, time=False, yaw_ilk=None, **kwargs):
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        h_i, D_i = self.windTurbines.get_defaults(len(x_i), type_i, h_i)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        x_i, y_i, type_i = [np.asarray(v) for v in [x_i, y_i, type_i]]
        wd, ws = self.site.get_defaults(wd, ws)

        # Find local wind speed, wind direction, turbulence intensity and probability
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        lw = self.site.local_wind(x_i=x_i, y_i=y_i, h_i=h_i, wd=wd, ws=ws, time=time)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        self._validate_input(x_i, y_i)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        I, L, K, = len(x_i), len(wd), (1, len(ws))[time is False]
        WS_eff_ilk = lw.WS.ilk((I, L, K)).copy()
        TI_eff_ilk = lw.TI.ilk((I, L, K)).copy()
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        if yaw_ilk is not None:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            yaw_ilk = np.zeros((I, L, K)) + np.deg2rad(yaw_ilk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
#        eps = 2 * np.finfo(float).eps ** 2 if 'autograd' in np.__name__ else 0

        self.site.distance.setup(x_i, y_i, h_i)
        # cw_iil = np.sqrt(hcw_iil**2 + dh_iil**2 + eps)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        wt_kwargs = kwargs
        ri, oi = self.windTurbines.function_inputs
        unused_inputs = set(wt_kwargs) - set(ri) - set(oi) - {'TI'}
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        if unused_inputs:
            raise TypeError("""got unexpected keyword argument(s): '%s'
            required arguments: %s
            optional arguments: %s""" % ("', '".join(unused_inputs), ['ws'] + ri, oi))
Mads M. Pedersen's avatar
Mads M. Pedersen committed

        def add_arg(name, optional):
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            if name in wt_kwargs:  # custom WindFarmModel.__call__ arguments
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                return
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            elif name in {'yaw', 'type'}:  # fixed WindFarmModel.__call__ arguments
                wt_kwargs[name] = {'yaw': yaw_ilk, 'type': type_i}[name]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            elif name in lw:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                wt_kwargs[name] = lw[name]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            elif name in self.site.ds:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                wt_kwargs[name] = self.site.interp(self.site.ds[name], lw.coords).values
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            elif name in ['TI_eff']:
                if self.turbulenceModel:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    wt_kwargs['TI_eff'] = None
                elif optional is False:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    raise KeyError("Argument, TI_eff, needed to calculate power and ct requires a TurbulenceModel")
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            elif name in ['dw_ijl', 'cw_ijl', 'hcw_ijl']:
                pass
            elif optional:
                pass
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            else:
                raise KeyError("Argument, %s, required to calculate power and ct not found" % name)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        for opt, lst in zip([False, True], self.windTurbines.function_inputs):
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            for k in lst:
                add_arg(k, opt)

        if yaw_ilk is None:
            yaw_ilk = np.zeros((I, L, K))

        kwargs = {'localWind': lw,
                  'WS_eff_ilk': WS_eff_ilk, 'TI_eff_ilk': TI_eff_ilk,
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                  'x_i': x_i, 'y_i': y_i, 'h_i': h_i, 'D_i': D_i, 'yaw_ilk': yaw_ilk,
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                  'I': I, 'L': L, 'K': K, **wt_kwargs}
        WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk = self._calc_wt_interaction(**kwargs)
        if 'TI_eff' in wt_kwargs:
            wt_kwargs['TI_eff'] = TI_eff_ilk
        d_ijl_keys = ({k for l in self.windTurbines.function_inputs for k in l} &
                      {'dw_ijl', 'hcw_ijl', 'dh_ijl', 'cw_ijl'})
        if d_ijl_keys:
            d_ijl_dict = {k: lambda v=v: v for k, v in zip(['dw_ijl', 'hcw_ijl', 'dh_ijl'], self.site.distance(wd[na]))}
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            d_ijl_dict['cw_ijl'] = lambda d_ijl_dict=d_ijl_dict: np.sqrt(
                d_ijl_dict['dw_ijl']**2 + d_ijl_dict['hcw_ijl']**2)
            wt_kwargs.update({k: d_ijl_dict[k]() for k in d_ijl_keys})

        return WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk, lw, wt_kwargs

    @abstractmethod
    def _calc_wt_interaction(self, **kwargs):
        """calculate WT interaction"""

    def _flow_map(self, x_j, y_j, h_j, sim_res_data):
        """call this function via SimulationResult.flow_map"""
        # calculate distances
        wt_d_i = self.windTurbines.diameter(sim_res_data.type)
        wt_x_i, wt_y_i, wt_h_i, wd, ws = [sim_res_data[k] for k in ['x', 'y', 'h', 'wd', 'ws']]

        lw_j = self.site.local_wind(x_i=x_j, y_i=y_j, h_i=h_j, wd=wd, ws=ws)
        I, J, L, K = [len(x) for x in [wt_x_i, x_j, wd, ws]]
        WS_eff_jlk = np.zeros((len(x_j), L, K))
        TI_eff_jlk = np.zeros((len(x_j), L, K))

Mads M. Pedersen's avatar
Mads M. Pedersen committed
        self.site.distance.setup(wt_x_i, wt_y_i, wt_h_i, (x_j, y_j, h_j))

        for l in tqdm(range(L), disable=L <= 1 or not self.verbose, desc='Calculate flow map', unit='wd'):
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            dw_ijl, hcw_ijl, dh_ijl = self.site.distance(wd_il=sim_res_data.WD.ilk((I, L, K))[:, l:l + 1, :].mean(2))
            def get_ilk(k):
                def wrap():
                    v = sim_res_data[k].ilk((I, L, K))
                    l_ = [l, 0][v.shape[1] == 1]
                    return v[:, l_][:, na]
                return wrap
            arg_funcs = {'WS_ilk': get_ilk('WS'),
                         'WS_eff_ilk': get_ilk('WS_eff'),
                         'TI_ilk': get_ilk('TI'),
                         'TI_eff_ilk': get_ilk('TI_eff'),
                         'yaw_ilk': lambda: np.deg2rad(get_ilk('Yaw')()),
                         'D_src_il': lambda: wt_d_i[:, na],
                         'D_dst_ijl': lambda: np.zeros_like(dh_ijl),
                         'h_il': lambda: wt_h_i.data[:, na],
                         'ct_ilk': get_ilk('CT')}
            if self.deflectionModel:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                dw_ijlk, hcw_ijlk, dh_ijlk = self.deflectionModel.calc_deflection(
                    dw_ijl=dw_ijl, hcw_ijl=hcw_ijl, dh_ijl=dh_ijl,
                    ** {k: arg_funcs[k]() for k in self.deflectionModel.args4deflection})
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                dw_ijlk, hcw_ijlk, dh_ijlk = dw_ijl[..., na], hcw_ijl[..., na], dh_ijl[..., na]
            arg_funcs.update({'cw_ijlk': lambda: np.hypot(dh_ijl[..., na], hcw_ijlk),
                              'dw_ijlk': lambda: dw_ijlk, 'hcw_ijlk': lambda: hcw_ijlk, 'dh_ijlk': lambda: dh_ijlk})
            args = {k: arg_funcs[k]() for k in self.args4deficit if k != 'dw_ijlk'}
            arg_funcs['wake_radius_ijlk'] = lambda: self.wake_deficitModel.wake_radius(dw_ijlk=dw_ijlk, **args)
            if self.turbulenceModel:
                args.update({k: arg_funcs[k]() for k in self.turbulenceModel.args4addturb
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                             if k not in self.args4deficit and k != 'dw_ijlk'})
            if I * J * K * 8 / 1024**2 > 10:
                # one wt at the time to avoid memory problems
                deficit_ijk = np.zeros((I, J, K))
                add_turb_ijk = np.zeros((I, J, K))
                uc_ijk = np.zeros((I, J, K))
                sigma_sqr_ijk = np.zeros((I, J, K))
                for i in tqdm(range(I), disable=I <= 1 or not self.verbose,
                              desc="Calculate flow map for wd=%d" % l, unit='wt'):
                    args_i = {k: v[i][na] for k, v in args.items()}
                    if isinstance(self.superpositionModel, WeightedSum):
Alex's avatar
Alex committed
                        deficit, uc, sigma_sqr, blockage = self._calc_deficit_convection(
                            dw_ijlk=dw_ijlk[i][na], **args_i)
                        deficit_ijk[i] = deficit[0, :, 0]
                        uc_ijk[i] = uc[0, :, 0]
                        sigma_sqr_ijk[i] = sigma_sqr[0, :, 0]
                    else:
                        deficit_ijk[i] = self._calc_deficit(dw_ijlk=dw_ijlk[i][na], **args_i)[0, :, 0]

                        add_turb_ijk[i] = self.turbulenceModel.calc_added_turbulence(
                            dw_ijlk=dw_ijlk[i][na], **args_i)[0, :, 0]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                if isinstance(self.superpositionModel, WeightedSum):
Alex's avatar
Alex committed
                    deficit, uc, sigma_sqr, blockage = self._calc_deficit_convection(dw_ijlk=dw_ijlk, **args)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    deficit_ijk = deficit[:, :, 0]
                    uc_ijk = uc[:, :, 0]
                    sigma_sqr_ijk = sigma_sqr[:, :, 0]
                else:
                    deficit_ijk = self._calc_deficit(dw_ijlk=dw_ijlk, **args)[:, :, 0]
                    add_turb_ijk = self.turbulenceModel.calc_added_turbulence(dw_ijlk=dw_ijlk, **args)[:, :, 0]
            l_ = [l, 0][lw_j.WS_ilk.shape[1] == 1]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            if isinstance(self.superpositionModel, WeightedSum):
                cw_ijk = np.hypot(dh_ijl[..., na], hcw_ijlk)[:, :, 0]
                hcw_ijk, dh_ijk = hcw_ijlk[:, :, 0], dh_ijl[:, :, 0, na]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                WS_eff_jlk[:, l] = self.superpositionModel.calc_effective_WS(
                    lw_j.WS_ilk[:, l_], deficit_ijk, uc_ijk, sigma_sqr_ijk, cw_ijk, hcw_ijk, dh_ijk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            else:
                WS_eff_jlk[:, l] = self.superpositionModel.calc_effective_WS(lw_j.WS_ilk[:, l_], deficit_ijk)

                l_ = [l, 0][lw_j.TI_ilk.shape[1] == 1]
                TI_eff_jlk[:, l] = self.turbulenceModel.calc_effective_TI(lw_j.TI_ilk[:, l_], add_turb_ijk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
    def _validate_input(self, x_i, y_i):
        i1, i2 = np.where((np.abs(x_i[:, na] - x_i[na]) + np.abs(y_i[:, na] - y_i[na]) + np.eye(len(x_i))) == 0)
        if len(i1):
            msg = "\n".join(["Turbines %d and %d are at the same position" %
                             (i1[i], i2[i]) for i in range(len(i1))])
            raise ValueError(msg)

Mads M. Pedersen's avatar
Mads M. Pedersen committed
    def dAEPdn(self, argnum, gradient_method):
        def aep(x, y, h=None, type=0, wd=None, ws=None, yaw_ilk=None):  # @ReservedAssignment
            if gradient_method == autograd:
                with use_autograd_in():
                    return self.aep(x, y, h, type, wd, ws, yaw_ilk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            else:
                return self.aep(x, y, h, type, wd, ws, yaw_ilk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        return gradient_method(aep, True, argnum)
Mads M. Pedersen's avatar
Mads M. Pedersen committed

    def dAEPdxy(self, gradient_method, normalize_probabilities=False, with_wake_loss=True, gradient_method_kwargs={}):
Mads M. Pedersen's avatar
Mads M. Pedersen committed

        def wrap(x, y, h=None, type=0, wd=None, ws=None, yaw_ilk=None):  # @ReservedAssignment
            def aep(x, y, h, type, wd, ws, yaw_ilk):  # @ReservedAssignment
                if gradient_method == autograd:
                    with use_autograd_in():
                        return self.aep(x, y, h, type, wd, ws, yaw_ilk,
                                        normalize_probabilities=normalize_probabilities, with_wake_loss=with_wake_loss)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                else:
                    return self.aep(x, y, h, type, wd, ws, yaw_ilk,
                                    normalize_probabilities=normalize_probabilities, with_wake_loss=with_wake_loss)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            return (gradient_method(aep, True, 0, **gradient_method_kwargs)(x, y, h, type, wd, ws, yaw_ilk),
                    gradient_method(aep, True, 1, **gradient_method_kwargs)(x, y, h, type, wd, ws, yaw_ilk))
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        return wrap


class PropagateDownwind(EngineeringWindFarmModel):
    """Downstream wake deficits calculated and propagated in downstream direction.
    Very fast, but ignoring blockage effects
    """

    def __init__(self, site, windTurbines, wake_deficitModel,
                 rotorAvgModel=RotorCenter(), superpositionModel=LinearSum(),
                 deflectionModel=None, turbulenceModel=None,
                 groundModel=None):
        """Initialize flow model

        Parameters
        ----------
        site : Site
            Site object
        windTurbines : WindTurbines
            WindTurbines object representing the wake generating wind turbines
        wake_deficitModel : DeficitModel
            Model describing the wake(downstream) deficit
        rotorAvgModel : RotorAvgModel, optional
            Model defining one or more points at the down stream rotors to
            calculate the rotor average wind speeds from.
            Default is RotorCenter, i.e. one point at rotor center
        superpositionModel : SuperpositionModel
            Model defining how deficits sum up
        deflectionModel : DeflectionModel
            Model describing the deflection of the wake due to yaw misalignment, sheared inflow, etc.
        turbulenceModel : TurbulenceModel
            Model describing the amount of added turbulence in the wake
        """
        EngineeringWindFarmModel.__init__(self, site, windTurbines, wake_deficitModel, rotorAvgModel, superpositionModel,
                                          blockage_deficitModel=None, deflectionModel=deflectionModel,
                                          turbulenceModel=turbulenceModel, groundModel=groundModel)

    def _calc_wt_interaction(self, localWind,
                             WS_eff_ilk, TI_eff_ilk,
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                             x_i, y_i, h_i, D_i, yaw_ilk,
                             I, L, K, **kwargs):
        """
        Additional suffixes:

        - m: turbines and wind directions (il.flatten())
        - n: from_turbines, to_turbines and wind directions (iil.flatten())

        """
        lw = localWind
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        deficit_nk = []
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        uc_nk = []
        sigma_sqr_nk = []
        cw_nk = []
        hcw_nk = []
        dh_nk = []
            return np.broadcast_to(x_ilk.astype(float), (I, L, K)).reshape((I * L, K))
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        TI_mk = ilk2mk(lw.TI_ilk)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        WS_eff_mk = []
        TI_eff_mk = []
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        power_jlk = []
        ct_jlk = []

Mads M. Pedersen's avatar
Mads M. Pedersen committed
        if self.turbulenceModel:
            add_turb_nk = np.zeros((I * I * L, K))

Mads M. Pedersen's avatar
Mads M. Pedersen committed
        wd = lw.WD_ilk.mean((0, 2))
        dw_order_indices_dl = self.site.distance.dw_order_indices(wd)
        for j in tqdm(range(I), disable=I <= 1 or not self.verbose, desc="Calculate flow interaction", unit="wt"):
            i_wt_l = dw_order_indices_dl[:, j]
            m = i_wt_l * L + i_wd_l  # current wt (j'th most upstream wts for all wdirs)

            # generate indexes of up wind(n_uw) and down wind(n_dw) turbines
            n_uw = indices[:, i_wt_l, i_wd_l][dw_order_indices_dl[:, :j].T, np.arange(L)]
            n_dw = indices[i_wt_l, :, i_wd_l][np.arange(L), dw_order_indices_dl[:, j + 1:].T]

            # Calculate effectiv wind speed at current turbines(all wind directions and wind speeds) and
            # look up power and thrust coefficient
            if j == 0:  # Most upstream turbines (no wake)
                WS_eff_lk = WS_mk[m]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                WS_eff_mk.append(WS_eff_lk)
                if self.turbulenceModel:
                    TI_eff_mk.append(TI_mk[m])
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                if isinstance(self.superpositionModel, WeightedSum):
                    deficit2WT = np.array([d_nk2[i] for d_nk2, i in zip(deficit_nk, range(j)[::-1])])
                    uc2WT = np.array([d_nk2[i] for d_nk2, i in zip(uc_nk, range(j)[::-1])])
                    sigmasqr2WT = np.array([d_nk2[i] for d_nk2, i in zip(sigma_sqr_nk, range(j)[::-1])])
                    cw2WT = np.array([d_nk2[i] for d_nk2, i in zip(cw_nk, range(j)[::-1])])
                    hcw2WT = np.array([d_nk2[i] for d_nk2, i in zip(hcw_nk, range(j)[::-1])])
                    dh2WT = np.array([d_nk2[i] for d_nk2, i in zip(dh_nk, range(j)[::-1])])

                    WS_eff_lk = self.superpositionModel.calc_effective_WS(
                        WS_mk[m], deficit2WT, uc2WT, sigmasqr2WT, cw2WT, hcw2WT, dh2WT)
                else:
                    deficit2WT = np.array([d_nk2[i] for d_nk2, i in zip(deficit_nk, range(j)[::-1])])
                    WS_eff_lk = self.superpositionModel.calc_effective_WS(WS_mk[m], deficit2WT)

Mads M. Pedersen's avatar
Mads M. Pedersen committed
                WS_eff_mk.append(WS_eff_lk)
                    TI_eff_mk.append(self.turbulenceModel.calc_effective_TI(TI_mk[m], add_turb_nk[n_uw]))
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            # Calculate Power/CT
                if v is None or isinstance(v, (int, float)) or len(np.asarray(v).shape) == 0:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    return v
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    return v[i_wt_l]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    return v[i_wt_l, i_wd_l]
                elif v.shape == (L,):
                    return v[i_wd_l]
                else:
                    valid_shapes = f"(), ({I}), ({I},{L}), ({I},{L},{K}), ({L}), ({L},{K})"
                    raise ValueError(
                        f"Argument, {k}(shape={v.shape}), has unsupported shape. Valid shapes are {valid_shapes}")
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            keys = self.windTurbines.powerCtFunction.required_inputs + self.windTurbines.powerCtFunction.optional_inputs
            _kwargs = {k: mask(k, v) for k, v in kwargs.items() if k in keys}
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            if 'TI_eff' in _kwargs:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                _kwargs['TI_eff'] = TI_eff_mk[-1]
            power_lk, ct_lk, = self.windTurbines.power_ct(WS_eff_lk, **_kwargs)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            power_jlk.append(power_lk)
            ct_jlk.append(ct_lk)
                # Calculate required args4deficit parameters
                arg_funcs = {'WS_ilk': lambda: WS_mk[m][na],
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                             'WS_eff_ilk': lambda: WS_eff_mk[-1][na],
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                             'TI_eff_ilk': lambda: TI_eff_mk[-1][na],
                             'D_src_il': lambda: D_i[i_wt_l][na],
                             'yaw_ilk': lambda: yaw_mk[m][na],
                             'D_dst_ijl': lambda: D_i[dw_order_indices_dl[:, j + 1:]].T[na],
                             'h_il': lambda: h_i[i_wt_l][na],
                             'ct_ilk': lambda: ct_lk[na],
                             'wake_radius_ijlk': lambda: wake_radius_ijlk
                             }
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                i_dw = dw_order_indices_dl[:, j + 1:]

                dw_jl, hcw_jl, dh_jl = self.site.distance(wd, src_idx=i_wt_l, dst_idx=i_dw.T)
                if self.wec != 1:
                    hcw_jl = hcw_jl / self.wec

Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    dw_ijlk, hcw_ijlk, dh_ijlk = self.deflectionModel.calc_deflection(
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                        dw_ijl=dw_jl[na], hcw_ijl=hcw_jl[na], dh_ijl=dh_jl[na],
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                        ** {k: arg_funcs[k]() for k in self.deflectionModel.args4deflection})
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    dw_ijlk, hcw_ijlk, dh_ijlk = [v[na, :, :, na] for v in [dw_jl, hcw_jl, dh_jl]]
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                # sqrt(a**2+b**2) as hypot does not support complex numbers
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                cw_ijlk = np.sqrt(dh_ijlk**2 + hcw_ijlk**2)

                arg_funcs.update({'hcw_ijlk': lambda: hcw_ijlk, 'cw_ijlk': lambda: cw_ijlk, 'dh_ijlk': lambda: dh_ijlk})
                args = {k: arg_funcs[k]() for k in self.args4deficit if k != "dw_ijlk"}
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                hcw_nk.append(hcw_ijlk[0])
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                dh_nk.append(dh_ijlk[0])
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                cw_nk.append(cw_ijlk[0])
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                if isinstance(self.superpositionModel, WeightedSum):
Alex's avatar
Alex committed
                    deficit, uc, sigma_sqr, blockage = self._calc_deficit_convection(dw_ijlk=dw_ijlk, **args)
                    deficit += blockage
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                    uc_nk.append(uc[0])
                    sigma_sqr_nk.append(sigma_sqr[0])
                else:
                    deficit = self._calc_deficit(dw_ijlk=dw_ijlk, **args)
                deficit_nk.append(deficit[0])
                    if 'wake_radius_ijlk' in self.args4addturb:
                        wake_radius_ijlk = self.wake_deficitModel.wake_radius(dw_ijlk=dw_ijlk, **args)
                        arg_funcs['wake_radius_ijlk'] = lambda: wake_radius_ijlk

                    turb_args = {k: arg_funcs[k]() for k in self.args4addturb
                                 if k != "dw_ijlk"}

                    add_turb_nk[n_dw] = self.turbulenceModel.rotorAvgModel(self.turbulenceModel.calc_added_turbulence,
                                                                           dw_ijlk=dw_ijlk, **turb_args)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        WS_eff_jlk, power_jlk, ct_jlk = np.array(WS_eff_mk), np.array(power_jlk), np.array(ct_jlk)

        dw_inv_indices = (np.argsort(dw_order_indices_dl, 1).T * L + np.arange(L)[na]).flatten()
        WS_eff_ilk = WS_eff_jlk.reshape((I * L, K))[dw_inv_indices].reshape((I, L, K))

        power_ilk = power_jlk.reshape((I * L, K))[dw_inv_indices].reshape((I, L, K))
        ct_ilk = ct_jlk.reshape((I * L, K))[dw_inv_indices].reshape((I, L, K))
            TI_eff_ilk = np.reshape(TI_eff_mk, (I * L, K))[dw_inv_indices].reshape((I, L, K))

        return WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk


class All2AllIterative(EngineeringWindFarmModel):
    """Wake and blockage deficits calculated from all wt to all points of interest (wt/map points).
    The calculations are iteratively repeated until convergence (change of effective wind speed < convergence_tolerance)"""

    def __init__(self, site, windTurbines, wake_deficitModel,
                 rotorAvgModel=RotorCenter(), superpositionModel=LinearSum(),
                 blockage_deficitModel=None, deflectionModel=None, turbulenceModel=None,
                 convergence_tolerance=1e-6):
        """Initialize flow model

        Parameters
        ----------
        site : Site
            Site object
        windTurbines : WindTurbines
            WindTurbines object representing the wake generating wind turbines
        wake_deficitModel : DeficitModel
            Model describing the wake(downstream) deficit
        rotorAvgModel : RotorAvgModel
            Model defining one or more points at the down stream rotors to
            calculate the rotor average wind speeds from.\n
            Defaults to RotorCenter that uses the rotor center wind speed (i.e. one point) only
        superpositionModel : SuperpositionModel
            Model defining how deficits sum up
        blockage_deficitModel : DeficitModel
            Model describing the blockage(upstream) deficit
        deflectionModel : DeflectionModel
            Model describing the deflection of the wake due to yaw misalignment, sheared inflow, etc.
        turbulenceModel : TurbulenceModel
            Model describing the amount of added turbulence in the wake
        convergence_tolerance : float
            maximum accepted change in WS_eff_ilk [m/s]
        """
        EngineeringWindFarmModel.__init__(self, site, windTurbines, wake_deficitModel, rotorAvgModel, superpositionModel,
                                          blockage_deficitModel=blockage_deficitModel, deflectionModel=deflectionModel,
                                          turbulenceModel=turbulenceModel, groundModel=groundModel)
        self.convergence_tolerance = convergence_tolerance

    def _calc_wt_interaction(self, localWind,
                             WS_eff_ilk, TI_eff_ilk,
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                             x_i, y_i, h_i, D_i, yaw_ilk,
                             # dw_iil, hcw_iil, cw_iil, dh_iil, dw_order_indices_dl,
                             I, L, K, **kwargs):
        lw = localWind
        power_ilk = np.zeros((I, L, K))
        WS_eff_ilk_last = WS_eff_ilk.copy()
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        dw_iil, hcw_iil, dh_iil = self.site.distance(lw.WD_ilk.mean(2))
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        ct_ilk = self.windTurbines.ct(lw.WS.ilk((I, L, K)), **kwargs)
        args = {'WS_ilk': lw.WS.ilk((I, L, K)),
                'TI_ilk': lw.TI.ilk((I, L, K)),
                'TI_eff_ilk': lw.TI.ilk((I, L, K)),
                'yaw_ilk': yaw_ilk,
                'D_src_il': D_src_il,
                'D_dst_ijl': D_src_il[na],
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                'cw_ijlk': np.sqrt(hcw_iil**2 + dh_iil**2)[..., na],
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                'dh_ijlk': dh_iil[..., na],
        for j in tqdm(range(I), disable=I <= 1 or not self.verbose, desc="Calculate flow interaction", unit="wt"):
Mads M. Pedersen's avatar
Mads M. Pedersen committed
            power_ilk, ct_ilk = self.windTurbines.power_ct(WS_eff_ilk, **kwargs)
            args['ct_ilk'] = ct_ilk
            args['WS_eff_ilk'] = WS_eff_ilk
            if self.deflectionModel:
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                dw_ijlk, hcw_ijlk, dh_ijlk = self.deflectionModel.calc_deflection(
                    dw_ijl=dw_iil, hcw_ijl=dw_iil, dh_ijl=dh_iil, **args)
                args.update({'dw_ijlk': dw_ijlk, 'hcw_ijlk': hcw_ijlk, 'dh_ijlk': dh_ijlk,
                             'cw_ijlk': np.hypot(dh_iil[..., na], hcw_ijlk)})
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                args.update({'dw_ijlk': dw_iil[..., na], 'hcw_ijlk': hcw_iil[..., na], 'dh_ijlk': dh_iil[..., na]})
                self._init_deficit(**args)
            if self.turbulenceModel:
                args['TI_eff_ilk'] = TI_eff_ilk
                if 'wake_radius_ijlk' in self.turbulenceModel.args4addturb:
                    args['wake_radius_ijlk'] = self.wake_deficitModel.wake_radius(**args)
Alex's avatar
Alex committed
            if isinstance(self.superpositionModel, WeightedSum):
                deficit_iilk, uc_iilk, sigmasqr_iilk, blockage_iilk = self._calc_deficit_convection(**args)
            else:
                deficit_iilk = self._calc_deficit(**args)
Alex's avatar
Alex committed
            if isinstance(self.superpositionModel, WeightedSum):
                WS_eff_ilk = self.superpositionModel.calc_effective_WS(lw.WS_ilk, deficit_iilk,
                                                                       uc_iilk, sigmasqr_iilk,
                                                                       args['cw_ijlk'],
                                                                       args['hcw_ijlk'],
                                                                       dh_iil[..., na])
                # Add blockage as linear effect
                WS_eff_ilk -= np.sum(blockage_iilk, 0)
            else:
                WS_eff_ilk = self.superpositionModel.calc_effective_WS(lw.WS_ilk, deficit_iilk)

                add_turb_ijlk = self.turbulenceModel.rotorAvgModel(self.turbulenceModel.calc_added_turbulence, **args)
                TI_eff_ilk = self.turbulenceModel.calc_effective_TI(lw.TI_ilk, add_turb_ijlk)

            # Check if converged
            diff = np.abs(WS_eff_ilk_last - WS_eff_ilk)
            max_diff = np.max(diff)
            if self.convergence_tolerance and max_diff < self.convergence_tolerance:
                # print("All2AllIterative converge after %d iterations" % j)
                break
            # i_, l_, k_ = list(zip(*np.where(diff == max_diff)))[0]
            # print("Iteration: %d, max diff: %f, WT: %d, WD: %d, WS: %d" % (j, max_diff, i_, l_, WS_ilk[i_, l_, k_]))
            WS_eff_ilk_last = WS_eff_ilk.copy()
        self._reset_deficit()
        return WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk


def main():
    if __name__ == '__main__':
        from py_wake.examples.data.iea37 import IEA37Site, IEA37_WindTurbines
        from py_wake.deficit_models.selfsimilarity import SelfSimilarityDeficit
Alex's avatar
Alex committed
        from py_wake.deficit_models.gaussian import ZongGaussianDeficit
        from py_wake.turbulence_models.stf import STF2017TurbulenceModel
        from py_wake.flow_map import XYGrid
        import matplotlib.pyplot as plt

        site = IEA37Site(16)
        x, y = site.initial_position.T

        windTurbines = IEA37_WindTurbines()
        from py_wake.deficit_models.noj import NOJDeficit
        from py_wake.superposition_models import SquaredSum

        # NOJ wake model
        noj = PropagateDownwind(site, windTurbines, wake_deficitModel=NOJDeficit(), superpositionModel=SquaredSum())

        # NOJ wake and selfsimilarity blockage
        noj_ss = All2AllIterative(site, windTurbines, wake_deficitModel=NOJDeficit(), superpositionModel=SquaredSum(),
                                  blockage_deficitModel=SelfSimilarityDeficit())

Alex's avatar
Alex committed
        # Zong convection superposition
        zongp_ss = PropagateDownwind(site, windTurbines, wake_deficitModel=ZongGaussianDeficit(), superpositionModel=WeightedSum(),
                                     turbulenceModel=STF2017TurbulenceModel())

        # Zong convection superposition
        zong_ss = All2AllIterative(site, windTurbines, wake_deficitModel=ZongGaussianDeficit(), superpositionModel=WeightedSum(),
                                   blockage_deficitModel=SelfSimilarityDeficit(), turbulenceModel=STF2017TurbulenceModel())

        for wm in [noj, noj_ss, zongp_ss, zong_ss]:
            sim = wm(x=x, y=y, wd=[30], ws=[9])
Alex's avatar
Alex committed
            sim.flow_map(XYGrid(resolution=200)).plot_wake_map()
            plt.title(' AEP: %.3f GWh' % sim.aep().sum())