Source code for floris.core.wake_deflection.gauss


from __future__ import annotations

from typing import Any

import numexpr as ne
import numpy as np
from attrs import (
    define,
    field,
    fields,
)
from numpy import pi

from floris.core import (
    BaseModel,
    Farm,
    FlowField,
    Grid,
    Turbine,
)
from floris.utilities import cosd, sind


NUM_EPS = fields(BaseModel).NUM_EPS.default

[docs] @define class GaussVelocityDeflection(BaseModel): """ The Gauss deflection model is a blend of the models described in :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` for calculating the deflection field in turbine wakes. parameter_dictionary (dict): Model-specific parameters. Default values are used when a parameter is not included in `parameter_dictionary`. Possible key-value pairs include: - **ka** (*float*): Parameter used to determine the linear relationship between the turbulence intensity and the width of the Gaussian wake shape. - **kb** (*float*): Parameter used to determine the linear relationship between the turbulence intensity and the width of the Gaussian wake shape. - **alpha** (*float*): Parameter that determines the dependence of the downstream boundary between the near wake and far wake region on the turbulence intensity. - **beta** (*float*): Parameter that determines the dependence of the downstream boundary between the near wake and far wake region on the turbine's induction factor. - **ad** (*float*): Additional tuning parameter to modify the wake deflection with a lateral offset. Defaults to 0. - **bd** (*float*): Additional tuning parameter to modify the wake deflection with a lateral offset. Defaults to 0. - **dm** (*float*): Additional tuning parameter to scale the amount of wake deflection. Defaults to 1.0 - **use_secondary_steering** (*bool*): Flag to use secondary steering on the wake velocity using methods developed in [2]. - **eps_gain** (*float*): Tuning value for calculating the V- and W-component velocities using methods developed in [7]. TODO: Believe this should be removed, need to verify. See property on super-class for more details. References: .. bibliography:: /references.bib :style: unsrt :filter: docname in docnames :keyprefix: gdm- """ ad: float = field(converter=float, default=0.0) bd: float = field(converter=float, default=0.0) alpha: float = field(converter=float, default=0.58) beta: float = field(converter=float, default=0.077) ka: float = field(converter=float, default=0.38) kb: float = field(converter=float, default=0.004) dm: float = field(converter=float, default=1.0) eps_gain: float = field(converter=float, default=0.2) use_secondary_steering: bool = field(converter=bool, default=True)
[docs] def prepare_function( self, grid: Grid, flow_field: FlowField, ) -> dict[str, Any]: kwargs = { "x": grid.x_sorted, "y": grid.y_sorted, "z": grid.z_sorted, "freestream_velocity": flow_field.u_initial_sorted, "wind_veer": flow_field.wind_veer, } return kwargs
# @profile
[docs] def function( self, x_i: np.ndarray, y_i: np.ndarray, yaw_i: np.ndarray, turbulence_intensity_i: np.ndarray, ct_i: np.ndarray, rotor_diameter_i: float, *, x: np.ndarray, y: np.ndarray, z: np.ndarray, freestream_velocity: np.ndarray, wind_veer: float, ): """ Calculates the deflection field of the wake. See :cite:`gdm-bastankhah2016experimental` and :cite:`gdm-King2019Controls` for details on the methods used. Args: x_i (np.array): x-coordinates of turbine i. y_i (np.array): y-coordinates of turbine i. yaw_i (np.array): Yaw angle of turbine i. turbulence_intensity_i (np.array): Turbulence intensity at turbine i. ct_i (np.array): Thrust coefficient of turbine i. rotor_diameter_i (float): Rotor diameter of turbine i. Returns: np.array: Deflection field for the wake. """ # ============================================================== # Opposite sign convention in this model yaw_i *= -1 # TODO: connect support for tilt tilt = 0.0 # turbine.tilt_angle # initial velocity deficits uR = ( freestream_velocity * ct_i * cosd(tilt) * cosd(yaw_i) / (2.0 * (1 - np.sqrt(1 - (ct_i * cosd(tilt) * cosd(yaw_i))))) ) u0 = freestream_velocity * np.sqrt(1 - ct_i) # length of near wake x0 = ( rotor_diameter_i * (cosd(yaw_i) * (1 + np.sqrt(1 - ct_i * cosd(yaw_i)))) / (np.sqrt(2) * ( 4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i)) )) + x_i ) # wake expansion parameters ky = self.ka * turbulence_intensity_i + self.kb kz = self.ka * turbulence_intensity_i + self.kb C0 = 1 - u0 / freestream_velocity M0 = C0 * (2 - C0) E0 = ne.evaluate("C0 ** 2 - 3 * exp(1.0 / 12.0) * C0 + 3 * exp(1.0 / 3.0)") # initial Gaussian wake expansion sigma_z0 = ne.evaluate("rotor_diameter_i * 0.5 * sqrt(uR / (freestream_velocity + u0))") sigma_y0 = sigma_z0 * cosd(yaw_i) * cosd(wind_veer) # yR = y - y_i xR = x_i # yR * tand(yaw) + x_i # yaw parameters (skew angle and distance from centerline) # skew angle in radians theta_c0 = self.dm * (0.3 * np.radians(yaw_i) / cosd(yaw_i)) theta_c0 *= (1 - np.sqrt(1 - ct_i * cosd(yaw_i))) delta0 = np.tan(theta_c0) * (x0 - x_i) # initial wake deflection; # NOTE: use np.tan here since theta_c0 is radians # deflection in the near wake delta_near_wake = ((x - xR) / (x0 - xR)) * delta0 + (self.ad + self.bd * (x - x_i)) delta_near_wake *= (x >= xR) & (x <= x0) # deflection in the far wake sigma_y = ky * (x - x0) + sigma_y0 sigma_z = kz * (x - x0) + sigma_z0 sigma_y = sigma_y * (x >= x0) + sigma_y0 * (x < x0) sigma_z = sigma_z * (x >= x0) + sigma_z0 * (x < x0) M0_sqrt = np.sqrt(M0) middle_term = np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) ln_deltaNum = (1.6 + M0_sqrt) * (1.6 * middle_term - M0_sqrt) ln_deltaDen = (1.6 - M0_sqrt) * (1.6 * middle_term + M0_sqrt) middle_term = ne.evaluate( "theta_c0" " * E0" " / 5.2" " * sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0))" " * log(ln_deltaNum / ln_deltaDen)" ) delta_far_wake = delta0 + middle_term + (self.ad + self.bd * (x - x_i)) delta_far_wake = delta_far_wake * (x > x0) deflection = delta_near_wake + delta_far_wake return deflection
## GCH components
[docs] def gamma( D, velocity, Uinf, Ct, scale=1.0, ): """ Vortex circulation strength. Units of XXX TODO Args: D (float): Rotor diameter of the current turbine velocity (np.array(float)): Velocities at the current turbine Uinf (float): Free-stream velocity Ct (float): Thrust coefficient at the current turbine Returns: [type]: [description] """ # NOTE the cos commented below is included in Ct return scale * (pi / 8) * D * velocity * Uinf * Ct # * cosd(yaw)
[docs] def wake_added_yaw( u_i, v_i, u_initial, delta_y, z_i, rotor_diameter, hub_height, ct_i, tip_speed_ratio, axial_induction_i, wind_shear, scale=1.0, ): """ what yaw angle would have produced that same average spanwise velocity These calculations focus around the current turbine. The formulation could remove the dimension for n-turbines, but for consistency with other similar equations it is left. However, the turbine dimension should always have length 1. """ # turbine parameters D = rotor_diameter # scalar HH = hub_height # scalar Ct = ct_i # (findex, 1, 1, 1) for the current turbine TSR = tip_speed_ratio # scalar aI = axial_induction_i # (findex, 1, 1, 1) for the current turbine avg_v = np.mean(v_i, axis=(2,3)) # (findex, 1, grid, grid) # flow parameters Uinf = np.mean(u_initial, axis=(1, 2, 3)) Uinf = Uinf[:, None, None, None] # TODO: Allow user input for eps gain eps_gain = 0.2 eps = eps_gain * D # Use set value vel_top = ((HH + D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1)) Gamma_top = gamma( D, vel_top, Uinf, Ct, scale, ) vel_bottom = ((HH - D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1)) Gamma_bottom = -1 * gamma( D, vel_bottom, Uinf, Ct, scale, ) turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(2, 3)))[:, :, None, None] Gamma_wake_rotation = 0.25 * 2 * pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR ### compute the spanwise and vertical velocities induced by yaw # decay = eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2) # This is the decay downstream yLocs = delta_y + NUM_EPS # top vortex # NOTE: this is the top of the grid, not the top of the rotor zT = z_i - (HH + D / 2) + NUM_EPS # distance from the top of the grid rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is (-) in the paper # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") v_top = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape") v_top = np.mean( v_top, axis=(2,3) ) # w_top = (-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay # bottom vortex zB = z_i - (HH - D / 2) + NUM_EPS rB = ne.evaluate("yLocs ** 2 + zB ** 2") core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") v_bottom = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape") v_bottom = np.mean( v_bottom, axis=(2,3) ) # w_bottom = (-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay # wake rotation vortex zC = z_i - HH + NUM_EPS rC = ne.evaluate("yLocs ** 2 + zC ** 2") core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") v_core = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape") v_core = np.mean( v_core, axis=(2,3) ) # w_core = (-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay # Cap the effective yaw values between -45 and 45 degrees val = 2 * (avg_v - v_core) / (v_top + v_bottom) val = np.where(val < -1.0, -1.0, val) val = np.where(val > 1.0, 1.0, val) y = np.degrees(0.5 * np.arcsin(val)) return y[:, :, None, None]
[docs] def calculate_transverse_velocity( u_i, u_initial, dudz_initial, delta_x, delta_y, z, rotor_diameter, hub_height, yaw, ct_i, tsr_i, axial_induction_i, wind_shear, scale=1.0, ): """ Calculate transverse velocity components for all downstream turbines given the vortices at the current turbine. """ # turbine parameters D = rotor_diameter HH = hub_height Ct = ct_i TSR = tsr_i aI = axial_induction_i # flow parameters Uinf = np.mean(u_initial, axis=(1, 2, 3)) Uinf = Uinf[:, None, None, None] eps_gain = 0.2 eps = eps_gain * D # Use set value vel_top = ((HH + D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1)) Gamma_top = sind(yaw) * cosd(yaw) * gamma( D, vel_top, Uinf, Ct, scale, ) vel_bottom = ((HH - D / 2) / HH) ** wind_shear * np.ones((1, 1, 1, 1)) Gamma_bottom = -1 * sind(yaw) * cosd(yaw) * gamma( D, vel_bottom, Uinf, Ct, scale, ) turbine_average_velocity = np.cbrt(np.mean(u_i ** 3, axis=(2,3)))[:, :, None, None] Gamma_wake_rotation = 0.25 * 2 * pi * D * (aI - aI ** 2) * turbine_average_velocity / TSR ### compute the spanwise and vertical velocities induced by yaw # decay the vortices as they move downstream - using mixing length lmda = D / 8 kappa = 0.41 lm = kappa * z / (1 + kappa * z / lmda) nu = lm ** 2 * np.abs(dudz_initial) # This is the decay downstream decay = ne.evaluate("eps ** 2 / (4 * nu * delta_x / Uinf + eps ** 2)") yLocs = delta_y + NUM_EPS # top vortex zT = z - (HH + D / 2) + NUM_EPS rT = ne.evaluate("yLocs ** 2 + zT ** 2") # TODO: This is - in the paper # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions core_shape = ne.evaluate("1 - exp(-rT / (eps ** 2))") V1 = ne.evaluate("(Gamma_top * zT) / (2 * pi * rT) * core_shape * decay") W1 = ne.evaluate("(-1 * Gamma_top * yLocs) / (2 * pi * rT) * core_shape * decay") # bottom vortex zB = z - (HH - D / 2) + NUM_EPS rB = ne.evaluate("yLocs ** 2 + zB ** 2") core_shape = ne.evaluate("1 - exp(-rB / (eps ** 2))") V2 = ne.evaluate("(Gamma_bottom * zB) / (2 * pi * rB) * core_shape * decay") W2 = ne.evaluate("(-1 * Gamma_bottom * yLocs) / (2 * pi * rB) * core_shape * decay") # wake rotation vortex zC = z - HH + NUM_EPS rC = ne.evaluate("yLocs ** 2 + zC ** 2") core_shape = ne.evaluate("1 - exp(-rC / (eps ** 2))") V5 = ne.evaluate("(Gamma_wake_rotation * zC) / (2 * pi * rC) * core_shape * decay") W5 = ne.evaluate("(-1 * Gamma_wake_rotation * yLocs) / (2 * pi * rC) * core_shape * decay") ### Boundary condition - ground mirror vortex # top vortex - ground zTb = z + (HH + D / 2) + NUM_EPS rTb = ne.evaluate("yLocs ** 2 + zTb ** 2") # This looks like spanwise decay; # it defines the vortex profile in the spanwise directions core_shape = ne.evaluate("1 - exp(-rTb / (eps ** 2))") V3 = ne.evaluate("(-1 * Gamma_top * zTb) / (2 * pi * rTb) * core_shape * decay") W3 = ne.evaluate("(Gamma_top * yLocs) / (2 * pi * rTb) * core_shape * decay") # bottom vortex - ground zBb = z + (HH - D / 2) + NUM_EPS rBb = ne.evaluate("yLocs ** 2 + zBb ** 2") core_shape = ne.evaluate("1 - exp(-rBb / (eps ** 2))") V4 = ne.evaluate("(-1 * Gamma_bottom * zBb) / (2 * pi * rBb) * core_shape * decay") W4 = ne.evaluate("(Gamma_bottom * yLocs) / (2 * pi * rBb) * core_shape * decay") # wake rotation vortex - ground effect zCb = z + HH + NUM_EPS rCb = ne.evaluate("yLocs ** 2 + zCb ** 2") core_shape = ne.evaluate("1 - exp(-rCb / (eps ** 2))") V6 = ne.evaluate("(-1 * Gamma_wake_rotation * zCb) / (2 * pi * rCb) * core_shape * decay") W6 = ne.evaluate("(Gamma_wake_rotation * yLocs) / (2 * pi * rCb) * core_shape * decay") # total spanwise velocity V = V1 + V2 + V3 + V4 + V5 + V6 W = W1 + W2 + W3 + W4 + W5 + W6 # No spanwise and vertical velocity upstream of the turbine ### Original v3 implementation # V[delta_x < -1] = 0.0 # Subtract by 1 to avoid numerical issues on rotation # W[delta_x < -1] = 0.0 # Subtract by 1 to avoid numerical issues on rotation # TODO Should this be <= ? Shouldn't be adding V and W on the current turbine? ### Then we changed it to this # V[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation # W[delta_x < 0.0] = 0.0 # Subtract by 1 to avoid numerical issues on rotation ### Currently, here V = np.where(delta_x >= 0.0, V, 0.0) W = np.where(delta_x >= 0.0, W, 0.0) # TODO: Why would the say W cannot be negative? W = np.where(W >= 0, W, 0.0) return V, W
[docs] def yaw_added_turbulence_mixing( u_i, I_i, v_i, w_i, turb_v_i, turb_w_i ): # Since turbulence mixing is constant for the turbine, # use the left two dimensions only here and expand # before returning. Dimensions are (wd, ws). I_i = I_i[:, 0, 0, 0] average_u_i = np.cbrt(np.mean(u_i ** 3, axis=(1, 2, 3))) # Convert ambient turbulence intensity to TKE (eq 24) k = (average_u_i * I_i) ** 2 / (2 / 3) u_term = np.sqrt(2 * k) v_term = np.mean(v_i + turb_v_i, axis=(1, 2, 3)) w_term = np.mean(w_i + turb_w_i, axis=(1, 2, 3)) # Compute the new TKE (eq 23) k_total = 0.5 * (u_term ** 2 + v_term ** 2 + w_term ** 2) # Convert TKE back to TI I_total = np.sqrt((2 / 3) * k_total) / average_u_i # Remove ambient from total TI leaving only the TI due to mixing I_mixing = I_total - I_i return I_mixing[:, None, None, None]