Source code for floris.core.wake_velocity.empirical_gauss


from typing import Any, Dict

import numexpr as ne
import numpy as np
from attrs import define, field

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


[docs] @define class EmpiricalGaussVelocityDeficit(BaseModel): """ The Empirical Gauss velocity model has a Gaussian profile (see :cite:`bastankhah2016experimental` and :cite:`King2019Controls`) throughout and expands in a (smoothed) piecewise linear fashion. parameter_dictionary (dict): Model-specific parameters. Default values are used when a parameter is not included in `parameter_dictionary`. Possible key-value pairs include: - **wake_expansion_rates** (*list*): List of expansion rates for the Gaussian wake width. Must be of length 1 or greater. - **breakpoints_D** (*list*): List of downstream locations, specified in terms of rotor diameters, where the expansion rates go into effect. Must be one element shorter than wake_expansion_rates. May be empty. - **sigma_0_D** (*float*): Initial width of the Gaussian wake at the turbine location, specified as a multiplier of the rotor diameter. - **smoothing_length_D** (*float*): Distance over which the corners in the piece-wise linear wake expansion rate are smoothed (specified as a multiplier of the rotor diameter). - **mixing_gain_deflection** (*float*): Gain to set the increase in wake expansion due to wake-induced mixing. References: .. bibliography:: /references.bib :style: unsrt :filter: docname in docnames """ wake_expansion_rates: list = field(factory=lambda: [0.023, 0.008]) breakpoints_D: list = field(factory=lambda: [10]) sigma_0_D: float = field(default=0.28) smoothing_length_D: float = field(default=2.0) mixing_gain_velocity: float = field(default=2.0) awc_mode: str = field(default="baseline") awc_wake_exp: float = field(default=1.2) awc_wake_denominator: float = field(default=400)
[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, "wind_veer": flow_field.wind_veer } return kwargs
[docs] def function( self, x_i: np.ndarray, y_i: np.ndarray, z_i: np.ndarray, axial_induction_i: np.ndarray, deflection_field_y_i: np.ndarray, deflection_field_z_i: np.ndarray, yaw_angle_i: np.ndarray, tilt_angle_i: np.ndarray, mixing_i: np.ndarray, ct_i: np.ndarray, hub_height_i: float, rotor_diameter_i: np.ndarray, # enforces the use of the below as keyword arguments and adherence to the # unpacking of the results from prepare_function() *, x: np.ndarray, y: np.ndarray, z: np.ndarray, wind_veer: float ) -> None: """ Calculates the velocity deficits in the wake. Args: x_i (np.array): Streamwise direction grid coordinates of the ith turbine (m). y_i (np.array): Cross stream direction grid coordinates of the ith turbine (m). z_i (np.array): Vertical direction grid coordinates of the ith turbine (m) [not used]. axial_induction_i (np.array): Axial induction factor of the ith turbine (-) [not used]. deflection_field_y_i (np.array): Horizontal wake deflections due to the ith turbine's yaw misalignment (m). deflection_field_z_i (np.array): Vertical wake deflections due to the ith turbine's tilt angle (m). yaw_angle_i (np.array): Yaw angle of the ith turbine (deg). tilt_angle_i (np.array): Tilt angle of the ith turbine (deg). mixing_i (np.array): The wake-induced mixing term for the ith turbine. ct_i (np.array): Thrust coefficient for the ith turbine (-). hub_height_i (float): Hub height for the ith turbine (m). rotor_diameter_i (np.array): Rotor diameter for the ith turbine (m). x (np.array): Streamwise direction grid coordinates of the flow field domain (m). y (np.array): Cross stream direction grid coordinates of the flow field domain (m). z (np.array): Vertical direction grid coordinates of the flow field domain (m). wind_veer (np.array): Wind veer (deg). Returns: np.array: Velocity deficits (-). """ include_mirror_wake = True # Could add this as a user preference. # Only symmetric terms using yaw, but keep for consistency yaw_angle = -1 * yaw_angle_i # Initial wake widths sigma_y0 = self.sigma_0_D * rotor_diameter_i * cosd(yaw_angle) sigma_z0 = self.sigma_0_D * rotor_diameter_i * cosd(tilt_angle_i) # No specific near, far wakes in this model downstream_mask = (x > x_i + 0.1) upstream_mask = (x < x_i - 0.1) # Wake expansion in the lateral (y) and the vertical (z) # TODO: could compute shared components in sigma_z, sigma_y # with one function call. sigma_y = empirical_gauss_model_wake_width( x - x_i, self.wake_expansion_rates, [b * rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] sigma_y0, self.smoothing_length_D * rotor_diameter_i, self.mixing_gain_velocity * mixing_i, ) sigma_y[upstream_mask] = \ np.tile(sigma_y0, np.shape(sigma_y)[1:])[upstream_mask] sigma_z = empirical_gauss_model_wake_width( x - x_i, self.wake_expansion_rates, [b * rotor_diameter_i for b in self.breakpoints_D], # .flatten()[0] sigma_z0, self.smoothing_length_D * rotor_diameter_i, self.mixing_gain_velocity * mixing_i, ) sigma_z[upstream_mask] = \ np.tile(sigma_z0, np.shape(sigma_z)[1:])[upstream_mask] # 'Standard' wake component r, C = rCalt( wind_veer, sigma_y, sigma_z, y, y_i, deflection_field_y_i, deflection_field_z_i, z, hub_height_i, ct_i, yaw_angle, tilt_angle_i, rotor_diameter_i, sigma_y0, sigma_z0 ) # Normalize to match end of actuator disk model tube C = C / (8 * self.sigma_0_D**2 ) wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) if include_mirror_wake: # TODO: speed up this option by calculating various elements in # rCalt only once. # Mirror component r_mirr, C_mirr = rCalt( wind_veer, # TODO: Is veer OK with mirror wakes? sigma_y, sigma_z, y, y_i, deflection_field_y_i, deflection_field_z_i, z, -hub_height_i, # Turbine at negative hub height location ct_i, yaw_angle, tilt_angle_i, rotor_diameter_i, sigma_y0, sigma_z0 ) # Normalize to match end of actuator disk model tube C_mirr = C_mirr / (8 * self.sigma_0_D**2) # ASSUME sum-of-squares superposition for the real and mirror wakes wake_deficit = np.sqrt( wake_deficit**2 + gaussian_function(C_mirr, r_mirr, 1, np.sqrt(0.5))**2 ) velocity_deficit = wake_deficit * downstream_mask return velocity_deficit
[docs] def rCalt(wind_veer, sigma_y, sigma_z, y, y_i, delta_y, delta_z, z, HH, Ct, yaw, tilt, D, sigma_y0, sigma_z0): ## Numexpr wind_veer = np.deg2rad(wind_veer) a = ne.evaluate( "cos(wind_veer) ** 2 / (2 * sigma_y ** 2) + sin(wind_veer) ** 2 / (2 * sigma_z ** 2)" ) b = ne.evaluate( "-sin(2 * wind_veer) / (4 * sigma_y ** 2) + sin(2 * wind_veer) / (4 * sigma_z ** 2)" ) c = ne.evaluate( "sin(wind_veer) ** 2 / (2 * sigma_y ** 2) + cos(wind_veer) ** 2 / (2 * sigma_z ** 2)" ) r = ne.evaluate( "a * ( (y - y_i - delta_y) ** 2) - "+\ "2 * b * (y - y_i - delta_y) * (z - HH - delta_z) + "+\ "c * ((z - HH - delta_z) ** 2)" ) d = 1 - Ct * (sigma_y0 * sigma_z0)/(sigma_y * sigma_z) * cosd(yaw) * cosd(tilt) C = ne.evaluate("1 - sqrt(d)") return r, C
[docs] def sigmoid_integral(x, center=0, width=1): y = np.zeros_like(x) # TODO: Can this be made faster? above_smoothing_zone = (x-center) > width/2 y[above_smoothing_zone] = (x-center)[above_smoothing_zone] in_smoothing_zone = ((x-center) >= -width/2) & ((x-center) <= width/2) z = ((x-center)/width + 0.5)[in_smoothing_zone] if width.shape[0] > 1: # multiple turbine sizes width = np.broadcast_to(width, x.shape)[in_smoothing_zone] y[in_smoothing_zone] = (width*(z**6 - 3*z**5 + 5/2*z**4)).flatten() return y
[docs] def empirical_gauss_model_wake_width( x, wake_expansion_rates, breakpoints, sigma_0, smoothing_length, mixing_final, ): assert len(wake_expansion_rates) == len(breakpoints) + 1, \ "Invalid combination of wake_expansion_rates and breakpoints." sigma = (wake_expansion_rates[0] + mixing_final) * x + sigma_0 for ib, b in enumerate(breakpoints): sigma += (wake_expansion_rates[ib+1] - wake_expansion_rates[ib]) * \ sigmoid_integral(x, center=b, width=smoothing_length) return sigma
[docs] def awc_added_wake_mixing( awc_mode_i, awc_amplitude_i, awc_frequency_i, awc_wake_exp, awc_wake_denominator ): # TODO: Add TI in the mix, finetune amplitude/freq effect if (awc_mode_i == "helix").any(): return awc_amplitude_i[:,:,0,0]**awc_wake_exp/awc_wake_denominator elif (awc_mode_i == "baseline").any(): return 0 else: raise NotImplementedError( 'Active wake mixing strategies other than the `helix` mode ' 'have not yet been implemented in FLORIS.' )