Пример #1
0
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)))
Пример #2
0
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
Пример #3
0
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
Пример #4
0
    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
Пример #5
0
    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,
        )
Пример #6
0
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
Пример #7
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
Пример #8
0
    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
Пример #9
0
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
Пример #10
0
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