Skip to content
Snippets Groups Projects
engineering_models.py 25.9 KiB
Newer Older
from abc import abstractmethod
from numpy import newaxis as na
import numpy as np
from py_wake.deficit_models import DeficitModel
from py_wake.superposition_models import SuperpositionModel
from py_wake.turbulence_models.turbulence_model import TurbulenceModel
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.gradients import use_autograd_in, autograd


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
    - WS_eff_ilk: Local wind speed with wake effects
    - 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
    - ct_ilk: Thrust coefficient

    """
    default_grid_resolution = 500

    def __init__(self, site, windTurbines, wake_deficitModel, superpositionModel,
                 blockage_deficitModel=None, deflectionModel=None, turbulenceModel=None):

        WindFarmModel.__init__(self, site, windTurbines)

        assert isinstance(wake_deficitModel, DeficitModel)
        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)
        self.site = site
        self.windTurbines = windTurbines
        self.wake_deficitModel = wake_deficitModel
        self.superpositionModel = superpositionModel
        self.blockage_deficitModel = blockage_deficitModel
        self.deflectionModel = deflectionModel
        self.turbulenceModel = turbulenceModel

        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
        if self.blockage_deficitModel:
            self.args4deficit = set(self.args4deficit) | set(self.blockage_deficitModel.args4deficit)

    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-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.wake_deficitModel._calc_layout_terms(**kwargs)
        self.wake_deficitModel.deficit_initalized = True
        if self.blockage_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

    def _calc_deficit(self, dw_ijlk, **kwargs):
        """Calculate wake (and blockage) deficit"""
        deficit = self.wake_deficitModel.calc_deficit(dw_ijlk=dw_ijlk, **kwargs)

        # the split line between wake and blockage is set slightly downstream to handle
        # numerical inaccuracy in the trigonometric functions that calculates dw_ijlk
        rotor_pos = 1e-10
            # downstream wake deficit + upstream blockage
            deficit = ((dw_ijlk > rotor_pos) * deficit +
                       (dw_ijlk <= rotor_pos) * self.blockage_deficitModel.calc_deficit(dw_ijlk=dw_ijlk, **kwargs))
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, yaw_ilk=None):
        """See WindFarmModel.calc_wt_interaction"""
        type_i, h_i, D_i = self.windTurbines.get_defaults(len(x_i), type_i, h_i)
        wd, ws = self.site.get_defaults(wd, ws)

        # Find local wind speed, wind direction, turbulence intensity and probability
        lw = self.site.local_wind(x_i=x_i, y_i=y_i, h_i=h_i, wd=wd, ws=ws)

        # Calculate down-wind and cross-wind distances
        dw_iil, hcw_iil, dh_iil, dw_order_indices_dl = self.site.wt2wt_distances(x_i, y_i, h_i, lw.WD_ilk.mean(2))
        self._validate_input(dw_iil, hcw_iil)

        I, L = dw_iil.shape[1:]
        K = lw.WS_ilk.shape[2]
        WS_eff_ilk = lw.WS_ilk.copy()
        TI_eff_ilk = lw.TI_ilk.copy()
        if yaw_ilk is None:
            yaw_ilk = np.zeros((I, L, K))
        else:
            yaw_ilk = np.deg2rad(yaw_ilk)

        if self.wec != 1:
            hcw_iil = hcw_iil / self.wec

        # add eps to avoid non-differentiable 0

        cw_iil = np.sqrt(hcw_iil**2 + dh_iil**2)

        kwargs = {'localWind': lw,
                  'WS_eff_ilk': WS_eff_ilk, 'TI_eff_ilk': TI_eff_ilk,
                  'type_i': type_i, 'h_i': h_i, 'D_i': D_i, 'yaw_ilk': yaw_ilk,
                  'dw_iil': dw_iil, 'hcw_iil': hcw_iil, 'cw_iil': cw_iil, 'dh_iil': dh_iil,
                  'dw_order_indices_dl': dw_order_indices_dl, 'I': I, 'L': L, 'K': K}
        WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk = self._calc_wt_interaction(**kwargs)
        return WS_eff_ilk, TI_eff_ilk, power_ilk, ct_ilk, lw

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

    def _flow_map(self, x_j, y_j, h_j,
                  wt_x_i, wt_y_i, wt_h_i, wt_type_i, yaw_ilk,
                  WD_ilk, WS_ilk, TI_ilk, WS_eff_ilk, TI_eff_ilk, ct_ilk,
                  wd, ws):
        """call this function via SimulationResult.flow_map"""
        # calculate distances
        wt_d_i = self.windTurbines.diameter(wt_type_i)

        lw_j = self.site.local_wind(x_i=x_j, y_i=y_j, h_i=h_j, wd=wd, ws=ws)
        if len(wt_x_i) == 0:
            # If not turbines just return local wind
            return lw_j, lw_j.WS_ilk, lw_j.TI_ilk

        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))

        if self.deflectionModel:
            if yaw_ilk is None:
                yaw_ilk = np.zeros((I, L, K))
            yaw_ilk = np.deg2rad(yaw_ilk)
        if L > 1:
            print("|" + "-" * 100 + "|\n|", end="", flush=True)
        for l in range(L):
            if L > 1 and (l * 100) // L > ((l - 1) * 100) // L:
                print(".", end="", flush=True)
            dw_ijl, hcw_ijl, dh_ijl, _ = self.site.distances(wt_x_i, wt_y_i, wt_h_i, x_j, y_j, h_j,
                                                             wd_il=WD_ilk[:, l:l + 1, :].mean(2))

            if self.wec != 1:
                hcw_ijl = hcw_ijl / self.wec

            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))
                for i in range(I):
                    arg_funcs = {'WS_ilk': lambda: WS_ilk[i, l][na, na],
                                 'WS_eff_ilk': lambda: WS_eff_ilk[i, l][na, na],
                                 'TI_ilk': lambda: TI_ilk[i, l][na, na],
                                 'TI_eff_ilk': lambda: TI_eff_ilk[i, l][na, na],
                                 'yaw_ilk': lambda: yaw_ilk[i, l][na, na],
                                 'D_src_il': lambda: wt_d_i[i][na, na],
                                 'D_dst_ijl': lambda: None,
                                 'dh_ijl': lambda: dh_ijl[i][na],
                                 'h_il': lambda: wt_h_i[i][na, na],
                                 'ct_ilk': lambda: ct_ilk[i, l][na, na]}

                    if self.deflectionModel:
                        dw_ijlk, hcw_ijlk = self.deflectionModel.calc_deflection(
                            dw_ijl[i][na], hcw_ijl[i][na],
                            **{k: arg_funcs[k]() for k in self.deflectionModel.args4deflection})
                    else:
                        dw_ijlk, hcw_ijlk = dw_ijl[i][na, :, :, na], hcw_ijl[i][na, :, :, na]
                    arg_funcs['cw_ijlk'] = lambda: np.hypot(dh_ijl[i][na, :, :, na], hcw_ijlk)
                    arg_funcs['hcw_ijlk'] = lambda: hcw_ijlk

                    args = {k: arg_funcs[k]() for k in self.args4deficit if k != 'dw_ijlk'}
                    deficit_ijk[i] = self._calc_deficit(dw_ijlk=dw_ijlk, **args)[0, :, 0]
                    if self.turbulenceModel:
                        arg_funcs['wake_radius_ijlk'] = lambda: self.wake_deficitModel.wake_radius(
                            dw_ijlk=dw_ijlk, **args)
                        args.update({k: arg_funcs[k]() for k in self.turbulenceModel.args4addturb
                                     if k not in self.args4deficit})
                        add_turb_ijk[i] = self.turbulenceModel.calc_added_turbulence(dw_ijlk=dw_ijlk, **args)[0, :, 0]

            else:

                arg_funcs = {'WS_ilk': lambda: WS_ilk[:, l][:, na],
                             'WS_eff_ilk': lambda: WS_eff_ilk[:, l][:, na],
                             'TI_ilk': lambda: TI_ilk[:, l][:, na],
                             'TI_eff_ilk': lambda: TI_eff_ilk[:, l][:, na],
                             'yaw_ilk': lambda: yaw_ilk[:, l][:, na],
                             'D_src_il': lambda: wt_d_i[:, na],
                             'D_dst_ijl': lambda: None,
                             'dh_ijl': lambda: dh_ijl,
                             'h_il': lambda: wt_h_i[:, na],
                             'ct_ilk': lambda: ct_ilk[:, l][:, na]}

                if self.deflectionModel:
                    dw_ijlk, hcw_ijlk = self.deflectionModel.calc_deflection(
                        dw_ijl, hcw_ijl,
                        **{k: arg_funcs[k]() for k in self.deflectionModel.args4deflection})
                else:
                    dw_ijlk, hcw_ijlk = dw_ijl[..., na], hcw_ijl[..., na]
                arg_funcs['cw_ijlk'] = lambda: np.hypot(dh_ijl[..., na], hcw_ijlk)
                arg_funcs['dw_ijlk'] = lambda: dw_ijlk
                arg_funcs['hcw_ijlk'] = lambda: hcw_ijlk

                args = {k: arg_funcs[k]() for k in self.args4deficit if k != 'dw_ijlk'}
                deficit_ijk = self._calc_deficit(dw_ijlk=dw_ijlk, **args)[:, :, 0]
                    arg_funcs['wake_radius_ijlk'] = lambda: self.wake_deficitModel.wake_radius(
                        dw_ijlk=dw_ijlk, **args)
                    args.update({k: arg_funcs[k]() for k in self.turbulenceModel.args4addturb
                                 if k not in self.args4deficit})
                    add_turb_ijk = self.turbulenceModel.calc_added_turbulence(dw_ijlk=dw_ijlk, **args)[:, :, 0]
            WS_eff_jlk[:, l] = self.superpositionModel.calc_effective_WS(lw_j.WS_ilk[:, l], deficit_ijk)
            if self.turbulenceModel:
                TI_eff_jlk[:, l] = self.turbulenceModel.calc_effective_TI(lw_j.TI_ilk[:, l], add_turb_ijk)
        return lw_j, WS_eff_jlk, TI_eff_jlk

    def _validate_input(self, dw_iil, hcw_iil):
        I_ = dw_iil.shape[0]
        i1, i2, _ = np.where((np.abs(dw_iil) + np.abs(hcw_iil) + np.eye(I_)[:, :, na]) == 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.__call__(x, y, h, type, wd, ws, yaw_ilk).aep()
            else:
                return self.__call__(x, y, h, type, wd, ws, yaw_ilk).aep()
        return gradient_method(aep, argnum)

    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():
                        sr = self.__call__(x, y, h, type, wd, ws, yaw_ilk)
                        return sr.aep(normalize_probabilities=normalize_probabilities, with_wake_loss=with_wake_loss)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                else:
                    return self.__call__(x, y, h, type, wd, ws, yaw_ilk).aep(
                        normalize_probabilities=normalize_probabilities, with_wake_loss=with_wake_loss)
            return (gradient_method(aep, 0, **gradient_method_kwargs)(x, y, h, type, wd, ws, yaw_ilk),
                    gradient_method(aep, 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, superpositionModel,
                 deflectionModel=None, turbulenceModel=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
        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, superpositionModel,
                                          blockage_deficitModel=None, deflectionModel=deflectionModel, turbulenceModel=turbulenceModel)

    def _calc_wt_interaction(self, localWind,
                             WS_eff_ilk, TI_eff_ilk,
                             type_i, h_i, D_i, yaw_ilk,
                             dw_iil, hcw_iil, cw_iil, dh_iil, dw_order_indices_dl, I, L, K):
        """
        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 = []

        def ilk2mk(x_ilk):
            return x_ilk.astype(np.float).reshape((I * L, K))

        indices = np.arange(I * I * L).reshape((I, I, L))
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 = []
Mads M. Pedersen's avatar
Mads M. Pedersen committed
        power_jlk = []
        ct_jlk = []

Mads M. Pedersen's avatar
Mads M. Pedersen committed
        if not self.deflectionModel:
            dw_ijlk, hcw_ijlk = dw_iil[..., na], hcw_iil[..., na]

        if self.turbulenceModel:
            add_turb_nk = np.zeros((I * I * L, K))
            TI_eff_mk = ilk2mk(TI_eff_ilk)

        dw_n, hcw_n, cw_n, dh_n = [a.flatten() for a in [dw_iil, hcw_iil, cw_iil, dh_iil]]

        i_wd_l = np.arange(L)

        # Iterate over turbines in down wind order
        for j in range(I):
            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)
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                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)
                WS_eff_mk.append(WS_eff_lk)
                if self.turbulenceModel:
                    TI_eff_mk[m] = self.turbulenceModel.calc_effective_TI(TI_mk[m], add_turb_nk[n_uw])

            ct_lk, power_lk = self.windTurbines._ct_power(WS_eff_lk, type_i[i_wt_l])

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],
                             'TI_ilk': lambda: TI_mk[m][na],
                             'TI_eff_ilk': lambda: TI_eff_mk[m][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],
                             'dh_ijl': lambda: dh_n[n_dw][na],
                             'h_il': lambda: h_i[i_wt_l][na],
                             'ct_ilk': lambda: ct_lk[na],
                             'wake_radius_ijlk': lambda: wake_radius_ijlk
                             }

                if self.deflectionModel:
                    dw_ijlk, hcw_ijlk = self.deflectionModel.calc_deflection(
                        dw_ijl=dw_n[n_dw][na], hcw_ijl=hcw_n[n_dw][na],
                        **{k: arg_funcs[k]() for k in self.deflectionModel.args4deflection})

                else:
                    dw_ijlk, hcw_ijlk = dw_n[n_dw][na, :, :, na], hcw_n[n_dw][na, :, :, na],

                arg_funcs['hcw_ijlk'] = lambda: hcw_ijlk
Mads M. Pedersen's avatar
Mads M. Pedersen committed
                # sqrt(a**2+b**2) as hypot does not support complex numbers
                arg_funcs['cw_ijlk'] = lambda: np.sqrt(dh_n[n_dw][na, :, :, na]**2 + hcw_ijlk**2)
                args = {k: arg_funcs[k]() for k in self.args4deficit if k != "dw_ijlk"}

Mads M. Pedersen's avatar
Mads M. Pedersen committed

                deficit = self.wake_deficitModel.calc_deficit(dw_ijlk=dw_ijlk, **args)[0]
                deficit_nk.append(deficit)

                    if 'wake_radius_ijlk' in self.turbulenceModel.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.turbulenceModel.args4addturb if k != "dw_ijlk"}
                    add_turb_nk[n_dw] = 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))
        if self.turbulenceModel:
            TI_eff_ilk = TI_eff_mk.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, superpositionModel,
                 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
        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, superpositionModel,
                                          blockage_deficitModel=blockage_deficitModel, deflectionModel=deflectionModel, turbulenceModel=turbulenceModel)
        self.convergence_tolerance = convergence_tolerance

    def _calc_wt_interaction(self, localWind,
                             WS_eff_ilk, TI_eff_ilk,
                             type_i, h_i, D_i, yaw_ilk,
                             dw_iil, hcw_iil, cw_iil, dh_iil, dw_order_indices_dl, I, L, K):
        lw = localWind
        power_ilk = np.zeros((I, L, K))
        WS_eff_ilk_last = WS_eff_ilk.copy()

        ct_ilk = self.windTurbines.ct(lw.WS_ilk, type_i)
        D_src_il = D_i[:, na]
        args = {'WS_ilk': lw.WS_ilk,
                'TI_ilk': lw.TI_ilk,
                'TI_eff_ilk': lw.TI_ilk,
                'yaw_ilk': yaw_ilk,
                'D_src_il': D_src_il,
                'D_dst_ijl': D_src_il[na],
                'cw_ijlk': cw_iil[..., na],
                'dh_ijl': dh_iil,
                'h_il': h_i[:, na]
                }

        # Iterate until convergence
        for j in range(I):

            ct_ilk, power_ilk = self.windTurbines._ct_power(WS_eff_ilk, type_i)
            args['ct_ilk'] = ct_ilk
            args['WS_eff_ilk'] = WS_eff_ilk
            if self.deflectionModel:
                dw_ijlk, hcw_ijlk = self.deflectionModel.calc_deflection(dw_ijl=dw_iil, hcw_ijl=dw_iil, **args)
                args['dw_ijlk'] = dw_ijlk
                args['hcw_ijlk'] = hcw_ijlk
                args['cw_ijlk'] = np.hypot(dh_iil[..., na], hcw_ijlk)
            else:
                args['dw_ijlk'] = dw_iil[..., na]
                args['hcw_ijlk'] = hcw_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)

            # Calculate deficit
            deficit_iilk = self._calc_deficit(**args)

            # Calculate effective wind speed
            WS_eff_ilk = self.superpositionModel.calc_effective_WS(lw.WS_ilk, deficit_iilk)
            if self.turbulenceModel:
                add_turb_ijlk = 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

        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())

        for wm in [noj, noj_ss]:
            plt.figure()
            wm(x=x, y=y, wd=[30], ws=[9]).flow_map().plot_wake_map()
        plt.show()


main()