Source code for floris.core.wake_velocity.cumulative_gauss_curl


from typing import Any, Dict

import numpy as np
from attrs import define, field
from scipy.special import gamma

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


[docs] @define class CumulativeGaussCurlVelocityDeficit(BaseModel): """ The cumulative curl model is an implementation of the model described in :cite:`cc-bay_2022`, which itself is based on the cumulative model of :cite:`cc-bastankhah_2021`. References: .. bibliography:: /references.bib :style: unsrt :filter: docname in docnames :keyprefix: cc- """ a_s: float = field(default=0.179367259) b_s: float = field(default=0.0118889215) c_s1: float = field(default=0.0563691592) c_s2: float = field(default=0.13290157) a_f: float = field(default=3.11) b_f: float = field(default=-0.68) c_f: float = field(default=2.41) alpha_mod: float = field(default=1.0)
[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, "u_initial": flow_field.u_initial_sorted, } return kwargs
[docs] def function( self, ii: int, x_i: np.ndarray, y_i: np.ndarray, z_i: np.ndarray, u_i: np.ndarray, deflection_field: np.ndarray, yaw_i: np.ndarray, turbulence_intensity: np.ndarray, ct: np.ndarray, turbine_diameter: np.ndarray, turb_u_wake: np.ndarray, Ctmp: 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, u_initial: np.ndarray, ) -> None: turbine_Ct = ct turbine_ti = turbulence_intensity turbine_yaw = yaw_i # TODO Should this be cbrt? This is done to match v2 turb_avg_vels = np.cbrt(np.mean(u_i ** 3, axis=(2, 3))) turb_avg_vels = turb_avg_vels[:, :, None, None] delta_x = x - x_i sigma_n = wake_expansion( delta_x, turbine_Ct[:, ii:ii+1], turbine_ti[:, ii:ii+1], turbine_diameter[:, ii:ii+1], self.a_s, self.b_s, self.c_s1, self.c_s2, ) x_i_loc = np.mean(x_i, axis=(2, 3)) x_i_loc = x_i_loc[:, :, None, None] y_i_loc = np.mean(y_i, axis=(2, 3)) y_i_loc = y_i_loc[:, :, None, None] z_i_loc = np.mean(z_i, axis=(2, 3)) z_i_loc = z_i_loc[:, :, None, None] x_coord = np.mean(x, axis=(2, 3))[:, :, None, None] y_loc = y y_coord = np.mean(y, axis=(2, 3))[:, :, None, None] z_loc = z # np.mean(z, axis=(3,4)) z_coord = np.mean(z, axis=(2, 3))[:, :, None, None] sum_lbda = np.zeros_like(u_initial) for m in range(0, ii - 1): x_coord_m = x_coord[:, m:m+1] y_coord_m = y_coord[:, m:m+1] z_coord_m = z_coord[:, m:m+1] # For computing cross planes, we don't need to compute downstream # turbines from out cross plane position. if x_coord[:, m:m+1].size == 0: break delta_x_m = x - x_coord_m sigma_i = wake_expansion( delta_x_m, turbine_Ct[:, m:m+1], turbine_ti[:, m:m+1], turbine_diameter[:, m:m+1], self.a_s, self.b_s, self.c_s1, self.c_s2, ) S_i = sigma_n ** 2 + sigma_i ** 2 Y_i = (y_i_loc - y_coord_m - deflection_field) ** 2 / (2 * S_i) Z_i = (z_i_loc - z_coord_m) ** 2 / (2 * S_i) lbda = 1.0 * sigma_i ** 2 / S_i * np.exp(-Y_i) * np.exp(-Z_i) sum_lbda = sum_lbda + lbda * (Ctmp[m] / u_initial) # Vectorized version of sum_lbda calc; has issues with y_coord (needs to be # down-selected appropriately. Prelim. timings show vectorized form takes # longer than for loop.) # if ii >= 2: # S = sigma_n ** 2 + sigma_i[0:ii-1, :, :, :, :, :] ** 2 # Y = (y_i_loc - y_coord - deflection_field) ** 2 / (2 * S) # Z = (z_i_loc - z_coord) ** 2 / (2 * S) # lbda = self.alpha_mod * sigma_i[0:ii-1, :, :, :, :, :] ** 2 # lbda /= S * np.exp(-Y) * np.exp(-Z) # sum_lbda = np.sum(lbda * (Ctmp[0:ii-1, :, :, :, :, :] / u_initial), axis=0) # else: # sum_lbda = 0.0 # sigma_i[ii] = sigma_n # blondel # super gaussian # b_f = self.b_f1 * np.exp(self.b_f2 * TI) + self.b_f3 x_tilde = np.abs(delta_x) / turbine_diameter[:,ii:ii+1] r_tilde = np.sqrt( (y_loc - y_i_loc - deflection_field) ** 2 + (z_loc - z_i_loc) ** 2 ) r_tilde /= turbine_diameter[:,ii:ii+1] n = self.a_f * np.exp(self.b_f * x_tilde) + self.c_f a1 = 2 ** (2 / n - 1) a2 = 2 ** (4 / n - 2) # based on Blondel model, modified to include cumulative effects tmp = a2 - ( (n * turbine_Ct[:, ii:ii+1]) * cosd(turbine_yaw) / ( 16.0 * gamma(2 / n) * np.sign(sigma_n) * (np.abs(sigma_n) ** (4 / n)) * (1 - sum_lbda) ** 2 ) ) # for some low wind speeds, tmp can become slightly negative, which causes NANs, # so replace the slightly negative values with zeros tmp = tmp * (tmp >= 0) C = a1 - np.sqrt(tmp) C = C * (1 - sum_lbda) Ctmp[ii] = C yR = y_loc - y_i_loc xR = yR * tand(turbine_yaw) + x_i # add turbines together velDef = C * np.exp((-1 * r_tilde ** n) / (2 * sigma_n ** 2)) velDef = velDef * (x - xR >= 0.1) turb_u_wake = turb_u_wake + turb_avg_vels * velDef return (turb_u_wake, Ctmp)
[docs] def wake_expansion( delta_x, ct_i, turbulence_intensity_i, rotor_diameter, a_s, b_s, c_s1, c_s2, ): # Calculate Beta (Eq 10, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) beta = 0.5 * (1.0 + np.sqrt(1.0 - ct_i)) / np.sqrt(1.0 - ct_i) k = a_s * turbulence_intensity_i + b_s eps = (c_s1 * ct_i + c_s2) * np.sqrt(beta) # Calculate sigma_tilde (Eq 9, pp 5 of ref. [1] and table 4 of ref. [2] in docstring) x_tilde = np.abs(delta_x) / rotor_diameter sigma_y = k * x_tilde + eps # [added dimension to get upstream values, empty, wd, ws, x, y, z ] # return sigma_y[na, :, :, :, :, :, :] # Do this ^^ in the main function return sigma_y