From 0d875ad2cd17ed40680defade7845ed49b70a9a8 Mon Sep 17 00:00:00 2001
From: Alex <alrf@dtu.dk>
Date: Thu, 8 Jul 2021 11:22:13 +0200
Subject: [PATCH] added RathmannScaled from paper and TurboPark as well as
 tests

codestyle and test crash fix
---
 py_wake/deficit_models/__init__.py            |   2 +-
 py_wake/deficit_models/noj.py                 | 114 ++++++++--
 py_wake/deficit_models/rathmann.py            | 201 +++++++++++++++++-
 .../test_blockage_models.py                   |  11 +-
 .../test_deficit_models.py                    |  10 +-
 py_wake/tests/test_utils/test_model_utils.py  |   5 +-
 py_wake/turbulence_models/stf.py              |   5 +-
 7 files changed, 317 insertions(+), 31 deletions(-)

diff --git a/py_wake/deficit_models/__init__.py b/py_wake/deficit_models/__init__.py
index 68ae1d2cc..7d062657c 100644
--- a/py_wake/deficit_models/__init__.py
+++ b/py_wake/deficit_models/__init__.py
@@ -5,7 +5,7 @@ from .rankinehalfbody import RankineHalfBody
 from .vortexcylinder import VortexCylinder
 from .vortexdipole import VortexDipole
 from .rathmann import Rathmann
-from .noj import NOJDeficit, NOJLocalDeficit
+from .noj import NOJDeficit, NOJLocalDeficit, TurboNOJDeficit
 from .gaussian import BastankhahGaussianDeficit, IEA37SimpleBastankhahGaussianDeficit, \
     NiayifarGaussianDeficit, ZongGaussianDeficit
 from .fuga import FugaDeficit, FugaYawDeficit
diff --git a/py_wake/deficit_models/noj.py b/py_wake/deficit_models/noj.py
index 92516950c..0987b3902 100644
--- a/py_wake/deficit_models/noj.py
+++ b/py_wake/deficit_models/noj.py
@@ -1,7 +1,5 @@
 from numpy import newaxis as na
-
 import numpy as np
-from py_wake.deficit_models import DeficitModel
 from py_wake.superposition_models import SquaredSum, LinearSum
 from py_wake.wind_farm_models.engineering_models import PropagateDownwind
 from py_wake.utils.area_overlapping_factor import AreaOverlappingFactor
@@ -21,10 +19,10 @@ class NOJDeficit(NiayifarGaussianDeficit, AreaOverlappingFactor):
     def _calc_layout_terms(self, WS_ilk, WS_eff_ilk, D_src_il, D_dst_ijl, dw_ijlk, cw_ijlk, **kwargs):
         WS_ref_ilk = (WS_ilk, WS_eff_ilk)[self.use_effective_ws]
         R_src_il = D_src_il / 2
-        wake_radius, k_ijlk = self._wake_radius(D_src_il, dw_ijlk, **kwargs)
-        term_denominator_ijlk = (1 + k_ijlk * dw_ijlk / R_src_il[:, na, :, na])**2
+        wake_radius_ijlk = self.wake_radius(D_src_il, dw_ijlk, **kwargs)
+        term_denominator_ijlk = (wake_radius_ijlk / R_src_il[:, na, :, na])**2
         term_denominator_ijlk += (term_denominator_ijlk == 0)
-        A_ol_factor_ijlk = self.overlapping_area_factor(wake_radius, dw_ijlk, cw_ijlk, D_src_il, D_dst_ijl)
+        A_ol_factor_ijlk = self.overlapping_area_factor(wake_radius_ijlk, dw_ijlk, cw_ijlk, D_src_il, D_dst_ijl)
 
         with np.warnings.catch_warnings():
             np.warnings.filterwarnings('ignore', r'invalid value encountered in true_divide')
@@ -32,18 +30,16 @@ class NOJDeficit(NiayifarGaussianDeficit, AreaOverlappingFactor):
 
     def calc_deficit(self, WS_ilk, WS_eff_ilk, D_src_il, D_dst_ijl, dw_ijlk, cw_ijlk, ct_ilk, **kwargs):
         if not self.deficit_initalized:
+            kwargs['ct_ilk'] = ct_ilk
             self._calc_layout_terms(WS_ilk, WS_eff_ilk, D_src_il, D_dst_ijl, dw_ijlk, cw_ijlk, **kwargs)
         ct_ilk = np.minimum(ct_ilk, 1)   # treat ct_ilk for np.sqrt()
         term_numerator_ilk = (1 - np.sqrt(1 - ct_ilk))
         return term_numerator_ilk[:, na] * self.layout_factor_ijlk
 
-    def _wake_radius(self, D_src_il, dw_ijlk, **kwargs):
+    def wake_radius(self, D_src_il, dw_ijlk, **kwargs):
         k_ijlk = np.atleast_3d(self.k_ilk(kwargs.get('TI_eff_ilk', 0)))[:, na]
         wake_radius_ijlk = (k_ijlk * dw_ijlk + D_src_il[:, na, :, na] / 2)
-        return wake_radius_ijlk, k_ijlk
-
-    def wake_radius(self, D_src_il, dw_ijlk, **kwargs):
-        return self._wake_radius(D_src_il, dw_ijlk, **kwargs)[0]
+        return wake_radius_ijlk
 
     def calc_deficit_convection(self, WS_ilk, WS_eff_ilk, D_src_il, dw_ijlk, cw_ijlk, ct_ilk, **kwargs):
         raise NotImplementedError("calc_deficit_convection not implemented for NOJ")
@@ -129,6 +125,57 @@ class NOJLocal(PropagateDownwind):
                                    turbulenceModel=turbulenceModel)
 
 
+class TurboNOJDeficit(NOJDeficit, AreaOverlappingFactor):
+    """
+    Modified definition of the wake expansion given by Nygaard [1], which
+    assumes the wake expansion rate to be proportional to the local turbulence
+    intensity in the wake. Here the local turbulence intensity is defined as
+    the combination of ambient and wake added turbulence. Using the added
+    wake turbulence model by Frandsen and integrating, an analytical expression
+    for the wake radius can be obtained.
+    The definition in [1] of ambient turbulence is the free-stream TI and for
+    this the model constant A has been tuned, however a fully consistent
+    formulation of the model should probably use the locally effective TI,
+    which includes the added turbulence from upstream turbines.
+    [1] Nygaard 2020 J. Phys.: Conf. Ser. 1618 062072
+        https://doi.org/10.1088/1742-6596/1618/6/062072
+    """
+    args4deficit = ['WS_ilk', 'WS_eff_ilk', 'D_src_il', 'D_dst_ijl',
+                    'dw_ijlk', 'cw_ijlk', 'ct_ilk', 'TI_ilk', 'TI_eff_ilk']
+
+    def __init__(self, A=.6, cTI=[1.5, 0.8], use_effective_ws=False, use_effective_ti=False):
+        self.A = A
+        self.use_effective_ws = use_effective_ws
+        self.use_effective_ti = use_effective_ti
+        self.cTI = cTI
+
+    def wake_radius(self, D_src_il, dw_ijlk, **kwargs):
+        TI_ref_ilk = (kwargs['TI_ilk'], kwargs['TI_eff_ilk'])[
+            self.use_effective_ti]
+        ct_ilk = kwargs['ct_ilk']
+        # constants from Frandsen
+        c1, c2 = self.cTI
+
+        # constants related to ambient turbulence
+        alpha_ilk = c1 * TI_ref_ilk
+        # avoid zero division
+        ct_ilk = np.maximum(ct_ilk, 1e-20)
+        beta_ilk = c2 * TI_ref_ilk / np.sqrt(ct_ilk)
+
+        fac_ilk = self.A * TI_ref_ilk * D_src_il[..., na] / beta_ilk
+        term1_ijlk = np.sqrt(
+            (alpha_ilk[:, na] + beta_ilk[:, na] * dw_ijlk / D_src_il[:, na, :, na])**2 + 1)
+        term2_ilk = np.sqrt(1 + alpha_ilk**2)
+        term3_ijlk = (term1_ijlk + 1) * alpha_ilk[:, na]
+        term4_ijlk = (term2_ilk[:, na] + 1) * (alpha_ilk[:, na] +
+                                               beta_ilk[:, na] * np.abs(dw_ijlk) / D_src_il[:, na, :, na])
+
+        wake_radius_ijlk = 0.5 * (D_src_il[:, na, :, na] + fac_ilk[:, na] * (
+            term1_ijlk - term2_ilk[:, na] - np.log(term3_ijlk / term4_ijlk)))
+
+        return wake_radius_ijlk
+
+
 def main():
     if __name__ == '__main__':
         from py_wake.examples.data.iea37._iea37 import IEA37Site
@@ -143,16 +190,25 @@ def main():
 
         wf_model = NOJ(site, windTurbines)
         wf_model_local = NOJLocal(site, windTurbines, turbulenceModel=STF2017TurbulenceModel())
-
+        wf_model_turbo = PropagateDownwind(site, windTurbines, rotorAvgModel=RotorCenter(),
+                                           wake_deficitModel=TurboNOJDeficit(
+                                               use_effective_ws=True, use_effective_ti=False),
+                                           superpositionModel=LinearSum(),
+                                           turbulenceModel=STF2017TurbulenceModel())
+        # wf_model_turbo = NOJLocal(
+        #     site, windTurbines, turbulenceModel=STF2017TurbulenceModel())
         # run wind farm simulation
         sim_res = wf_model(x, y)
         sim_res_local = wf_model_local(x, y)
+        sim_res_turbo = wf_model_turbo(x, y)
         # calculate AEP
         aep = sim_res.aep().sum()
         aep_local = sim_res_local.aep().sum()
+        aep_turbo = sim_res_turbo.aep().sum()
 
         # plot wake map
-        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4.5), tight_layout=True)
+        fig, (ax1, ax2, ax3) = plt.subplots(
+            1, 3, figsize=(11, 4.5), tight_layout=True)
         levels = np.arange(0, 10.5, 0.5)
         print(wf_model)
         flow_map = sim_res.flow_map(wd=30, ws=9.8)
@@ -167,10 +223,44 @@ def main():
         flow_map.plot_windturbines(ax=ax2)
         ax2.set_title('Local Jensen, AEP: %.2f GWh' % aep_local)
 
+        # plot wake map
+        print(wf_model_turbo)
+        flow_map = sim_res_turbo.flow_map(wd=30, ws=9.8)
+        flow_map.plot_wake_map(levels=levels, ax=ax3, plot_colorbar=False)
+        flow_map.plot_windturbines(ax=ax3)
+        ax3.set_title('Turbo Jensen, AEP: %.2f GWh' % aep_turbo)
+
         plt.figure()
         flow_map.plot_ti_map()
         plt.title('TI map for NOJLocal with STF2017 turbulence model')
         plt.show()
 
+        # plot wake width as in Nygaard 2020
+        D = 1
+        D_src_il = np.array([[D]])
+        x = np.linspace(0, 60, 100)
+        dw_ijlk = x[na, :, na, na]
+
+        noj = NOJDeficit(k=0.04)
+        noj_wr = noj.wake_radius(D_src_il, dw_ijlk)
+
+        ct_ilk = np.array([[[8 / 9]]])  # thrust coefficient
+        TI_ilk = np.array([[[0.06]]])
+        TI_eff_ilk = np.array([[[0.06]]])
+        tj = TurboNOJDeficit()
+        tj_wr = tj.wake_radius(
+            D_src_il, dw_ijlk, ct_ilk=ct_ilk, TI_ilk=TI_ilk, TI_eff_ilk=TI_eff_ilk)
+
+        plt.figure()
+        plt.title(
+            'Wake width comparison, NOJ orig and TurboNOJ (Nygaard2020) TI=6%')
+        plt.plot(x, noj_wr[0, :, 0, 0], label='NOJ, k=0.04')
+        plt.plot(x, tj_wr[0, :, 0, 0], label='TurboNOJ')
+        plt.xlabel('x/D')
+        plt.ylabel('y/D')
+        plt.grid()
+        plt.legend()
+        plt.show()
+
 
 main()
diff --git a/py_wake/deficit_models/rathmann.py b/py_wake/deficit_models/rathmann.py
index 603ce499c..9a6c651d4 100644
--- a/py_wake/deficit_models/rathmann.py
+++ b/py_wake/deficit_models/rathmann.py
@@ -5,7 +5,7 @@ from py_wake.deficit_models import BlockageDeficitModel
 
 class Rathmann(BlockageDeficitModel):
     """
-    Ole Sten Rathmann (DTU) developed in 2020 an approximation to the vortex
+    Ole Sten Rathmann (DTU) developed in 2020 an approximation [1] to the vortex
     cylinder solution (E. Branlard and M. Gaunaa, 2014). In speed it is
     comparable to the vortex dipole method, whilst giving a flow-field
     nearly identical to the vortex cylinder model for x/R < -1. Its centreline
@@ -14,6 +14,10 @@ class Rathmann(BlockageDeficitModel):
     a point upstream. To simulate the speed-up downstream the deficit is mirrored in the
     rotor plane with a sign change.
 
+    References:
+        [1] A Meyer Forsting, OS Rathmann, MP van der Laan, N Troldborg, B Gribben, G Hawkes, E Branlard -
+        Verification of induction zone models for wind farm annual energy production estimation -
+        Journal of Physics: Conference Series 1934 (2021) 012023
     """
 
     args4deficit = ['WS_ilk', 'D_src_il', 'dw_ijlk', 'cw_ijlk', 'ct_ilk']
@@ -96,18 +100,72 @@ class Rathmann(BlockageDeficitModel):
         return deficit_ijlk
 
 
+class RathmannScaled(Rathmann):
+    """
+    Vortex cylinder based models consistently underestimate the induction, due to missing
+    wake expansion [1]. A simple fix alleviating this issue is to simply scale the results
+    with respect to the thrust coefficient as demonstrated in [1]. There is also a small
+    depenancy on the distance fron the rotor.
+
+    References:
+        [1] A Meyer Forsting, OS Rathmann, MP van der Laan, N Troldborg, B Gribben, G Hawkes, E Branlard -
+        Verification of induction zone models for wind farm annual energy production estimation -
+        Journal of Physics: Conference Series 1934 (2021) 012023
+    """
+
+    args4deficit = ['WS_ilk', 'D_src_il', 'dw_ijlk', 'cw_ijlk', 'ct_ilk']
+
+    def __init__(self, sct=1.0, limiter=1e-10, exclude_wake=True):
+        BlockageDeficitModel.__init__(self)
+        # coefficients for BEM approximation by Madsen (1997)
+        self.a0p = np.array([0.2460, 0.0586, 0.0883])
+        # limiter to avoid singularities
+        self.limiter = limiter
+        # coefficient for scaling the effective forcing
+        self.sct = sct
+        # if used in a wind farm simulation, set deficit in wake region to
+        # zero, as here the wake model is active
+        self.exclude_wake = exclude_wake
+        # scaling coefficients for Eq.11-13 in [1]
+        self.sd = np.array([1.02, 0.1554, 0.0005012, 8.45, 0.025])
+
+    def deficit_scaling(self, D_src_il, dw_ijlk, cw_ijlk, ct_ilk):
+        """
+        Scaling function defined in [1], Eq. 11-13 forcing the output closer to the
+        CFD results.
+        """
+        r = np.hypot(cw_ijlk, dw_ijlk) / D_src_il[:, na, :, na]
+        fac = 1. + self.sd[4] * (r - 5.)
+        fac[fac > 1.] = 1.
+        mval = 1. - 4. * self.sd[4]
+        fac[fac < mval] = mval
+        boost_ijlk = self.sd[0] * \
+            np.exp(self.sd[1] * fac * ct_ilk[:, na]) + \
+            self.sd[2] * np.exp(self.sd[3] * fac * ct_ilk[:, na])
+
+        return boost_ijlk
+
+    def calc_deficit(self, WS_ilk, D_src_il, dw_ijlk, cw_ijlk, ct_ilk, **_):
+        boost_ijlk = self.deficit_scaling(D_src_il, dw_ijlk, cw_ijlk, ct_ilk)
+        return boost_ijlk * Rathmann.calc_deficit(self, WS_ilk, D_src_il, dw_ijlk, cw_ijlk, ct_ilk, **_)
+
+
 def main():
     if __name__ == '__main__':
         from py_wake.examples.data.iea37._iea37 import IEA37Site
         from py_wake.examples.data.iea37._iea37 import IEA37_WindTurbines
+        from py_wake.site._site import UniformSite
+        from py_wake.wind_turbines import OneTypeWindTurbines
         from py_wake.superposition_models import LinearSum
         from py_wake.wind_farm_models import All2AllIterative
         from py_wake.deficit_models.no_wake import NoWakeDeficit
+        from py_wake.rotor_avg_models import RotorCenter
         from py_wake.deficit_models.vortexcylinder import VortexCylinder
         from py_wake.deficit_models.vortexdipole import VortexDipole
         import matplotlib.pyplot as plt
         from py_wake import HorizontalGrid
         from timeit import default_timer as timer
+        import time
 
         # setup site, turbines and wind farm model
         site = IEA37Site(16)
@@ -115,10 +173,13 @@ def main():
         windTurbines = IEA37_WindTurbines()
         d = windTurbines.diameter()
         ra = Rathmann()
+        ras = RathmannScaled()
         grid = HorizontalGrid(x=np.linspace(-6, 6, 100) * d, y=np.linspace(0, 4, 100) * d)
 
         noj_ra = All2AllIterative(site, windTurbines, wake_deficitModel=NoWakeDeficit(),
                                   superpositionModel=LinearSum(), blockage_deficitModel=ra)
+        noj_ras = All2AllIterative(site, windTurbines, wake_deficitModel=NoWakeDeficit(),
+                                   superpositionModel=LinearSum(), blockage_deficitModel=ras)
         noj_vc = All2AllIterative(site, windTurbines, wake_deficitModel=NoWakeDeficit(),
                                   superpositionModel=LinearSum(), blockage_deficitModel=VortexCylinder())
         noj_vd = All2AllIterative(site, windTurbines, wake_deficitModel=NoWakeDeficit(),
@@ -126,29 +187,40 @@ def main():
         t1 = timer()
         flow_map = noj_ra(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
         t2 = timer()
-        flow_map_vc = noj_vc(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
+        flow_map_ras = noj_ras(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
         t3 = timer()
-        flow_map_vd = noj_vd(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
+        flow_map_vc = noj_vc(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
         t4 = timer()
-        print(t2 - t1, t3 - t2, t4 - t3)
+        flow_map_vd = noj_vd(x=[0], y=[0], wd=[270], ws=[10]).flow_map(grid=grid)
+        t5 = timer()
+        print(t2 - t1, t3 - t2, t4 - t3, t5 - t4)
 
         plt.figure()
-        clevels = np.array([.6, .7, .8, .9, .95, .98, .99, .995, .998, .999, 1., 1.005, 1.01, 1.02, 1.05]) * 10.
+        clevels = np.array(
+            [.6, .7, .8, .9, .95, .98, .99, .995, .998, .999, 1., 1.005, 1.01, 1.02, 1.05]) * 10.
         flow_map.plot_wake_map(levels=clevels)
-        plt.contour(flow_map.x, flow_map.y, flow_map.WS_eff[:, :, 0, -1, 0], levels=clevels, colors='k', linewidths=1)
-        plt.contour(flow_map.x, flow_map.y, flow_map_vc.WS_eff[:, :, 0, -1, 0], levels=clevels, colors='r', linewidths=1, linestyles='dashed')
-        plt.contour(flow_map.x, flow_map.y, flow_map_vd.WS_eff[:, :, 0, -1, 0], levels=clevels, colors='b', linewidths=1, linestyles='dotted')
+        plt.contour(flow_map.x, flow_map.y,
+                    flow_map.WS_eff[:, :, 0, -1, 0], levels=clevels, colors='k', linewidths=1)
+        plt.contour(flow_map.x, flow_map.y, flow_map_ras.WS_eff[:, :, 0, -1, 0],
+                    levels=clevels, colors='g', linewidths=1, linestyles='dashed')
+        plt.contour(flow_map.x, flow_map.y, flow_map_vc.WS_eff[:, :, 0, -1, 0],
+                    levels=clevels, colors='r', linewidths=1, linestyles='dashed')
+        plt.contour(flow_map.x, flow_map.y, flow_map_vd.WS_eff[:, :, 0, -1, 0],
+                    levels=clevels, colors='b', linewidths=1, linestyles='dotted')
+        plt.plot([0, 0], [0, 0], 'k-', label='Rathmann')
+        plt.plot([0, 0], [0, 0], 'g--', label='scaled Rathmann')
+        plt.plot([0, 0], [0, 0], 'r--', label='vortex cylinder')
+        plt.plot([0, 0], [0, 0], 'b:', label='vortex dipole')
         plt.title('Rathmann')
         plt.ylabel("Crosswind distance [y/R]")
         plt.xlabel("Downwind distance [x/R]")
+        plt.legend()
         plt.show()
 
         # run wind farm simulation
         sim_res = noj_ra(x, y, wd=[0, 30, 45, 60, 90], ws=[5, 10, 15])
-
         # calculate AEP
         aep = sim_res.aep().sum()
-
         # plot wake map
         plt.figure()
         print(noj_ra)
@@ -157,5 +229,114 @@ def main():
         plt.title('Rathmann model, AEP: %.3f GWh' % aep)
         plt.show()
 
+        # run wind farm simulation
+        sim_res = noj_ras(x, y, wd=[0, 30, 45, 60, 90], ws=[5, 10, 15])
+        # calculate AEP
+        aep = sim_res.aep().sum()
+        # plot wake map
+        plt.figure()
+        print(noj_ras)
+        flow_map = sim_res.flow_map(wd=0, ws=10)
+        flow_map.plot_wake_map(levels=clevels, plot_colorbar=False)
+        plt.title('Rathmann model, AEP: %.3f GWh' % aep)
+        plt.show()
+
+        class epfl_model_wt(OneTypeWindTurbines):
+            def __init__(self):
+                OneTypeWindTurbines.__init__(self, 'NREL 5MW', diameter=2,
+                                             hub_height=1, ct_func=self._ct,
+                                             power_func=self._power, power_unit='W')
+
+            def _ct(self, u):
+                ct = 0.798
+                return ct * u / u
+
+            def _power(self, u):
+                cp = 0.5
+                A = np.pi
+                rho = 1.225
+                return 0.5 * rho * u**3 * A * cp
+
+        wt = epfl_model_wt()
+        d = wt.diameter()
+        h = wt.hub_height()
+        wt_x = np.array([-6. * d, -3. * d, 0. * d, 3. * d, 6. * d])
+        wt_y = np.array([0., 0., 0., 0., 0.])
+
+        class epfl_wt(UniformSite):
+            def __init__(self):
+                p_wd = [1]
+                ws = [1]
+                ti = [0.06]
+                UniformSite.__init__(self, p_wd=p_wd, ti=ti, ws=ws)
+                self.initial_position = np.array([wt_x, wt_y]).T
+
+        site = epfl_wt()
+        blockage_models = [
+            ('Rathmann', Rathmann()),
+            ('RathmannScaled', RathmannScaled())]
+
+        def pywake_run(blockage_models):
+
+            grid = HorizontalGrid(x=np.linspace(-10 * d, 10 * d, 100),
+                                  y=np.linspace(-15, 2 * d, 100))
+
+            res = {}
+            out = {}
+            for nam, blockage_model in blockage_models:
+                print(nam, blockage_model)
+                # for dum, rotor_avg_model in rotor_avg_models:
+                wm = All2AllIterative(site, wt, wake_deficitModel=NoWakeDeficit(),
+                                      superpositionModel=LinearSum(), blockage_deficitModel=blockage_model,
+                                      rotorAvgModel=RotorCenter())
+                res[nam] = wm(wt_x, wt_y, wd=[180., 195., 210., 225.], ws=[1.])
+                tic = time.perf_counter()
+                flow_map = res[nam].flow_map(grid=grid, ws=[1.], wd=225.)
+                toc = time.perf_counter()
+                elapsed_time = toc - tic
+                out[nam] = flow_map['WS_eff'] / flow_map['WS']
+                out[nam]['time'] = elapsed_time
+            return out, res
+
+        out, res = pywake_run(blockage_models)
+        # CFD data from Meyer Forsting et al. 2017
+        p_cfd = np.array([[-1.12511646713429, 0.268977884040651, 0.712062872514373, 1.08033923355738, 1.97378837188847],
+                          [-0.610410399845213, 0.355339771667814,
+                              0.670255435929930, 0.915154608331424, 1.52808830519513],
+                          [-0.0988002822865217, 0.451698279664756,
+                              0.636987630794206, 0.751760283763044, 1.03629687168984],
+                          [0.477918399858401, 0.628396438496795, 0.663799054750132, 0.628396438496795, 0.479688468006036]])
+
+        plt.figure()
+        lines = ['.-', 'o--', '^', '-.', '.-', '.-']
+        theta = [0, 15, 30, 45]
+        viridis = plt.cm.viridis(np.linspace(0, 0.9, 5))
+        jj = 0
+        tno = np.arange(1, 6, 1)
+        for i in range(4):
+            ii = len(theta) - i - 1
+            plt.plot(tno, ((p_cfd[ii, :] / 100. + 1.) / (p_cfd[ii, 0] / 100. + 1.) - 1.) * 100, 's:', color=viridis[i], label='CFD: ' + str(theta[i]) + 'deg',
+                     lw=2)
+
+        for nam, blockage_model in blockage_models:
+            for i in range(4):
+                if i == 3:
+                    plt.plot(tno, (res[nam].Power[:, i, 0] - res[nam].Power[0, i, 0]) / res[nam].Power[0, i, 0] * 100, lines[jj],
+                             label=nam, color=viridis[i],
+                             lw=1, alpha=0.8)
+                else:
+                    plt.plot(tno, (res[nam].Power[:, i, 0] - res[nam].Power[0, i, 0]) / res[nam].Power[0, i, 0] * 100, lines[jj],
+                             color=viridis[i],
+                             lw=1, alpha=0.8)
+            jj += 1
+
+        plt.grid(alpha=0.2)
+        plt.xlabel('Turbine no.')
+        plt.ylabel('Power change, $(P-P_1)/P_1$ [%]')
+        plt.xticks([0, 1, 2, 3, 4, 5])
+        plt.xlim([.9, 5.1])
+        plt.legend(fontsize=11)
+        plt.show()
+
 
 main()
diff --git a/py_wake/tests/test_deficit_models/test_blockage_models.py b/py_wake/tests/test_deficit_models/test_blockage_models.py
index 1ee3e4986..fb4b9a5ac 100644
--- a/py_wake/tests/test_deficit_models/test_blockage_models.py
+++ b/py_wake/tests/test_deficit_models/test_blockage_models.py
@@ -13,7 +13,7 @@ from py_wake.deficit_models.selfsimilarity import SelfSimilarityDeficit, SelfSim
 from py_wake.deficit_models.vortexdipole import VortexDipole
 from py_wake.deficit_models.rankinehalfbody import RankineHalfBody
 from py_wake.deficit_models.hybridinduction import HybridInduction
-from py_wake.deficit_models.rathmann import Rathmann
+from py_wake.deficit_models.rathmann import Rathmann, RathmannScaled
 from py_wake.flow_map import XYGrid
 
 debug = False
@@ -49,6 +49,9 @@ def setup():
     (Rathmann,
      [9.950432, 9.926598, 9.882538, 9.792603, 9.597037, 10.041663, 10.397465, 10.204838, 10.116258, 10.072778],
      [9.94641, 9.917526, 9.858037, 9.707178, 9.190401, 10.0, 10.0, 10.0, 10.0, 10.0]),
+    (RathmannScaled,
+     [9.929791, 9.897708, 9.838744, 9.719092, 9.456743, 10.056169, 10.535845, 10.277511, 10.159648, 10.101453],
+     [9.924278, 9.88539, 9.805788, 9.60523, 8.908535, 10.0, 10.0, 10.0, 10.0, 10.0]),
 ][::-1])
 def test_blockage_map(setup, blockage_model, center_ref, side_ref):
     site, windTurbines = setup
@@ -98,6 +101,9 @@ def test_blockage_map(setup, blockage_model, center_ref, side_ref):
     (Rathmann,
      [9.950432, 9.926598, 9.882538, 9.792603, 9.597037, 10.041663, 10.397465, 6.428758, 6.899183, 7.299176],
      [9.94641, 9.917526, 9.858037, 9.707178, 9.190401, 4.560631, 5.505472, 6.223921, 6.782925, 7.226399]),
+    (RathmannScaled,
+     [9.929791, 9.897708, 9.838744, 9.719092, 9.456743, 10.056169, 10.535845, 6.501432, 6.942573, 7.327852],
+     [9.924278, 9.88539, 9.805788, 9.60523, 8.908535, 4.560631, 5.505472, 6.223921, 6.782925, 7.226399]),
 ][::-1])
 def test_wake_and_blockage(setup, blockage_model, center_ref, side_ref):
     site, windTurbines = setup
@@ -132,7 +138,8 @@ def test_wake_and_blockage(setup, blockage_model, center_ref, side_ref):
     (VortexDipole, 0.4321021),
     (RankineHalfBody, 0.4321021),
     (HybridInduction, 0.5372064),
-    (Rathmann, 0.4233034298949511)
+    (Rathmann, 0.4233034298949511),
+    (RathmannScaled, 0.5928161590163314)
 ][::-1])
 def test_aep_two_turbines(setup, blockage_model, blockage_loss):
     site, windTurbines = setup
diff --git a/py_wake/tests/test_deficit_models/test_deficit_models.py b/py_wake/tests/test_deficit_models/test_deficit_models.py
index ec925d089..7cffc3816 100644
--- a/py_wake/tests/test_deficit_models/test_deficit_models.py
+++ b/py_wake/tests/test_deficit_models/test_deficit_models.py
@@ -8,7 +8,7 @@ from py_wake.deficit_models.gaussian import BastankhahGaussianDeficit, IEA37Simp
     ZongGaussianDeficit, NiayifarGaussianDeficit, BastankhahGaussian, IEA37SimpleBastankhahGaussian, ZongGaussian,\
     NiayifarGaussian
 from py_wake.deficit_models.gcl import GCLDeficit, GCL, GCLLocal
-from py_wake.deficit_models.noj import NOJDeficit, NOJ, NOJLocalDeficit, NOJLocal
+from py_wake.deficit_models.noj import NOJDeficit, NOJ, NOJLocalDeficit, NOJLocal, TurboNOJDeficit
 from py_wake.deficit_models import VortexDipole
 from py_wake.examples.data.hornsrev1 import Hornsrev1Site
 from py_wake.examples.data.iea37 import iea37_path
@@ -41,6 +41,10 @@ class GCLLocalDeficit(GCLDeficit):
                                               23558.34198, 36738.52415, 38663.44595, 21056.39764, 12042.79324,
                                               13813.46269, 30999.42279, 63947.61202, 17180.40299, 11334.12323,
                                               6972.14345])),
+     (TurboNOJDeficit(), (354154.2962989713, [9320.85263, 8138.29496, 10753.75662, 13398.00865, 21189.29438,
+                                              24190.84895, 37081.91938, 41369.66605, 23488.54863, 12938.48451,
+                                              14065.00719, 30469.75602, 71831.78532, 16886.85274, 11540.51872,
+                                              7490.70156])),
 
      (BastankhahGaussianDeficit(), (355971.9717035484,
                                     [9143.74048, 8156.71681, 11311.92915, 13955.06316, 19807.65346,
@@ -102,6 +106,8 @@ def test_IEA37_ex16(deficitModel, aep_ref):
       [3.27, 3.27, 9.0, 7.46, 7.46, 7.46, 7.46, 7.31, 7.31, 7.31, 7.31, 8.3, 8.3, 8.3, 8.3, 8.3, 8.3]),
      (NOJLocalDeficit(),
       [3.09, 3.09, 9., 9., 5.54, 5.54, 5.54, 5.54, 5.54, 5.54, 6.73, 6.73, 6.73, 6.73, 6.73, 6.73, 6.73]),
+     (TurboNOJDeficit(),
+      [3.09, 3.09, 9., 9., 5.54, 5.54, 5.54, 5.54, 5.54, 5.54, 6.73, 6.73, 6.73, 6.73, 6.73, 6.73, 6.73]),
      (BastankhahGaussianDeficit(),
       [0.18, 3.6, 7.27, 8.32, 7.61, 6.64, 5.96, 6.04, 6.8, 7.69, 8.08, 7.87, 7.59, 7.46, 7.55, 7.84, 8.19]),
      (IEA37SimpleBastankhahGaussianDeficit(),
@@ -156,6 +162,8 @@ def test_deficitModel_wake_map(deficitModel, ref):
     # test that the result is equal to last run (no evidens that  these number are correct)
     [(NOJDeficit(), [100., 75., 150., 100., 100.]),
      (NOJLocalDeficit(), [71., 46., 92., 71., 61.5]),
+     (TurboNOJDeficit(), [99.024477, 61.553917,
+      123.107833, 92.439673, 97.034049]),
      (BastankhahGaussianDeficit(),
       [83.336286, 57.895893, 115.791786, 75.266662, 83.336286]),
      (IEA37SimpleBastankhahGaussianDeficit(),
diff --git a/py_wake/tests/test_utils/test_model_utils.py b/py_wake/tests/test_utils/test_model_utils.py
index 79c34566c..d0778e257 100644
--- a/py_wake/tests/test_utils/test_model_utils.py
+++ b/py_wake/tests/test_utils/test_model_utils.py
@@ -23,14 +23,13 @@ def test_get_models():
 
 def test_get_signature():
     assert get_signature(NOJDeficit) == "NOJDeficit(k=0.1, use_effective_ws=False)"
-    print()
     assert get_signature(NOJDeficit, indent_level=1) == """NOJDeficit(
     k=0.1,
     use_effective_ws=False)"""
     assert (get_signature(STF2017TurbulenceModel) ==
-            "STF2017TurbulenceModel(addedTurbulenceSuperpositionModel=LinearSum(), weight_function=FrandsenWeight())")
+            "STF2017TurbulenceModel(c=[1.5, 0.8], addedTurbulenceSuperpositionModel=LinearSum(), weight_function=FrandsenWeight())")
     assert (get_signature(STF2017TurbulenceModel, {'addedTurbulenceSuperpositionModel': SqrMaxSum}) ==
-            "STF2017TurbulenceModel(addedTurbulenceSuperpositionModel=SqrMaxSum(), weight_function=FrandsenWeight())")
+            "STF2017TurbulenceModel(c=[1.5, 0.8], addedTurbulenceSuperpositionModel=SqrMaxSum(), weight_function=FrandsenWeight())")
     assert(get_signature(XRSite) ==
            "XRSite(ds, initial_position=None, interp_method='linear', shear=None, distance=StraightDistance(), default_ws=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25], bounds='check')")
 
diff --git a/py_wake/turbulence_models/stf.py b/py_wake/turbulence_models/stf.py
index 15b99fc06..ef31fe09a 100644
--- a/py_wake/turbulence_models/stf.py
+++ b/py_wake/turbulence_models/stf.py
@@ -59,16 +59,17 @@ class STF2017TurbulenceModel(TurbulenceModel):
 
     args4addturb = ['dw_ijlk', 'cw_ijlk', 'D_src_il', 'ct_ilk', 'TI_ilk']
 
-    def __init__(self, addedTurbulenceSuperpositionModel=LinearSum(),
+    def __init__(self, c=[1.5, 0.8], addedTurbulenceSuperpositionModel=LinearSum(),
                  weight_function=FrandsenWeight(), **kwargs):
         TurbulenceModel.__init__(self, addedTurbulenceSuperpositionModel, **kwargs)
+        self.c = c
         self.apply_weight = weight_function.apply_weight
 
     def max_centre_wake_turbulence(self, dw_ijlk, cw_ijlk, D_src_il, ct_ilk, **_):
         dist_ijlk = np.sqrt(dw_ijlk**2 + cw_ijlk**2) / D_src_il[:, na, :, na]
         # In the standard (see page 103), the maximal added TI is calculated as
         # TI_add = 1/(1.5 + 0.8*d/sqrt(Ct))
-        return 1 / (1.5 + 0.8 * dist_ijlk / np.sqrt(ct_ilk)[:, na])
+        return 1 / (self.c[0] + self.c[1] * dist_ijlk / np.sqrt(ct_ilk)[:, na])
 
     def calc_added_turbulence(self, dw_ijlk, cw_ijlk, D_src_il, TI_ilk, **kwargs):
         """ Calculate the added turbulence intensity at locations specified by
-- 
GitLab