def axial_induction( velocities: NDArrayFloat, # (wind directions, wind speeds, turbines, grid, grid) yaw_angle: NDArrayFloat, # (wind directions, wind speeds, turbines) fCt: NDArrayObject, # (turbines) turbine_type_map: NDArrayObject, # (wind directions, 1, turbines) ix_filter: NDArrayFilter | Iterable[int] | None = None, ) -> NDArrayFloat: """Axial induction factor of the turbine incorporating the thrust coefficient and yaw angle. Args: velocities (NDArrayFloat): The velocity field at each turbine; should be shape: (number of turbines, ngrid, ngrid), or (ngrid, ngrid) for a single turbine. fCt (np.array): The thrust coefficient function for each turbine. turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition for each turbine. ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or integer indices (as an aray or iterable) to filter out before calculation. Defaults to None. Returns: Union[float, NDArrayFloat]: [description] """ if isinstance(yaw_angle, list): yaw_angle = np.array(yaw_angle) # Get Ct first before modifying any data thrust_coefficient = Ct(velocities, yaw_angle, fCt, turbine_type_map, ix_filter) # Then, process the input arguments as needed for this function ix_filter = _filter_convert(ix_filter, yaw_angle) if ix_filter is not None: yaw_angle = yaw_angle[:, :, ix_filter] return 0.5 / cosd(yaw_angle) * ( 1 - np.sqrt(1 - thrust_coefficient * cosd(yaw_angle)))
def Ct( velocities: NDArrayFloat, yaw_angle: NDArrayFloat, fCt: NDArrayObject, turbine_type_map: NDArrayObject, ix_filter: NDArrayFilter | Iterable[int] | None = None, ) -> NDArrayFloat: """Thrust coefficient of a turbine incorporating the yaw angle. The value is interpolated from the coefficient of thrust vs wind speed table using the rotor swept area average velocity. Args: velocities (NDArrayFloat[wd, ws, turbines, grid1, grid2]): The velocity field at a turbine. yaw_angle (NDArrayFloat[wd, ws, turbines]): The yaw angle for each turbine. fCt (NDArrayObject[wd, ws, turbines]): The thrust coefficient for each turbine. turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition for each turbine. ix_filter (NDArrayFilter | Iterable[int] | None, optional): The boolean array, or integer indices as an iterable of array to filter out before calculation. Defaults to None. Returns: NDArrayFloat: Coefficient of thrust for each requested turbine. """ if isinstance(yaw_angle, list): yaw_angle = np.array(yaw_angle) # Down-select inputs if ix_filter is given if ix_filter is not None: ix_filter = _filter_convert(ix_filter, yaw_angle) velocities = velocities[:, :, ix_filter] yaw_angle = yaw_angle[:, :, ix_filter] turbine_type_map = turbine_type_map[:, :, ix_filter] average_velocities = average_velocity(velocities) # Loop over each turbine type given to get thrust coefficient for all turbines thrust_coefficient = np.zeros(np.shape(average_velocities)) fCt = dict(fCt) turb_types = np.unique(turbine_type_map) for turb_type in turb_types: # Using a masked array, apply the thrust coefficient for all turbines of the current # type to the main thrust coefficient array thrust_coefficient += fCt[turb_type](average_velocities) * np.array( turbine_type_map == turb_type) thrust_coefficient = np.clip(thrust_coefficient, 0.0001, 0.9999) effective_thrust = thrust_coefficient * cosd(yaw_angle) return effective_thrust
def rC(wind_veer, sigma_y, sigma_z, y, y_i, delta, z, HH, Ct, yaw, D): ## original # a = cosd(wind_veer) ** 2 / (2 * sigma_y ** 2) + sind(wind_veer) ** 2 / (2 * sigma_z ** 2) # b = -sind(2 * wind_veer) / (4 * sigma_y ** 2) + sind(2 * wind_veer) / (4 * sigma_z ** 2) # c = sind(wind_veer) ** 2 / (2 * sigma_y ** 2) + cosd(wind_veer) ** 2 / (2 * sigma_z ** 2) # r = ( # a * (y - y_i - delta) ** 2 # - 2 * b * (y - y_i - delta) * (z - HH) # + c * (z - HH) ** 2 # ) # C = 1 - np.sqrt( np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / D ** 2)), 0.0, 1.0) ) ## Precalculate some parts # twox_sigmay_2 = 2 * sigma_y ** 2 # twox_sigmaz_2 = 2 * sigma_z ** 2 # a = cosd(wind_veer) ** 2 / (twox_sigmay_2) + sind(wind_veer) ** 2 / (twox_sigmaz_2) # b = -sind(2 * wind_veer) / (2 * twox_sigmay_2) + sind(2 * wind_veer) / (2 * twox_sigmaz_2) # c = sind(wind_veer) ** 2 / (twox_sigmay_2) + cosd(wind_veer) ** 2 / (twox_sigmaz_2) # delta_y = y - y_i - delta # delta_z = z - HH # r = (a * (delta_y ** 2) - 2 * b * (delta_y) * (delta_z) + c * (delta_z ** 2)) # C = 1 - np.sqrt( np.clip(1 - (Ct * cosd(yaw) / ( 8.0 * sigma_y * sigma_z / (D * D) )), 0.0, 1.0) ) ## 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) ** 2) - 2 * b * (y - y_i - delta) * (z - HH) + c * ((z - HH) ** 2)" ) d = np.clip(1 - (Ct * cosd(yaw) / (8.0 * sigma_y * sigma_z / (D * D))), 0.0, 1.0) C = ne.evaluate("1 - sqrt(d)") return r, C
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: np.ndarray, *, x: np.ndarray, ): """ Calcualtes the deflection field of the wake in relation to the yaw of the turbine. This is coded as defined in [1]. Args: x_locations (np.array): streamwise locations in wake y_locations (np.array): spanwise locations in wake z_locations (np.array): vertical locations in wake (not used in Jiménez) turbine (:py:class:`floris.simulation.turbine.Turbine`): Turbine object coord (:py:meth:`floris.simulation.turbine_map.TurbineMap.coords`): Spatial coordinates of wind turbine. flow_field (:py:class:`floris.simulation.flow_field.FlowField`): Flow field object. Returns: deflection (np.array): Deflected wake centerline. This function calculates the deflection of the entire flow field given the yaw angle and Ct of the current turbine """ # NOTE: Its important to remember the rules of broadcasting here. # An operation between two np.arrays of different sizes involves # broadcasting. First, the rank and then the dimensions are compared. # If the ranks are different, new dimensions of size 1 are added to # the missing dimensions. Then, arrays can be combined (arithmetic) # if corresponding dimensions are either the same size or 1. # https://numpy.org/doc/stable/user/basics.broadcasting.html # Here, many dimensions are 1, but these are essentially treated # as a scalar value for that dimension. # angle of deflection xi_init = cosd(yaw_i) * sind(yaw_i) * ct_i / 2.0 """ delta_x = x - x_i # yaw displacement A = 15 * (2 * self.kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0 B = (30 * self.kd / rotor_diameter_i) * ( 2 * self.kd * delta_x / rotor_diameter_i + 1 ) ** 5.0 C = xi_init * rotor_diameter_i * (15 + xi_init ** 2.0) D = 30 * self.kd yYaw_init = (xi_init * A / B) - (C / D) # corrected yaw displacement with lateral offset # This has the same shape as the grid deflection = yYaw_init + self.ad + self.bd * delta_x """ # Numexpr - do not change below without corresponding changes above. kd = self.kd ad = self.ad bd = self.bd delta_x = ne.evaluate("x - x_i") A = ne.evaluate( "15 * (2 * kd * delta_x / rotor_diameter_i + 1) ** 4.0 + xi_init ** 2.0" ) B = ne.evaluate( "(30 * kd / rotor_diameter_i) * ( 2 * kd * delta_x / rotor_diameter_i + 1 ) ** 5.0" ) C = ne.evaluate("xi_init * rotor_diameter_i * (15 + xi_init ** 2.0)") D = ne.evaluate("30 * kd") yYaw_init = ne.evaluate("(xi_init * A / B) - (C / D)") deflection = ne.evaluate("yYaw_init + ad + bd * delta_x") return deflection
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=(3, 4))) 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=(3, 4)) x_i_loc = x_i_loc[:, :, :, None, None] y_i_loc = np.mean(y_i, axis=(3, 4)) y_i_loc = y_i_loc[:, :, :, None, None] z_i_loc = np.mean(z_i, axis=(3, 4)) z_i_loc = z_i_loc[:, :, :, None, None] x_coord = np.mean(x, axis=(3, 4))[:, :, :, None, None] y_loc = y y_coord = np.mean(y, axis=(3, 4))[:, :, :, None, None] z_loc = z # np.mean(z, axis=(3,4)) z_coord = np.mean(z, axis=(3, 4))[:, :, :, 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 crossplanes, we don't need to compute downstream # turbines from out crossplane 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 / 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) / 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 C = a1 - np.sqrt(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))) 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 * np.array(x - xR >= 0.1) turb_u_wake = turb_u_wake + turb_avg_vels * velDef return ( turb_u_wake, Ctmp, )
def test_cosd(): assert pytest.approx(cosd(0.0)) == 1.0 assert pytest.approx(cosd(90.0)) == 0.0 assert pytest.approx(cosd(180.0)) == -1.0 assert pytest.approx(cosd(270.0)) == 0.0
def function( self, x_i: np.ndarray, y_i: np.ndarray, z_i: np.ndarray, axial_induction_i: np.ndarray, deflection_field_i: np.ndarray, yaw_angle_i: np.ndarray, turbulence_intensity_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, u_initial: np.ndarray, wind_veer: float) -> None: # yaw_angle is all turbine yaw angles for each wind speed # Extract and broadcast only the current turbine yaw setting # for all wind speeds # Opposite sign convention in this model yaw_angle = -1 * yaw_angle_i # Initialize the velocity deficit uR = u_initial * ct_i / (2.0 * (1 - np.sqrt(1 - ct_i))) u0 = u_initial * np.sqrt(1 - ct_i) # Initial lateral bounds sigma_z0 = rotor_diameter_i * 0.5 * np.sqrt(uR / (u_initial + u0)) sigma_y0 = sigma_z0 * cosd(yaw_angle) * cosd(wind_veer) # Compute the bounds of the near and far wake regions and a mask # Start of the near wake xR = x_i # Start of the far wake x0 = np.ones_like(u_initial) x0 *= rotor_diameter_i * cosd(yaw_angle) * (1 + np.sqrt(1 - ct_i)) x0 /= np.sqrt(2) * (4 * self.alpha * turbulence_intensity_i + 2 * self.beta * (1 - np.sqrt(1 - ct_i))) x0 += x_i # Initialize the velocity deficit array velocity_deficit = np.zeros_like(u_initial) # Masks # When we have only an inequality, the current turbine may be applied its own wake in cases where numerical precision # cause in incorrect comparison. We've applied a small bump to avoid this. "0.1" is arbitrary but it is a small, non zero value. near_wake_mask = np.array(x > xR + 0.1) * np.array( x < x0 ) # This mask defines the near wake; keeps the areas downstream of xR and upstream of x0 far_wake_mask = np.array(x >= x0) # Compute the velocity deficit in the NEAR WAKE region # ONLY If there are points within the near wake boundary # TODO: for the turbinegrid, do we need to do this near wake calculation at all? # same question for any grid with a resolution larger than the near wake region if np.sum(near_wake_mask): # Calculate the wake expansion near_wake_ramp_up = (x - xR) / ( x0 - xR ) # This is a linear ramp from 0 to 1 from the start of the near wake to the start of the far wake. near_wake_ramp_down = (x0 - x) / ( x0 - xR ) # Another linear ramp, but positive upstream of the far wake and negative in the far wake; 0 at the start of the far wake # near_wake_ramp_down = -1 * (near_wake_ramp_up - 1) # TODO: this is equivalent, right? sigma_y = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt( ct_i / 2.0) + near_wake_ramp_up * sigma_y0 sigma_y = sigma_y * np.array(x >= xR) + np.ones_like( sigma_y) * np.array(x < xR) * 0.5 * rotor_diameter_i sigma_z = near_wake_ramp_down * 0.501 * rotor_diameter_i * np.sqrt( ct_i / 2.0) + near_wake_ramp_up * sigma_z0 sigma_z = sigma_z * np.array(x >= xR) + np.ones_like( sigma_z) * np.array(x < xR) * 0.5 * rotor_diameter_i r, C = rC(wind_veer, sigma_y, sigma_z, y, y_i, deflection_field_i, z, hub_height_i, ct_i, yaw_angle, rotor_diameter_i) near_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) near_wake_deficit *= near_wake_mask velocity_deficit += near_wake_deficit # Compute the velocity deficit in the FAR WAKE region if np.sum(far_wake_mask): # Wake expansion in the lateral (y) and the vertical (z) ky = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters kz = self.ka * turbulence_intensity_i + self.kb # wake expansion parameters sigma_y = (ky * (x - x0) + sigma_y0) * far_wake_mask + sigma_y0 * np.array(x < x0) sigma_z = (kz * (x - x0) + sigma_z0) * far_wake_mask + sigma_z0 * np.array(x < x0) r, C = rC(wind_veer, sigma_y, sigma_z, y, y_i, deflection_field_i, z, hub_height_i, ct_i, yaw_angle, rotor_diameter_i) far_wake_deficit = gaussian_function(C, r, 1, np.sqrt(0.5)) far_wake_deficit *= far_wake_mask velocity_deficit += far_wake_deficit return velocity_deficit
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_locations (np.array): An array of floats that contains the streamwise direction grid coordinates of the flow field domain (m). y_locations (np.array): An array of floats that contains the grid coordinates of the flow field domain in the direction normal to x and parallel to the ground (m). z_locations (np.array): An array of floats that contains the grid coordinates of the flow field domain in the vertical direction (m). turbine (:py:obj:`floris.simulation.turbine`): Object that represents the turbine creating the wake. coord (:py:obj:`floris.utilities.Vec3`): Object containing the coordinate of the turbine creating the wake (m). flow_field (:py:class:`floris.simulation.flow_field`): Object containing the flow field information for the wind farm. Returns: np.array: Deflection field for the wake. """ # ============================================================== # Opposite sign convention in this model yaw_i = -1 * yaw_i # 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 = C0**2 - 3 * np.exp(1.0 / 12.0) * C0 + 3 * np.exp(1.0 / 3.0) # initial Gaussian wake expansion sigma_z0 = rotor_diameter_i * 0.5 * np.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)) * ( 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 = delta_near_wake * np.array(x >= xR) delta_near_wake = delta_near_wake * np.array(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 * np.array(x >= x0) + sigma_y0 * np.array(x < x0) sigma_z = sigma_z * np.array(x >= x0) + sigma_z0 * np.array(x < x0) ln_deltaNum = (1.6 + np.sqrt(M0)) * ( 1.6 * np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) - np.sqrt(M0)) ln_deltaDen = (1.6 - np.sqrt(M0)) * ( 1.6 * np.sqrt(sigma_y * sigma_z / (sigma_y0 * sigma_z0)) + np.sqrt(M0)) delta_far_wake = (delta0 + theta_c0 * E0 / 5.2 * np.sqrt(sigma_y0 * sigma_z0 / (ky * kz * M0)) * np.log(ln_deltaNum / ln_deltaDen) + (self.ad + self.bd * (x - x_i))) delta_far_wake = delta_far_wake * np.array(x > x0) deflection = delta_near_wake + delta_far_wake return deflection
def calculate_transverse_velocity(u_i, u_initial, delta_x, delta_y, z, rotor_diameter, hub_height, yaw, ct_i, tsr_i, axial_induction_i, 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=(2, 3, 4)) Uinf = Uinf[:, :, None, None, None] eps_gain = 0.2 eps = eps_gain * D # Use set value # TODO: wind sheer is hard-coded here but should be connected to the input vel_top = ((HH + D / 2) / HH)**0.12 * np.ones((1, 1, 1, 1, 1)) Gamma_top = sind(yaw) * cosd(yaw) * gamma( D, vel_top, Uinf, Ct, scale, ) vel_bottom = ((HH - D / 2) / HH)**0.12 * np.ones((1, 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=(3, 4))) turbine_average_velocity = turbine_average_velocity[:, :, :, None, None] Gamma_wake_rotation = 0.25 * 2 * np.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) # TODO: get this from the z input? z_basis = np.linspace(np.min(z), np.max(z), np.shape(u_initial)[4]) dudz_initial = np.gradient(u_initial, z_basis, axis=4) nu = lm**2 * np.abs(dudz_initial) decay = eps**2 / (4 * nu * delta_x / Uinf + eps**2 ) # This is the decay downstream yLocs = delta_y + BaseModel.NUM_EPS # top vortex zT = z - (HH + D / 2) + BaseModel.NUM_EPS rT = yLocs**2 + zT**2 # TODO: This is - in the paper core_shape = 1 - np.exp( -rT / (eps**2) ) # This looks like spanwise decay - it defines the vortex profile in the spanwise directions V1 = (Gamma_top * zT) / (2 * np.pi * rT) * core_shape * decay W1 = (-1 * Gamma_top * yLocs) / (2 * np.pi * rT) * core_shape * decay # bottom vortex zB = z - (HH - D / 2) + BaseModel.NUM_EPS rB = yLocs**2 + zB**2 core_shape = 1 - np.exp(-rB / (eps**2)) V2 = (Gamma_bottom * zB) / (2 * np.pi * rB) * core_shape * decay W2 = (-1 * Gamma_bottom * yLocs) / (2 * np.pi * rB) * core_shape * decay # wake rotation vortex zC = z - HH + BaseModel.NUM_EPS rC = yLocs**2 + zC**2 core_shape = 1 - np.exp(-rC / (eps**2)) V5 = (Gamma_wake_rotation * zC) / (2 * np.pi * rC) * core_shape * decay W5 = (-1 * Gamma_wake_rotation * yLocs) / (2 * np.pi * rC) * core_shape * decay ### Boundary condition - ground mirror vortex # top vortex - ground zTb = z + (HH + D / 2) + BaseModel.NUM_EPS rTb = yLocs**2 + zTb**2 core_shape = 1 - np.exp( -rTb / (eps**2) ) # This looks like spanwise decay - it defines the vortex profile in the spanwise directions V3 = (-1 * Gamma_top * zTb) / (2 * np.pi * rTb) * core_shape * decay W3 = (Gamma_top * yLocs) / (2 * np.pi * rTb) * core_shape * decay # bottom vortex - ground zBb = z + (HH - D / 2) + BaseModel.NUM_EPS rBb = yLocs**2 + zBb**2 core_shape = 1 - np.exp(-rBb / (eps**2)) V4 = (-1 * Gamma_bottom * zBb) / (2 * np.pi * rBb) * core_shape * decay W4 = (Gamma_bottom * yLocs) / (2 * np.pi * rBb) * core_shape * decay # wake rotation vortex - ground effect zCb = z + HH + BaseModel.NUM_EPS rCb = yLocs**2 + zCb**2 core_shape = 1 - np.exp(-rCb / (eps**2)) V6 = (-1 * Gamma_wake_rotation * zCb) / (2 * np.pi * rCb) * core_shape * decay W6 = (Gamma_wake_rotation * yLocs) / (2 * np.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 # 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? 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 # TODO: Why would the say W cannot be negative? W[W < 0] = 0 return V, W
def power( air_density: float, velocities: NDArrayFloat, yaw_angle: NDArrayFloat, pP: float, power_interp: NDArrayObject, turbine_type_map: NDArrayObject, ix_filter: NDArrayInt | Iterable[int] | None = None, ) -> NDArrayFloat: """Power produced by a turbine adjusted for yaw and tilt. Value given in Watts. Args: air_density (NDArrayFloat[wd, ws, turbines]): The air density value(s) at each turbine. velocities (NDArrayFloat[wd, ws, turbines, grid1, grid2]): The velocity field at a turbine. pP (NDArrayFloat[wd, ws, turbines]): The pP value(s) of the cosine exponent relating the yaw misalignment angle to power for each turbine. power_interp (NDArrayObject[wd, ws, turbines]): The power interpolation function for each turbine. turbine_type_map: (NDArrayObject[wd, ws, turbines]): The Turbine type definition for each turbine. ix_filter (NDArrayInt, optional): The boolean array, or integer indices to filter out before calculation. Defaults to None. Returns: NDArrayFloat: The power, in Watts, for each turbine after adjusting for yaw and tilt. """ # TODO: Change the order of input arguments to be consistent with the other # utility functions - velocities first... # Update to power calculation which replaces the fixed pP exponent with # an exponent pW, that changes the effective wind speed input to the power # calculation, rather than scaling the power. This better handles power # loss to yaw in above rated conditions # # based on the paper "Optimising yaw control at wind farm level" by # Ervin Bossanyi # TODO: check this - where is it? # P = 1/2 rho A V^3 Cp # NOTE: The below has a trivial performance hit for floats being passed (3.4% longer # on a meaningless test), but is actually faster when an array is passed through # That said, it adds overhead to convert the floats to 1-D arrays, so I don't # recommend just converting all values to arrays if isinstance(yaw_angle, list): yaw_angle = np.array(yaw_angle) # Down-select inputs if ix_filter is given if ix_filter is not None: ix_filter = _filter_convert(ix_filter, yaw_angle) velocities = velocities[:, :, ix_filter] yaw_angle = yaw_angle[:, :, ix_filter] pP = pP[:, :, ix_filter] turbine_type_map = turbine_type_map[:, :, ix_filter] # Compute the yaw effective velocity pW = pP / 3.0 # Convert from pP to w yaw_effective_velocity = ((air_density / 1.225)**( 1 / 3)) * average_velocity(velocities) * cosd(yaw_angle)**pW # Loop over each turbine type given to get thrust coefficient for all turbines p = np.zeros(np.shape(yaw_effective_velocity)) power_interp = dict(power_interp) turb_types = np.unique(turbine_type_map) for turb_type in turb_types: # Using a masked array, apply the thrust coefficient for all turbines of the current # type to the main thrust coefficient array p += power_interp[turb_type](yaw_effective_velocity) * np.array( turbine_type_map == turb_type) return p * 1.225