diff --git a/mitwindfarm/CurledWake.py b/mitwindfarm/CurledWake.py index 7f6094a..84e97f8 100644 --- a/mitwindfarm/CurledWake.py +++ b/mitwindfarm/CurledWake.py @@ -251,7 +251,7 @@ def stamp_ic( # rotate points into yaw-and-tilt frame _, y_i, z_i = eff_yaw_inv_rotation(np.zeros_like(r_i), np.zeros_like(r_i), r_i, eff_yaw, rotor.yaw, rotor.tilt) # NOTE: rotor.Ct differs from Shapiro et al. (2018) definition - includes cos^2(eff_yaw) - Gamma_0 = 0.5 * D * rotor.REWS * rotor.Ct * np.sin(eff_yaw) + Gamma_0 = 0.5 * D * rotor.REWS * rotor.extra.Ct * np.sin(eff_yaw) Gamma_i = ( Gamma_0 * 4 * r_i / (self.N_vortex * D * np.sqrt(1 - (2 * r_i / D) ** 2)) ) diff --git a/mitwindfarm/Rotor.py b/mitwindfarm/Rotor.py index 6b62bdb..3c33063 100644 --- a/mitwindfarm/Rotor.py +++ b/mitwindfarm/Rotor.py @@ -562,21 +562,21 @@ def residual( return e_an, e_u4, e_v4, e_x0, e_dp -def compute_x0_with_TI(rotor_solution: RotorSolution, alpha=2.32, beta_s=0.1403): - # Extract quantities from rotor solution for ease of use and documentation - Us = rotor_solution.REWS - a = rotor_solution.extra.an # HAS NOT been scaled by velocity - u4 = rotor_solution.u4 # HAS been scaled by velocity +def compute_x0_with_TI(rotor_solution: RotorSolution, alpha=2.32, beta_s=0.1403): + """ + U0-invariant x0, computed with non-dimensional velocities. Needed because + alpha*TI is non-dim only. This is *probably* the correct way to compute x0. + """ + Us = 1.0 # nondimensional reference (was: rotor_solution.REWS) + a = rotor_solution.extra.an + u4 = rotor_solution.extra.u4 # nondimensional (was: rotor_solution.u4) - # Convert yaw, tilt to rotated frame of reference yaw_eff = calc_eff_yaw(rotor_solution.yaw, rotor_solution.tilt) - # Compute near wake length x0 and return x0 = ( (np.cos(yaw_eff) * (Us + u4)) / - ((2*beta_s) * np.abs(Us - u4) + 4 * alpha * rotor_solution.TI) + ((2*beta_s) * np.abs(Us - u4) / Us + 4 * alpha * rotor_solution.TI) * np.sqrt(((1 - a) * np.cos(yaw_eff) * Us)/(Us + u4)) ) - return x0 \ No newline at end of file diff --git a/mitwindfarm/__init__.py b/mitwindfarm/__init__.py index cf67b1c..b7226a8 100644 --- a/mitwindfarm/__init__.py +++ b/mitwindfarm/__init__.py @@ -1,6 +1,9 @@ from ._Layout import GridLayout, Square, Layout -from .FlorisInterface import FlorisCurledWindfarm -from .Rotor import RotorSolution, AD, UnifiedAD, BEM, CosineRotor +from .Rotor import RotorSolution, AD, UnifiedAD, BEM, CosineRotor, UnifiedAD_TI +try: + from .FlorisInterface import FlorisCurledWindfarm +except ModuleNotFoundError: + pass from .RotorGrid import Point, Line, Area from .Superposition import Linear, Niayifar, Quadratic, Dominant from .Wake import WakeModel, GaussianWakeModel, GaussianWake, VariableKwGaussianWakeModel diff --git a/tests/test_uref_invariance.py b/tests/test_uref_invariance.py new file mode 100644 index 0000000..274c061 --- /dev/null +++ b/tests/test_uref_invariance.py @@ -0,0 +1,57 @@ +""" +Test that the CurledWake solver produces non-dimensionally invariant results +when the freestream velocity U0 is rescaled (and k-l is used). +""" + +import numpy as np +from pytest import approx +from mitwindfarm import Uniform, Layout +from mitwindfarm.windfarm import CurledWindfarm +from mitwindfarm.Rotor import UnifiedAD_TI + + +def _run_curled_farm(U0, k_model="const"): + """Run a 2-turbine yawed farm at given U0.""" + wf = CurledWindfarm( + rotor_model=UnifiedAD_TI(), + base_windfield=Uniform(U0=U0, TIamb=0.05), + solver_kwargs=dict( + dy=0.1, dz=0.1, dx=0.05, + integrator="ef", + k_model=k_model, + verbose=False, + ), + ) + layout = Layout([0, 5], [0, 0], [0, 0]) + yaw = np.radians(20) + setpoints = [(2, yaw, 0)] * 2 + return wf(layout, setpoints) + + +def test_uref_invariance_kl(): + """ + REWS/U0 must be identical at U0=1 and U0=2 (k-l turbulence model). + + Note: "const" will not be u-invariant unless the eddy viscosity `nu_T` is + also scaled with U0. + """ + sol1 = _run_curled_farm(U0=1.0, k_model="k-l") + sol2 = _run_curled_farm(U0=2.0, k_model="k-l") + + for i in range(2): + rews_ratio_1 = sol1.rotors[i].REWS / 1.0 + rews_ratio_2 = sol2.rotors[i].REWS / 2.0 + assert rews_ratio_2 == approx(rews_ratio_1, rel=1e-5), ( + f"Turbine {i}: REWS/U0 differs between U0=1 ({rews_ratio_1:.4f}) " + f"and U0=2 ({rews_ratio_2:.4f})" + ) + + +def test_downstream_rews_below_freestream(): + """Downstream turbine in a wake cannot see REWS > U0 with uniform inflow.""" + sol = _run_curled_farm(U0=8.0, k_model="const") + for i in range(1, 2): # skip turbine 0 (upstream) + assert sol.rotors[i].REWS <= 8.0 * 1.01, ( + f"Turbine {i}: REWS={sol.rotors[i].REWS:.2f} exceeds U0=8.0 " + f"— physically impossible with uniform inflow" + )