diff --git a/py_wake/tests/test_windturbines/test_power_ct_curves.py b/py_wake/tests/test_windturbines/test_power_ct_curves.py index 5cf38b2163c28a0c5c25589ce3b5bb30ee97abbb..8a162c3671098baa75d222089136d8bcb529d0b2 100644 --- a/py_wake/tests/test_windturbines/test_power_ct_curves.py +++ b/py_wake/tests/test_windturbines/test_power_ct_curves.py @@ -3,10 +3,13 @@ import pytest import matplotlib.pyplot as plt import numpy as np import xarray as xr -from py_wake.examples.data import hornsrev1 +from py_wake.examples.data import hornsrev1, wtg_path from py_wake.tests import npt from py_wake.wind_turbines.power_ct_functions import CubePowerSimpleCt, PowerCtNDTabular, DensityScale, \ - PowerCtTabular, PowerCtFunction, PowerCtFunctionList, PowerCtXr + PowerCtTabular, PowerCtFunction, PowerCtFunctionList, PowerCtXr, PowerCtGeneric +from py_wake.examples.data.hornsrev1 import V80 +from py_wake.wind_turbines._wind_turbines import WindTurbine +from py_wake.examples.data.dtu10mw import DTU10MW @pytest.mark.parametrize('method,unit,p_scale,p_ref,ct_ref', [ @@ -168,6 +171,70 @@ def test_FunctionalPowerCtCurve(): 0.245, 0.092, 0.031, 0.03]) * 1.3 / 1.225, 3) +def test_PowerCtGeneric(): + for ref, ti, p_tol, ct_tol in [(V80(), .1, 0.03, .16), + (WindTurbine.from_WAsP_wtg(wtg_path + "Vestas V112-3.0 MW.wtg"), .05, 0.035, .07), + (DTU10MW(), .05, 0.06, .13)]: + power_norm = ref.power(15) + curve = PowerCtGeneric( + power_norm=power_norm / 1000, + diameter=ref.diameter(), + turbulence_intensity=ti, + ws_cutin=None, + max_cp=.49, + constant_ct=.8) + + if 0: + u = np.arange(0, 30, .1) + p, ct = curve(u) + plt.plot(u, p / 1e6, label='Generic') + + plt.plot(u, ref.power(u) / 1e6, label=ref.name()) + + plt.ylabel('Power [MW]') + plt.legend() + ax = plt.twinx() + ax.plot(u, ct, '--') + ax.plot(u, ref.ct(u), '--') + plt.ylabel('Ct') + plt.show() + + u = np.arange(5, 25) + p, ct = curve(u) + p_ref, ct_ref = ref.power_ct(u) + # print(np.abs(p_ref - p).max() / power_norm) + npt.assert_allclose(p, p_ref, atol=power_norm * p_tol) + # print(np.abs(ct_ref - ct).max()) + npt.assert_allclose(ct, ct_ref, atol=ct_tol) + + +def test_PowerCtGeneric2(): + ref = V80() + power_norm = ref.power(15) + curve = PowerCtGeneric( + power_norm=power_norm / 1000, + diameter=ref.diameter(), + turbulence_intensity=0, + ws_cutin=3, + max_cp=.49, + constant_ct=.8) + + if 0: + u = np.arange(0, 30, .1) + p, ct = curve(u) + plt.plot(u, p / 1e6, label='Generic') + + plt.plot(u, ref.power(u) / 1e6, label=ref.name()) + + plt.ylabel('Power [MW]') + plt.legend() + ax = plt.twinx() + ax.plot(u, ct, '--') + ax.plot(u, ref.ct(u), '--') + plt.ylabel('Ct') + plt.show() + + def get_continuous_curve(key, optional): u_p, p = np.asarray(hornsrev1.power_curve).T.copy() ct = hornsrev1.ct_curve[:, 1] diff --git a/py_wake/utils/generic_power_ct_curves.py b/py_wake/utils/generic_power_ct_curves.py new file mode 100644 index 0000000000000000000000000000000000000000..f25f7f738ddba6ebf70879b76e5207b66d29fbc6 --- /dev/null +++ b/py_wake/utils/generic_power_ct_curves.py @@ -0,0 +1,90 @@ +import numpy as np +from numpy import newaxis as na +from scipy.optimize import optimize +from scipy.optimize.zeros import newton + + +def standard_power_ct_curve(power_norm, diameter, turbulence_intensity=.1, + + rho=1.225, max_cp=.49, constant_ct=.8, + gear_loss_const=.01, gear_loss_var=.014, generator_loss=0.03, converter_loss=.03, + wsp_lst=np.arange(3, 25, .1)): + """Generate standard power curve, extracted from WETB (original extracted from excel sheet made by Kenneth Thomsen) + + Parameters + ---------- + power_norm : int or float + Nominal power [kW] + diameter : int or float + Rotor diameter [m] + turbulence_intensity : float + Turbulence intensity [%] + rho : float, optional + Density of air [kg/m^3], default is 1.225 + max_cp : float, optional + Maximum power coefficient + constant_ct : float, optional + Ct value in constant-ct region + gear_loss_const : float, optional + Constant gear loss [% of power_norm in kW] + gear_loss_var : float, optional + Variable gear loss [%] + generator_loss : float, optional + Generator loss [%] + converter_loss : float, optional + converter loss [%] + """ + + area = (diameter / 2) ** 2 * np.pi + + sigma_lst = wsp_lst * turbulence_intensity + + p_aero = .5 * rho * area * wsp_lst ** 3 * max_cp / 1000 + + # calc power - gear, generator and conv loss + gear_loss = gear_loss_const * power_norm + gear_loss_var * p_aero + p_gear = p_aero - gear_loss + p_gear[p_gear < 0] = 0 + p_generator_loss = generator_loss * p_gear + p_gen = p_gear - p_generator_loss + p_converter_loss = converter_loss * p_gen + p_raw = p_gen - p_converter_loss + p_raw[p_raw > power_norm] = power_norm + + if turbulence_intensity > 0: + sigma = sigma_lst[:, na] + ndist = 1 / np.sqrt(2 * np.pi * sigma ** 2) * np.exp(-(wsp_lst - wsp_lst[:, na]) ** 2 / (2 * sigma ** 2)) + power_lst = (ndist * p_raw).sum(1) / ndist.sum(1) + else: + power_lst = p_raw + + p_gen = power_lst / (1 - converter_loss) + p_gear = p_gen / (1 - generator_loss) + p_aero = (p_gear + gear_loss_const * power_norm) / (1 - gear_loss_var) + cp = p_aero * 1000 / (.5 * rho * area * wsp_lst ** 3) + constant_ct_idx = np.where(np.abs(np.diff(cp)) < 1e-4)[0][0] + cp = np.minimum(cp, 16 / 27) + + def cp2ct(cp): + a = np.array([newton(lambda a, cp=cp:4 * a * (1 - a)**2 - cp, .1) for cp in cp]) + return 4 * a * (1 - a) + + ct_lst = cp2ct(cp) + f = constant_ct / ct_lst[constant_ct_idx] + ct_lst = np.minimum(8 / 9, ct_lst * f) + return wsp_lst, power_lst, ct_lst + + +def main(): + if __name__ == '__main__': + + import matplotlib.pyplot as plt + u = np.linspace(0., 30, 500) + u, p, ct = standard_power_ct_curve(10000, 178.3, 0.1) + plt.plot(u, p) + ax = plt.twinx() + ax.plot(u, ct) + plt.show() + + +main() diff --git a/py_wake/wind_turbines/power_ct_functions.py b/py_wake/wind_turbines/power_ct_functions.py index 6db35ee3b80b2da4672ce765635473012569c9ec..b83a83b6d0b8c9c4fa7f985ae8e38396f1a74243 100644 --- a/py_wake/wind_turbines/power_ct_functions.py +++ b/py_wake/wind_turbines/power_ct_functions.py @@ -8,6 +8,7 @@ from py_wake.utils.gradients import fd from scipy.interpolate.fitpack2 import UnivariateSpline from autograd.numpy.numpy_boxes import ArrayBox from autograd.builtins import SequenceBox +from py_wake.utils.generic_power_ct_curves import standard_power_ct_curve class PowerCtModel(): @@ -502,3 +503,61 @@ class CubePowerSimpleCt(PowerCtFunction): self.dct_rated2cutout(ws), 0) # constant ct return np.asarray([dp, dct]) + + +class PowerCtGeneric(PowerCtTabular): + def __init__(self, power_norm, diameter, turbulence_intensity=.1, + rho=1.225, max_cp=.49, constant_ct=.8, + gear_loss_const=.01, gear_loss_var=.014, generator_loss=0.03, converter_loss=.03, + wsp_lst=np.arange(.1, 25, .1), ws_cutin=None, + ws_cutout=None, power_idle=0, ct_idle=0, method='linear', additional_models=default_additional_models): + """Generate generic standard power curve based on max_cp, rated power and losses. + Ct is computed from the basic 1d momentum theory + + Parameters + ---------- + power_norm : int or float + Nominal power [kW] + diameter : int or float + Rotor diameter [m] + turbulence_intensity : float + Turbulence intensity [%] + rho : float optional + Density of air [kg/m^3], defualt is 1.225 + max_cp : float + Maximum power coefficient + constant_ct : float, optional + Ct value in constant-ct region + gear_loss_const : float + Constant gear loss [%] + gear_loss_var : float + Variable gear loss [%] + generator_loss : float + Generator loss [%] + converter_loss : float + converter loss [%] + ws_cutin : number or None, optional + if number, then the range [0,ws_cutin[ will be set to power_idle and ct_idle + ws_cutout : number or None, optional + if number, then the range ]ws_cutout,100] will be set to power_idle and ct_idle + power_idle : number, optional + see ws_cutin and ws_cutout + ct_idle : number, optional + see ws_cutin and ws_cutout + method : {'linear', 'phip','spline} + Interpolation method:\n + - linear: fast, discontinous gradients\n + - pchip: smooth\n + - spline: smooth, closer to linear, small overshoots in transition to/from constant plateaus) + additional_models : list, optional + list of additional models. + """ + + u, p, ct = standard_power_ct_curve(power_norm, diameter, turbulence_intensity, rho, max_cp, constant_ct, + gear_loss_const, gear_loss_var, generator_loss, converter_loss, wsp_lst) + + if ws_cutin is not None: + ct[u < ws_cutin] = ct_idle + PowerCtTabular.__init__(self, u, p * 1000, 'w', ct, ws_cutin=ws_cutin, ws_cutout=ws_cutout, + power_idle=power_idle, ct_idle=ct_idle, method=method, + additional_models=additional_models)