diff --git a/docs/notebooks/EngineeringWindFarmModels.ipynb b/docs/notebooks/EngineeringWindFarmModels.ipynb index 36d236db92d7fb7812a3523a7c328c8ab8b9052a..e563e8b042c69fda51dee7af135655118ef8a4b9 100644 --- a/docs/notebooks/EngineeringWindFarmModels.ipynb +++ b/docs/notebooks/EngineeringWindFarmModels.ipynb @@ -1651,7 +1651,7 @@ "source": [ "theta = np.linspace(-np.pi,np.pi,6, endpoint=False)\n", "r = 2/3\n", - "plot_rotor_avg_model(PolarGridRotorAvg(nodes_r =r, nodes_theta=theta, nodes_weight=None), 'PolarGrid_6')" + "plot_rotor_avg_model(PolarGridRotorAvg(r=r, theta=theta, r_weight=None, theta_weight=None), 'PolarGrid_6')" ] }, { diff --git a/py_wake/rotor_avg_models/__init__.py b/py_wake/rotor_avg_models/__init__.py index e9bb91589012b07503d9c6291f35fb18acd791d1..97077a75e4b5d0f805c8fdbd05b6abb7a40d49c9 100644 --- a/py_wake/rotor_avg_models/__init__.py +++ b/py_wake/rotor_avg_models/__init__.py @@ -1,4 +1,5 @@ -from .rotor_avg_model import RotorAvgModel, RotorCenter, EqGridRotorAvg, GQGridRotorAvg, GridRotorAvg, PolarGridRotorAvg, \ +from .rotor_avg_model import RotorAvgModel, RotorCenter, EqGridRotorAvg, GQGridRotorAvg, GridRotorAvg, \ + PolarGridRotorAvg, PolarRotorAvg, \ CGIRotorAvg, WSPowerRotorAvg, polar_gauss_quadrature, gauss_quadrature from .area_overlap_model import AreaOverlapAvgModel from .gaussian_overlap_model import GaussianOverlapAvgModel diff --git a/py_wake/rotor_avg_models/rotor_avg_model.py b/py_wake/rotor_avg_models/rotor_avg_model.py index 2e7aed4e7e83a4762c4c6de8bf5d2b9a7458abb2..00354237afbe7f2097d6490301a346dc7d94c7af 100644 --- a/py_wake/rotor_avg_models/rotor_avg_model.py +++ b/py_wake/rotor_avg_models/rotor_avg_model.py @@ -36,6 +36,8 @@ class NodeRotorAvgModel(RotorAvgModel): """ def __call__(self, func, D_dst_ijl, **kwargs): + if D_dst_ijl.shape == (1, 1, 1) and D_dst_ijl[0, 0, 0] == 0: + return func(**kwargs) # add extra dimension, p, with 40 points distributed over the destination rotors kwargs = self._update_kwargs(D_dst_ijl=D_dst_ijl, **kwargs) @@ -97,13 +99,27 @@ class GQGridRotorAvg(GridRotorAvg): GridRotorAvg.__init__(self, nodes_x=x[m], nodes_y=y[m], nodes_weight=w) -class PolarGridRotorAvg(GridRotorAvg): +class PolarRotorAvg(GridRotorAvg): def __init__(self, nodes_r=2 / 3, nodes_theta=np.linspace(-np.pi, np.pi, 6, endpoint=False), nodes_weight=None): self.nodes_x = nodes_r * np.cos(-nodes_theta - np.pi / 2) self.nodes_y = nodes_r * np.sin(-nodes_theta - np.pi / 2) self.nodes_weight = nodes_weight +class PolarGridRotorAvg(PolarRotorAvg): + def __init__(self, r=[1 / 3, 2 / 3], theta=np.linspace(-np.pi, np.pi, 6, endpoint=False), + r_weight=[.5**2, 1 - .5**2], theta_weight=1 / 6): + assert (r_weight is None) == (theta_weight is None) + nodes_r, nodes_theta = np.meshgrid(r, theta) + nodes_weight = None + if r_weight is not None: + r_weight, theta_weight = np.zeros(len(r)) + r_weight, np.zeros(len(theta)) + theta_weight + nodes_weight = np.prod(np.meshgrid(r_weight, theta_weight), 0).flatten() + + PolarRotorAvg.__init__(self, nodes_r=nodes_r.flatten(), nodes_theta=nodes_theta.flatten(), + nodes_weight=nodes_weight) + + class CGIRotorAvg(GridRotorAvg): """Circular Gauss Integration""" diff --git a/py_wake/tests/test_rotor_avg_models/test_rotor_avg_models.py b/py_wake/tests/test_rotor_avg_models/test_rotor_avg_models.py index 6410dfa385516c921f44ce2e4dc93bec506a750f..762ee38724c622c06bdf4de42647b405c877cbc2 100644 --- a/py_wake/tests/test_rotor_avg_models/test_rotor_avg_models.py +++ b/py_wake/tests/test_rotor_avg_models/test_rotor_avg_models.py @@ -13,7 +13,7 @@ from py_wake.flow_map import HorizontalGrid, XYGrid from py_wake.rotor_avg_models import gauss_quadrature, PolarGridRotorAvg, \ polar_gauss_quadrature, EqGridRotorAvg, GQGridRotorAvg, CGIRotorAvg, GridRotorAvg, WSPowerRotorAvg from py_wake.rotor_avg_models.gaussian_overlap_model import GaussianOverlapAvgModel -from py_wake.rotor_avg_models.rotor_avg_model import RotorAvgModel, RotorCenter +from py_wake.rotor_avg_models.rotor_avg_model import RotorAvgModel, RotorCenter, PolarRotorAvg from py_wake.site._site import UniformSite from py_wake.superposition_models import SquaredSum, LinearSum, WeightedSum from py_wake.tests import npt @@ -21,6 +21,8 @@ from py_wake.turbulence_models.stf import STF2017TurbulenceModel from py_wake.utils.model_utils import get_models from py_wake.wind_farm_models.engineering_models import All2AllIterative, PropagateDownwind, EngineeringWindFarmModel from py_wake.turbulence_models.turbulence_model import TurbulenceModel +from py_wake.rotor_avg_models.area_overlap_model import AreaOverlapAvgModel +from py_wake.deficit_models.noj import NOJ EngineeringWindFarmModel.verbose = False @@ -277,7 +279,7 @@ def test_RotorGaussQuadratureGridAvgModel(): def test_polar_gauss_quadrature(): - m = PolarGridRotorAvg(*polar_gauss_quadrature(4, 3)) + m = PolarRotorAvg(*polar_gauss_quadrature(4, 3)) if 0: for v in [m.nodes_x, m.nodes_y, m.nodes_weight]: @@ -296,6 +298,26 @@ def test_polar_gauss_quadrature(): 0.08, 0.05, 0.09, 0.09, 0.05], 2) +def test_polar_grid(): + m = PolarGridRotorAvg(r_weight=[.5**2, 1 - .5**2], theta_weight=1 / 6) + + if 0: + for v in [m.nodes_x, m.nodes_y, m.nodes_weight]: + print(np.round(v, 2).tolist()) + plt.scatter(m.nodes_x, m.nodes_y, c=m.nodes_weight) + plt.axis('equal') + plt.gca().add_artist(plt.Circle((0, 0), 1, fill=False)) + plt.ylim([-1, 1]) + plt.show() + + npt.assert_array_almost_equal(m.nodes_x, + [0.0, 0.0, 0.29, 0.58, 0.29, 0.58, 0.0, 0.0, -0.29, -0.58, -0.29, -0.58], 2) + npt.assert_array_almost_equal(m.nodes_y, + [0.33, 0.67, 0.17, 0.33, -0.17, -0.33, -0.33, -0.67, -0.17, -0.33, 0.17, 0.33], 2) + npt.assert_array_almost_equal(m.nodes_weight, + [0.04, 0.12, 0.04, 0.12, 0.04, 0.12, 0.04, 0.12, 0.04, 0.12, 0.04, 0.12], 2) + + @pytest.mark.parametrize('n,x,y,w', [ (4, [-0.5, -0.5, 0.5, 0.5], [-0.5, 0.5, -0.5, 0.5], [0.25, 0.25, 0.25, 0.25]), (7, [0.0, -0.82, 0.82, -0.41, -0.41, 0.41, 0.41], @@ -411,3 +433,21 @@ def test_WSPowerRotorAvgModel(): rotorAvgModel = WSPowerRotorAvg(GridRotorAvg(nodes_x=[-1, 0, 1], nodes_y=[0, 0, 0]), alpha=2) wfm = BastankhahGaussian(UniformSite(), V80(), rotorAvgModel=rotorAvgModel) npt.assert_almost_equal(wfm(x, y, wd=270).WS_eff.sel(wt=1).squeeze(), ws_eff_ref) + + +def test_flow_map(): + wfm = NOJ(UniformSite(), V80(), rotorAvgModel=CGIRotorAvg(7)) + y = np.linspace(-110, -50, 10) + fm = wfm([0], [0], wd=270, ws=12).flow_map(XYGrid(x=400, y=y)) + fm_avg = wfm([0], [0], wd=270, ws=12).flow_map(XYGrid(x=400, y=y), D_dst=40) + if 0: + print(list(np.round(fm.WS_eff.squeeze().values, 2))) + print(list(np.round(fm_avg.WS_eff.squeeze().values, 2))) + plt.plot(y, fm.WS_eff.squeeze()) + plt.plot(y, fm_avg.WS_eff.squeeze()) + plt.show() + + npt.assert_array_almost_equal(fm.WS_eff.squeeze(), + [12.0, 12.0, 12.0, 12.0, 12.0, 10.62, 10.62, 10.62, 10.62, 10.62], 2) + npt.assert_array_almost_equal(fm_avg.WS_eff.squeeze(), + [12.0, 12.0, 12.0, 11.83, 11.48, 11.14, 10.79, 10.62, 10.62, 10.62], 2) diff --git a/py_wake/wind_farm_models/engineering_models.py b/py_wake/wind_farm_models/engineering_models.py index e1cbac6191b34c800128bd255658c070ec64db91..59dce52eba7dcd0b421d00281c9eb51e7d62464e 100644 --- a/py_wake/wind_farm_models/engineering_models.py +++ b/py_wake/wind_farm_models/engineering_models.py @@ -249,7 +249,7 @@ class EngineeringWindFarmModel(WindFarmModel): def _calc_wt_interaction(self, **kwargs): """calculate WT interaction""" - def get_map_args(self, x_j, y_j, h_j, sim_res_data): + def get_map_args(self, x_j, y_j, h_j, sim_res_data, D_dst=0): wt_d_i = self.windTurbines.diameter(sim_res_data.type) wd, ws = [np.atleast_1d(sim_res_data[k].values) for k in ['wd', 'ws']] time = sim_res_data.get('time', False) @@ -270,7 +270,7 @@ class EngineeringWindFarmModel(WindFarmModel): for k in sim_res_data if k not in ['wd_bin_size', 'ws_l', 'ws_u']} map_arg_funcs.update({ 'D_src_il': lambda l: wt_d_i[:, na], - 'D_dst_ijl': lambda l: np.zeros((I, J, 1)), + 'D_dst_ijl': lambda l: np.zeros((1, 1, 1)) + D_dst, 'IJLK': lambda l=slice(None), I=I, J=J, L=L, K=K: (I, J, len(np.arange(L)[l]), K)}) return map_arg_funcs, lw_j, wd, WD_il @@ -341,9 +341,9 @@ class EngineeringWindFarmModel(WindFarmModel): aep_j = (power_jlk * lw_j.P_ilk).sum((1, 2)) return aep_j * 365 * 24 * 1e-9 - def _flow_map(self, x_j, y_j, h_j, sim_res_data): + def _flow_map(self, x_j, y_j, h_j, sim_res_data, D_dst=0): """call this function via SimulationResult.flow_map""" - arg_funcs, lw_j, wd, WD_il = self.get_map_args(x_j, y_j, h_j, sim_res_data) + arg_funcs, lw_j, wd, WD_il = self.get_map_args(x_j, y_j, h_j, sim_res_data, D_dst=D_dst) I, J, L, K = arg_funcs['IJLK']() if I == 0: return (lw_j, np.broadcast_to(lw_j.WS_ilk, (len(x_j), L, K)).astype(float), diff --git a/py_wake/wind_farm_models/wind_farm_model.py b/py_wake/wind_farm_models/wind_farm_model.py index 44bab78b85b6ba849ede39157916c74de4b35aec..d666f95f185182e1c6a10d32e255d995423955ba 100644 --- a/py_wake/wind_farm_models/wind_farm_model.py +++ b/py_wake/wind_farm_models/wind_farm_model.py @@ -727,7 +727,7 @@ class SimulationResult(xr.Dataset): else: # pragma: no cover raise NotImplementedError() - def flow_map(self, grid=None, wd=None, ws=None, time=None): + def flow_map(self, grid=None, wd=None, ws=None, time=None, D_dst=0): """Return a FlowMap object with WS_eff and TI_eff of all grid points Parameters @@ -736,6 +736,18 @@ class SimulationResult(xr.Dataset): Grid, e.g. HorizontalGrid or\n tuple(X, Y, x, y, h) where X, Y is the meshgrid for visualizing data\n and x, y, h are the flattened grid points + wd : int, float, array_like or None + Wind directions to include in the flow map (if more than one, an weighted average will be computed) + The simulation result must include the requested wind directions. + If None, an weighted average of all wind directions from the simulation results will be computed. + Note, computing a flow map with multiple wind directions may be slow + ws : int, float, array_like or None + Same as "wd", but for wind speed + ws : int, array_like or None + Same as "wd", but for time + D_dst : int, float or None + In combination with a rotor average model, D_dst defines the downstream rotor diameter + at which the deficits will be averaged See Also -------- @@ -750,7 +762,7 @@ class SimulationResult(xr.Dataset): for k in self.__slots__: setattr(sim_res, k, getattr(self, k)) - lw_j, WS_eff_jlk, TI_eff_jlk = self.windFarmModel._flow_map(x_j, y_j, h_j, sim_res) + lw_j, WS_eff_jlk, TI_eff_jlk = self.windFarmModel._flow_map(x_j, y_j, h_j, sim_res, D_dst=D_dst) return FlowMap(sim_res, X, Y, lw_j, WS_eff_jlk, TI_eff_jlk, plane=plane) def _wd_ws(self, wd, ws):