Module ocean_science_utilities.wavetheory.lineardispersion

Contents: Routines to calculate (inverse) linear dispersion relation and some related quantities such as phase and group velocity. NOTE: the effect of surface currents is currently not included in these calculations.

The implementation uses numba to speed up calculations. Consequently, all functions are compiled to machine code, but the first call to a function will be slow. Subsequent calls will be much faster.

Copyright (C) 2023 Sofar Ocean Technologies

Authors: Pieter Bart Smit

Functions: - intrinsic_dispersion_relation(), calculate angular frequency for a given wavenumber and depth - inverse_intrinsic_dispersion_relation(), calculate wavenumber for a given angularfrequency and depth - intrinsic_group_velocity(), calculate the group velocity given wave number and depth - phase_velocity(), calculate the phase velocity given wave number and depth - ratio_of_group_to_phase_velocity, calculate the ratio of group to phase velocity given wave number and depth - jacobian_wavenumber_to_radial_frequency(), calculate the Jacobian of the wavenumber to radial frequency transformation - jacobian_radial_frequency_to_wavenumber(), calculate the Jacobian of the radial frequency to wavenumber transformation

Expand source code
"""
Contents: Routines to calculate (inverse) linear dispersion relation and some related
quantities such as phase and group velocity. NOTE: the effect of surface currents is
currently not included in these calculations.

The implementation uses numba to speed up calculations. Consequently, all functions
are compiled to machine code, but the first call to a function will be slow.
Subsequent calls will be much faster.

Copyright (C) 2023
Sofar Ocean Technologies

Authors: Pieter Bart Smit
======================

Functions:
- `intrinsic_dispersion_relation`,
    calculate angular frequency for a given wavenumber and depth
- `inverse_intrinsic_dispersion_relation`,
    calculate wavenumber for a given angularfrequency and depth
- `intrinsic_group_velocity`,
    calculate the group velocity given wave number and depth
- `phase_velocity`,
    calculate the phase velocity given wave number and depth
- `ratio_of_group_to_phase_velocity`,
    calculate the ratio of group to phase velocity given wave number and depth
- `jacobian_wavenumber_to_radial_frequency`,
    calculate the Jacobian of the wavenumber to radial frequency transformation
- `jacobian_radial_frequency_to_wavenumber`,
    calculate the Jacobian of the radial frequency to wavenumber transformation
"""

import numpy as np

from numba import jit  # type: ignore
from numbers import Real
from typing import Union

from ocean_science_utilities.wavetheory.wavetheory_tools import atleast_1d
from ocean_science_utilities.wavetheory.constants import GRAV, numba_default


@jit(**numba_default)
def inverse_intrinsic_dispersion_relation(
    angular_frequency: Union[Real, np.ndarray],
    dep: Union[Real, np.ndarray],
    grav: float = GRAV,
    maximum_number_of_iterations: int = 10,
    tolerance: float = 1e-3,
) -> np.ndarray:
    """
    Find wavenumber k for a given radial frequency w using Newton Iteration.
    Exit when either maximum number of iterations is reached, or tolerance
    is achieved. Typically only 1 to 2 iterations are needed.

    :param w: radial frequency
    :param dep: depth in meters
    :param grav:  gravitational acceleration
    :param maximum_number_of_iterations: maximum number of iterations
    :param tolerance: relative accuracy
    :return: The wavenumber as a numpy array.
    """

    # Numba does not recognize "atleast_1d" for scalars
    w = atleast_1d(angular_frequency)

    k_deep_water_estimate = w**2 / grav
    k_shallow_water_estimate = w / np.sqrt(grav * dep)

    # == FIRST GUESS==
    # Use the intersection between shallow and deep water estimates to guestimate
    # which relation to use
    wavenumber_estimate = np.where(
        w > np.sqrt(grav / dep), k_deep_water_estimate, k_shallow_water_estimate
    )

    # == Newton Iteration ==
    error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w
    for ii in range(0, maximum_number_of_iterations):
        # Calculate the derivative of the error function with respect to wavenumber.
        # To note: the derivative is merely the group velocity.
        kd = wavenumber_estimate * dep
        error_derivative_to_wavenumber = np.where(
            kd > 5,
            0.5 * w / wavenumber_estimate,
            (1 / 2 + kd / np.sinh(2 * kd)) * w / wavenumber_estimate,
        )
        # Newton Iteration
        wavenumber_estimate = (
            wavenumber_estimate - error / error_derivative_to_wavenumber
        )

        # Update error
        error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w

        # Check for convergence
        relative_absolute_error = np.abs(error) / w
        if np.all(relative_absolute_error < tolerance):
            break
    else:
        print(
            "inverse_intrinsic_dispersion_relation::"
            " No convergence in solving for wavenumber"
        )

    return wavenumber_estimate


@jit(**numba_default)
def intrinsic_dispersion_relation(
    k: np.ndarray, dep: Union[Real, np.ndarray], grav: float = GRAV
) -> np.ndarray:
    """
    The intrinsic dispersion relation for linear waves in water of constant depth
    that relates the specific angular frequency to a given wavenumber and depth
    in a reference frame following mean ambient flow.

    Wavenumber may be a scalar or a numpy array. The function always returns
    a numpy array. If depth is specified as a numpy array it must have the same
    shape as the wavenumber array.

    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return: Intrinsic angular frequency (rad/s)
    """
    k = atleast_1d(k)
    return np.sqrt(grav * k * np.tanh(k * dep))


@jit(**numba_default)
def phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return intrinsic_dispersion_relation(k, depth, grav=grav) / k


@jit(**numba_default)
def ratio_group_velocity_to_phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    kd = k * depth
    return np.where(kd > 5, 0.5, 0.5 + kd / np.sinh(2 * kd))


@jit(**numba_default)
def intrinsic_group_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return ratio_group_velocity_to_phase_velocity(k, depth, grav=grav) * phase_velocity(
        k, depth, grav
    )


@jit(**numba_default)
def jacobian_wavenumber_to_radial_frequency(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return 1 / intrinsic_group_velocity(k, depth, grav)


@jit(**numba_default)
def jacobian_radial_frequency_to_wavenumber(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return intrinsic_group_velocity(k, depth, grav)


# Aliasses based on common notation in linear wave theory
c = phase_velocity
cg = intrinsic_group_velocity
k = inverse_intrinsic_dispersion_relation
w = intrinsic_dispersion_relation
n = ratio_group_velocity_to_phase_velocity

Functions

def c(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return intrinsic_dispersion_relation(k, depth, grav=grav) / k
def cg(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def intrinsic_group_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return ratio_group_velocity_to_phase_velocity(k, depth, grav=grav) * phase_velocity(
        k, depth, grav
    )
def intrinsic_dispersion_relation(k: numpy.ndarray, dep: Union[numbers.Real, numpy.ndarray], grav: float = 9.81) ‑> numpy.ndarray

The intrinsic dispersion relation for linear waves in water of constant depth that relates the specific angular frequency to a given wavenumber and depth in a reference frame following mean ambient flow.

Wavenumber may be a scalar or a numpy array. The function always returns a numpy array. If depth is specified as a numpy array it must have the same shape as the wavenumber array.

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return: Intrinsic angular frequency (rad/s)

Expand source code
@jit(**numba_default)
def intrinsic_dispersion_relation(
    k: np.ndarray, dep: Union[Real, np.ndarray], grav: float = GRAV
) -> np.ndarray:
    """
    The intrinsic dispersion relation for linear waves in water of constant depth
    that relates the specific angular frequency to a given wavenumber and depth
    in a reference frame following mean ambient flow.

    Wavenumber may be a scalar or a numpy array. The function always returns
    a numpy array. If depth is specified as a numpy array it must have the same
    shape as the wavenumber array.

    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return: Intrinsic angular frequency (rad/s)
    """
    k = atleast_1d(k)
    return np.sqrt(grav * k * np.tanh(k * dep))
def intrinsic_group_velocity(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def intrinsic_group_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return ratio_group_velocity_to_phase_velocity(k, depth, grav=grav) * phase_velocity(
        k, depth, grav
    )
def inverse_intrinsic_dispersion_relation(angular_frequency: Union[numbers.Real, numpy.ndarray], dep: Union[numbers.Real, numpy.ndarray], grav: float = 9.81, maximum_number_of_iterations: int = 10, tolerance: float = 0.001) ‑> numpy.ndarray

Find wavenumber k for a given radial frequency w using Newton Iteration. Exit when either maximum number of iterations is reached, or tolerance is achieved. Typically only 1 to 2 iterations are needed.

:param w: radial frequency :param dep: depth in meters :param grav: gravitational acceleration :param maximum_number_of_iterations: maximum number of iterations :param tolerance: relative accuracy :return: The wavenumber as a numpy array.

Expand source code
@jit(**numba_default)
def inverse_intrinsic_dispersion_relation(
    angular_frequency: Union[Real, np.ndarray],
    dep: Union[Real, np.ndarray],
    grav: float = GRAV,
    maximum_number_of_iterations: int = 10,
    tolerance: float = 1e-3,
) -> np.ndarray:
    """
    Find wavenumber k for a given radial frequency w using Newton Iteration.
    Exit when either maximum number of iterations is reached, or tolerance
    is achieved. Typically only 1 to 2 iterations are needed.

    :param w: radial frequency
    :param dep: depth in meters
    :param grav:  gravitational acceleration
    :param maximum_number_of_iterations: maximum number of iterations
    :param tolerance: relative accuracy
    :return: The wavenumber as a numpy array.
    """

    # Numba does not recognize "atleast_1d" for scalars
    w = atleast_1d(angular_frequency)

    k_deep_water_estimate = w**2 / grav
    k_shallow_water_estimate = w / np.sqrt(grav * dep)

    # == FIRST GUESS==
    # Use the intersection between shallow and deep water estimates to guestimate
    # which relation to use
    wavenumber_estimate = np.where(
        w > np.sqrt(grav / dep), k_deep_water_estimate, k_shallow_water_estimate
    )

    # == Newton Iteration ==
    error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w
    for ii in range(0, maximum_number_of_iterations):
        # Calculate the derivative of the error function with respect to wavenumber.
        # To note: the derivative is merely the group velocity.
        kd = wavenumber_estimate * dep
        error_derivative_to_wavenumber = np.where(
            kd > 5,
            0.5 * w / wavenumber_estimate,
            (1 / 2 + kd / np.sinh(2 * kd)) * w / wavenumber_estimate,
        )
        # Newton Iteration
        wavenumber_estimate = (
            wavenumber_estimate - error / error_derivative_to_wavenumber
        )

        # Update error
        error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w

        # Check for convergence
        relative_absolute_error = np.abs(error) / w
        if np.all(relative_absolute_error < tolerance):
            break
    else:
        print(
            "inverse_intrinsic_dispersion_relation::"
            " No convergence in solving for wavenumber"
        )

    return wavenumber_estimate
def jacobian_radial_frequency_to_wavenumber(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def jacobian_radial_frequency_to_wavenumber(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return intrinsic_group_velocity(k, depth, grav)
def jacobian_wavenumber_to_radial_frequency(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def jacobian_wavenumber_to_radial_frequency(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return 1 / intrinsic_group_velocity(k, depth, grav)
def k(angular_frequency: Union[numbers.Real, numpy.ndarray], dep: Union[numbers.Real, numpy.ndarray], grav: float = 9.81, maximum_number_of_iterations: int = 10, tolerance: float = 0.001) ‑> numpy.ndarray

Find wavenumber k for a given radial frequency w using Newton Iteration. Exit when either maximum number of iterations is reached, or tolerance is achieved. Typically only 1 to 2 iterations are needed.

:param w: radial frequency :param dep: depth in meters :param grav: gravitational acceleration :param maximum_number_of_iterations: maximum number of iterations :param tolerance: relative accuracy :return: The wavenumber as a numpy array.

Expand source code
@jit(**numba_default)
def inverse_intrinsic_dispersion_relation(
    angular_frequency: Union[Real, np.ndarray],
    dep: Union[Real, np.ndarray],
    grav: float = GRAV,
    maximum_number_of_iterations: int = 10,
    tolerance: float = 1e-3,
) -> np.ndarray:
    """
    Find wavenumber k for a given radial frequency w using Newton Iteration.
    Exit when either maximum number of iterations is reached, or tolerance
    is achieved. Typically only 1 to 2 iterations are needed.

    :param w: radial frequency
    :param dep: depth in meters
    :param grav:  gravitational acceleration
    :param maximum_number_of_iterations: maximum number of iterations
    :param tolerance: relative accuracy
    :return: The wavenumber as a numpy array.
    """

    # Numba does not recognize "atleast_1d" for scalars
    w = atleast_1d(angular_frequency)

    k_deep_water_estimate = w**2 / grav
    k_shallow_water_estimate = w / np.sqrt(grav * dep)

    # == FIRST GUESS==
    # Use the intersection between shallow and deep water estimates to guestimate
    # which relation to use
    wavenumber_estimate = np.where(
        w > np.sqrt(grav / dep), k_deep_water_estimate, k_shallow_water_estimate
    )

    # == Newton Iteration ==
    error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w
    for ii in range(0, maximum_number_of_iterations):
        # Calculate the derivative of the error function with respect to wavenumber.
        # To note: the derivative is merely the group velocity.
        kd = wavenumber_estimate * dep
        error_derivative_to_wavenumber = np.where(
            kd > 5,
            0.5 * w / wavenumber_estimate,
            (1 / 2 + kd / np.sinh(2 * kd)) * w / wavenumber_estimate,
        )
        # Newton Iteration
        wavenumber_estimate = (
            wavenumber_estimate - error / error_derivative_to_wavenumber
        )

        # Update error
        error = intrinsic_dispersion_relation(wavenumber_estimate, dep, grav) - w

        # Check for convergence
        relative_absolute_error = np.abs(error) / w
        if np.all(relative_absolute_error < tolerance):
            break
    else:
        print(
            "inverse_intrinsic_dispersion_relation::"
            " No convergence in solving for wavenumber"
        )

    return wavenumber_estimate
def n(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def ratio_group_velocity_to_phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    kd = k * depth
    return np.where(kd > 5, 0.5, 0.5 + kd / np.sinh(2 * kd))
def phase_velocity(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav=9.81) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav=GRAV
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    return intrinsic_dispersion_relation(k, depth, grav=grav) / k
def ratio_group_velocity_to_phase_velocity(k: numpy.ndarray, depth: Union[numbers.Real, numpy.ndarray], grav) ‑> numpy.ndarray

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return:

Expand source code
@jit(**numba_default)
def ratio_group_velocity_to_phase_velocity(
    k: np.ndarray, depth: Union[Real, np.ndarray], grav
) -> np.ndarray:
    """
    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return:
    """
    kd = k * depth
    return np.where(kd > 5, 0.5, 0.5 + kd / np.sinh(2 * kd))
def w(k: numpy.ndarray, dep: Union[numbers.Real, numpy.ndarray], grav: float = 9.81) ‑> numpy.ndarray

The intrinsic dispersion relation for linear waves in water of constant depth that relates the specific angular frequency to a given wavenumber and depth in a reference frame following mean ambient flow.

Wavenumber may be a scalar or a numpy array. The function always returns a numpy array. If depth is specified as a numpy array it must have the same shape as the wavenumber array.

:param k: Wavenumber (rad/m) :param depth: Depth (m) :param grav: Gravitational acceleration (m/s^2) :return: Intrinsic angular frequency (rad/s)

Expand source code
@jit(**numba_default)
def intrinsic_dispersion_relation(
    k: np.ndarray, dep: Union[Real, np.ndarray], grav: float = GRAV
) -> np.ndarray:
    """
    The intrinsic dispersion relation for linear waves in water of constant depth
    that relates the specific angular frequency to a given wavenumber and depth
    in a reference frame following mean ambient flow.

    Wavenumber may be a scalar or a numpy array. The function always returns
    a numpy array. If depth is specified as a numpy array it must have the same
    shape as the wavenumber array.

    :param k: Wavenumber (rad/m)
    :param depth: Depth (m)
    :param grav: Gravitational acceleration (m/s^2)
    :return: Intrinsic angular frequency (rad/s)
    """
    k = atleast_1d(k)
    return np.sqrt(grav * k * np.tanh(k * dep))