Module ocean_science_utilities.wavephysics.balance.wam_tail_stress

Expand source code
import numba
import numpy as np

from ocean_science_utilities.wavephysics.balance.solvers import numba_newton_raphson

#
# ----
# Wam Cy47r1 Implementation
# ----
#
# IFS DOCUMENTATION – Cy47r1 Operational implementation 30 June 2020 - PART VII
#


@numba.njit()
def log_dimensionless_critical_height(
    x, charnock_constant, vonkarman_constant, wave_age_tuning_parameter
):
    """

    Dimensionless Critical Height according to Janssen (see IFS Documentation).
    :param x:
    :param charnock_constant:
    :param vonkarman_constant:
    :param wave_age_tuning_parameter:
    :return:
    """
    return (
        np.log(charnock_constant)
        + 2 * x
        + vonkarman_constant / (np.exp(x) + wave_age_tuning_parameter)
    )



@numba.njit()
def integrate_tail_frequency_distribution(
    lower_bound, effective_charnock, vonkarman_constant, wave_age_tuning_parameter
):
    """
    Integrate the tail of the distributions. We are integrating

    np.log(Z(Y))Z(Y)**4 / Y      for   Y0 <= Y <= 1

    where

    Y = u_* / wavespeed
    Z = charnock * Y**2 * np.exp( vonkarman_constant / ( Y + wave_age_tuning_parameter)

    The boundaries of the integral are defined as the point where the critical height is at the surface
    (Y=1) and the point where Z >= 1 ( Y = Y0).

    We follow section 5 in the WAM documentation (see below). And introduce x = np.log(Y)

    so that we integrate in effect over

    np.log(Z(x))Z(x)**4     x0 <= x <= 0

    We find x0 as the point where Z(x0) = 0.

    REFERENCE:

    IFS DOCUMENTATION – Cy47r1 Operational implementation 30 June 2020 - PART VII

    :param effective_charnock:
    :param vonkarman_constant:
    :param wave_age_tuning_parameter:
    :return:
    """

    args = (effective_charnock, vonkarman_constant, wave_age_tuning_parameter)

    # find the location of the lower boundary of the integration domain. THis is where
    # loglog_mu = 0
    x0 = numba_newton_raphson(
        log_dimensionless_critical_height, np.log(0.01), args, (-10, 0), verbose=False
    )

    log_lower_bound = np.log(lower_bound)

    if log_lower_bound > x0:
        x0 = log_lower_bound

    if x0 > 0.0:
        return 0.0

    # Define the stepsize of the integration. Since the upper boundary is 0, this is merely the start
    stepsize = -x0 / 4

    # We use Boole's rule for integration.
    evaluation_points = np.array([x0, 3 * x0 / 4, 2 * x0 / 4, x0 / 4, 0])
    log_waveage = log_dimensionless_critical_height(evaluation_points, *args)
    values = log_waveage**4 * np.exp(log_waveage)
    integrant = (
        2
        / 45
        * stepsize
        * (
            7 * values[0]
            + 32 * values[1]
            + 12 * values[2]
            + 32 * values[3]
            + 7 * values[4]
        )
    )
    return integrant


@numba.njit()
def tail_stress_parametrization_wam(
    variance_density,
    wind,
    depth,
    roughness_length,
    spectral_grid,
    parameters,
):
    vonkarman_constant = parameters["vonkarman_constant"]
    growth_parameter_betamax = parameters["growth_parameter_betamax"]
    wave_age_tuning_parameter = parameters["wave_age_tuning_parameter"]
    gravitational_acceleration = parameters["gravitational_acceleration"]
    radian_frequency = spectral_grid["radian_frequency"]
    radian_direction = spectral_grid["radian_direction"]
    elevation = parameters["elevation"]
    air_density = parameters["air_density"]

    number_of_frequencies, number_of_directions = variance_density.shape
    direction_step = spectral_grid["direction_step"]

    wind_forcing, wind_direction_degrees, wind_forcing_type = wind
    wind_direction_radian = wind_direction_degrees * np.pi / 180
    cosine_mutual_angle = np.cos(radian_direction - wind_direction_radian)
    cosine = np.cos(radian_direction)
    sine = np.sin(radian_direction)

    if wind_forcing_type == "u10":
        friction_velocity = (
            wind_forcing * vonkarman_constant / np.log(elevation / roughness_length)
        )

    elif wind_forcing_type in ["ustar", "friction_velocity"]:
        friction_velocity = wind_forcing

    else:
        raise ValueError("Unknown wind input type")

    directional_integral_last_bin_east = 0.0
    directional_integral_last_bin_north = 0.0
    for direction_index in range(0, number_of_directions):
        if cosine_mutual_angle[direction_index] <= 0.0:
            continue

        directional_integral_last_bin_east += (
            cosine_mutual_angle[direction_index] ** 2
            * cosine[direction_index]
            * variance_density[number_of_frequencies - 1, direction_index]
            * direction_step[direction_index]
        )

        directional_integral_last_bin_north += (
            cosine_mutual_angle[direction_index] ** 2
            * sine[direction_index]
            * variance_density[number_of_frequencies - 1, direction_index]
            * direction_step[direction_index]
        )

    effective_charnock = (
        roughness_length * gravitational_acceleration / friction_velocity**2
    )

    lower_bound = (
        friction_velocity
        * radian_frequency[number_of_frequencies - 1]
        / gravitational_acceleration
    )
    frequency_integral = integrate_tail_frequency_distribution(
        lower_bound, effective_charnock, vonkarman_constant, wave_age_tuning_parameter
    )

    constant = (
        radian_frequency[number_of_frequencies - 1] ** 5
        / (2 * np.pi * gravitational_acceleration**2)
        * friction_velocity**2
        * growth_parameter_betamax
        / vonkarman_constant**2
    )

    # Add contribution of cappiliary waves
    total_stress = parameters["air_density"] * friction_velocity**2
    charnock_roughness = _charnock_relation_point(friction_velocity, parameters)
    background_stress = charnock_roughness**2 / roughness_length**2 * total_stress

    eastward_stress = (
        directional_integral_last_bin_east * frequency_integral * constant * air_density
        + np.cos(wind_direction_radian) * background_stress
    )

    northward_stress = (
        directional_integral_last_bin_north
        * frequency_integral
        * constant
        * air_density
        + np.sin(wind_direction_radian) * background_stress
    )

    return eastward_stress, northward_stress


@numba.njit(cache=True)
def _charnock_relation_point(friction_velocity, parameters):
    """
    Charnock relation
    :param friction_velocity:
    :param parameters:
    :return:
    """
    roughness_length = (
        friction_velocity**2
        / parameters["gravitational_acceleration"]
        * parameters["charnock_constant"]
    )

    if roughness_length > parameters["charnock_maximum_roughness"]:
        roughness_length = parameters["charnock_maximum_roughness"]

    return roughness_length

Functions

def integrate_tail_frequency_distribution(lower_bound, effective_charnock, vonkarman_constant, wave_age_tuning_parameter)

Integrate the tail of the distributions. We are integrating

np.log(Z(Y))Z(Y)**4 / Y for Y0 <= Y <= 1

where

Y = u_ / wavespeed Z = charnock * Y*2 * np.exp( vonkarman_constant / ( Y + wave_age_tuning_parameter)

The boundaries of the integral are defined as the point where the critical height is at the surface (Y=1) and the point where Z >= 1 ( Y = Y0).

We follow section 5 in the WAM documentation (see below). And introduce x = np.log(Y)

so that we integrate in effect over

np.log(Z(x))Z(x)**4 x0 <= x <= 0

We find x0 as the point where Z(x0) = 0.

REFERENCE:

IFS DOCUMENTATION – Cy47r1 Operational implementation 30 June 2020 - PART VII

:param effective_charnock: :param vonkarman_constant: :param wave_age_tuning_parameter: :return:

Expand source code
@numba.njit()
def integrate_tail_frequency_distribution(
    lower_bound, effective_charnock, vonkarman_constant, wave_age_tuning_parameter
):
    """
    Integrate the tail of the distributions. We are integrating

    np.log(Z(Y))Z(Y)**4 / Y      for   Y0 <= Y <= 1

    where

    Y = u_* / wavespeed
    Z = charnock * Y**2 * np.exp( vonkarman_constant / ( Y + wave_age_tuning_parameter)

    The boundaries of the integral are defined as the point where the critical height is at the surface
    (Y=1) and the point where Z >= 1 ( Y = Y0).

    We follow section 5 in the WAM documentation (see below). And introduce x = np.log(Y)

    so that we integrate in effect over

    np.log(Z(x))Z(x)**4     x0 <= x <= 0

    We find x0 as the point where Z(x0) = 0.

    REFERENCE:

    IFS DOCUMENTATION – Cy47r1 Operational implementation 30 June 2020 - PART VII

    :param effective_charnock:
    :param vonkarman_constant:
    :param wave_age_tuning_parameter:
    :return:
    """

    args = (effective_charnock, vonkarman_constant, wave_age_tuning_parameter)

    # find the location of the lower boundary of the integration domain. THis is where
    # loglog_mu = 0
    x0 = numba_newton_raphson(
        log_dimensionless_critical_height, np.log(0.01), args, (-10, 0), verbose=False
    )

    log_lower_bound = np.log(lower_bound)

    if log_lower_bound > x0:
        x0 = log_lower_bound

    if x0 > 0.0:
        return 0.0

    # Define the stepsize of the integration. Since the upper boundary is 0, this is merely the start
    stepsize = -x0 / 4

    # We use Boole's rule for integration.
    evaluation_points = np.array([x0, 3 * x0 / 4, 2 * x0 / 4, x0 / 4, 0])
    log_waveage = log_dimensionless_critical_height(evaluation_points, *args)
    values = log_waveage**4 * np.exp(log_waveage)
    integrant = (
        2
        / 45
        * stepsize
        * (
            7 * values[0]
            + 32 * values[1]
            + 12 * values[2]
            + 32 * values[3]
            + 7 * values[4]
        )
    )
    return integrant
def log_dimensionless_critical_height(x, charnock_constant, vonkarman_constant, wave_age_tuning_parameter)

Dimensionless Critical Height according to Janssen (see IFS Documentation). :param x: :param charnock_constant: :param vonkarman_constant: :param wave_age_tuning_parameter: :return:

Expand source code
@numba.njit()
def log_dimensionless_critical_height(
    x, charnock_constant, vonkarman_constant, wave_age_tuning_parameter
):
    """

    Dimensionless Critical Height according to Janssen (see IFS Documentation).
    :param x:
    :param charnock_constant:
    :param vonkarman_constant:
    :param wave_age_tuning_parameter:
    :return:
    """
    return (
        np.log(charnock_constant)
        + 2 * x
        + vonkarman_constant / (np.exp(x) + wave_age_tuning_parameter)
    )
def tail_stress_parametrization_wam(variance_density, wind, depth, roughness_length, spectral_grid, parameters)
Expand source code
@numba.njit()
def tail_stress_parametrization_wam(
    variance_density,
    wind,
    depth,
    roughness_length,
    spectral_grid,
    parameters,
):
    vonkarman_constant = parameters["vonkarman_constant"]
    growth_parameter_betamax = parameters["growth_parameter_betamax"]
    wave_age_tuning_parameter = parameters["wave_age_tuning_parameter"]
    gravitational_acceleration = parameters["gravitational_acceleration"]
    radian_frequency = spectral_grid["radian_frequency"]
    radian_direction = spectral_grid["radian_direction"]
    elevation = parameters["elevation"]
    air_density = parameters["air_density"]

    number_of_frequencies, number_of_directions = variance_density.shape
    direction_step = spectral_grid["direction_step"]

    wind_forcing, wind_direction_degrees, wind_forcing_type = wind
    wind_direction_radian = wind_direction_degrees * np.pi / 180
    cosine_mutual_angle = np.cos(radian_direction - wind_direction_radian)
    cosine = np.cos(radian_direction)
    sine = np.sin(radian_direction)

    if wind_forcing_type == "u10":
        friction_velocity = (
            wind_forcing * vonkarman_constant / np.log(elevation / roughness_length)
        )

    elif wind_forcing_type in ["ustar", "friction_velocity"]:
        friction_velocity = wind_forcing

    else:
        raise ValueError("Unknown wind input type")

    directional_integral_last_bin_east = 0.0
    directional_integral_last_bin_north = 0.0
    for direction_index in range(0, number_of_directions):
        if cosine_mutual_angle[direction_index] <= 0.0:
            continue

        directional_integral_last_bin_east += (
            cosine_mutual_angle[direction_index] ** 2
            * cosine[direction_index]
            * variance_density[number_of_frequencies - 1, direction_index]
            * direction_step[direction_index]
        )

        directional_integral_last_bin_north += (
            cosine_mutual_angle[direction_index] ** 2
            * sine[direction_index]
            * variance_density[number_of_frequencies - 1, direction_index]
            * direction_step[direction_index]
        )

    effective_charnock = (
        roughness_length * gravitational_acceleration / friction_velocity**2
    )

    lower_bound = (
        friction_velocity
        * radian_frequency[number_of_frequencies - 1]
        / gravitational_acceleration
    )
    frequency_integral = integrate_tail_frequency_distribution(
        lower_bound, effective_charnock, vonkarman_constant, wave_age_tuning_parameter
    )

    constant = (
        radian_frequency[number_of_frequencies - 1] ** 5
        / (2 * np.pi * gravitational_acceleration**2)
        * friction_velocity**2
        * growth_parameter_betamax
        / vonkarman_constant**2
    )

    # Add contribution of cappiliary waves
    total_stress = parameters["air_density"] * friction_velocity**2
    charnock_roughness = _charnock_relation_point(friction_velocity, parameters)
    background_stress = charnock_roughness**2 / roughness_length**2 * total_stress

    eastward_stress = (
        directional_integral_last_bin_east * frequency_integral * constant * air_density
        + np.cos(wind_direction_radian) * background_stress
    )

    northward_stress = (
        directional_integral_last_bin_north
        * frequency_integral
        * constant
        * air_density
        + np.sin(wind_direction_radian) * background_stress
    )

    return eastward_stress, northward_stress