Last updated on Jun 03, 2026.

Source code for netsse.analys.buoy

# -*- coding: utf-8 -*-
"""
Functions to **process wave buoy** motion signals into directional wave
spectra.

.. dropdown:: Copyright (C) 2023-2026 Technical University of Denmark, R.E.G. Mounet
    :color: primary
    :icon: law

    *This code is part of the NetSSE software.*

    NetSSE is free software: you can redistribute it and/or modify it under
    the terms of the GNU General Public License as published by the Free
    Software Foundation, either version 3 of the License, or (at your
    option) any later version.

    NetSSE is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    for more details.

    You should have received a copy of the GNU General Public License along
    with this program.  If not, see https://www.gnu.org/licenses/.

    To credit the author, users are encouraged to use below reference:

    .. code-block:: text

        Mounet, R. E. G., & Nielsen, U. D. NetSSE: An open-source Python package
        for network-based sea state estimation from ships, buoys, and other
        observation platforms (version 2.2). Technical University of Denmark,
        GitLab. February 2026. https://doi.org/10.11583/DTU.26379811.

*Last updated on 20-02-2026 by R.E.G. Mounet*

"""

import numpy as np
from scipy.integrate import trapezoid


[docs] def cross_spec2Fourier_coef(Gf): """Computes the Fourier coefficients from the cross-spectra of a heave-East-North wave buoy, as per Benoit et al. (1997). Parameters ---------- Gf : array_like of shape (3,3,Nf) Cross-spectra (Heave-East-North), as a function of frequency in Hertz. Returns ------- Sf : array_like of shape (Nf,) One-sided variance spectrum of the waves [m^2.s], as a function of frequency in Hertz. a1, a2, b1, b2 : array_like of shape (Nf,) Frequency-dependent Fourier coefficients of the directional wave spectrum. See Also -------- Shannon_MEMII_Newton : Reconstructs the directional spreading function based on the first four Fourier coefficients of a directional wave spectrum. References ---------- Benoit, M., Frigaard, P., & Schäffer, H. A. (1997). *Analysing Multidirectional Wave Spectra: A tentative classification of available methods*. Proceedings of the 27th IAHR Congress, San Francisco, CA, USA (pp. 131–158). Canadian Government Publishing. Example ------- >>> Sf, a1, a2, b1, b2 = cross_spec2Fourier_coef(Gf) """ C, Q = np.zeros(np.shape(Gf)), np.zeros(np.shape(Gf)) for m in range(3): for n in range(3): C[m, n, :] = np.real( Gf[m, n, :] ) # coïncident spectral density function, co-spectrum Q[m, n, :] = np.imag( Gf[m, n, :] ) # quadrature spectral density function, quad-spectrum Sf = np.squeeze(C[0, 0, :]).reshape((-1, 1)) # 1-D wave spectrum [m^2.s] # Fourier coefficients, as a function of frequency in Hertz: a1 = -Q[0, 1, :] / np.sqrt(C[0, 0, :] * (C[1, 1, :] + C[2, 2, :])) b1 = -Q[0, 2, :] / np.sqrt(C[0, 0, :] * (C[1, 1, :] + C[2, 2, :])) a2 = (C[1, 1, :] - C[2, 2, :]) / (C[1, 1, :] + C[2, 2, :]) b2 = 2 * C[1, 2, :] / (C[1, 1, :] + C[2, 2, :]) # Test: # b1 = -Q[0,1,:]/np.sqrt(C[0,0,:]*(C[1,1,:]+C[2,2,:])) # a1 = -Q[0,2,:]/np.sqrt(C[0,0,:]*(C[1,1,:]+C[2,2,:])) # a2 = (C[1,1,:]-C[2,2,:])/(C[1,1,:]+C[2,2,:]) # b2 = 2*C[1,2,:]/(C[1,1,:]+C[2,2,:]) # /!\ Important note: with the current definition of the Fourier coefficients, # the associated 2D wave spectra will show where the energy is GOING TO # (direction of propagation of the wave components), and NOT where the # energy comes from. return Sf, a1, a2, b1, b2
[docs] def Shannon_MEMII_Newton( a1, a2, b1, b2, freq, theta, maxiter, tol_error, miniter=50, approx=False ): """Reconstructs the directional spreading function based on the first four Fourier coefficients of a directional wave spectrum. The function implements the Maximum Entropy Principle (Hashimoto, 1997), either via running the Newton local linearisation method or following the approximation method by Kim et al. (1994). Parameters ---------- a1, a2, b1, b2 : array_like of shape (Nf,) First four Fourier coefficients of the directional wave spectrum. freq : array_like of shape (Nf,) Vector of (discretized) wave frequencies [Hz]. theta : array_like of shape (Ntheta,) Vector of wave headings [deg]. maxiter : int Maximum number of iterations. tol_error : float Tolerance in the relative error. miniter : int, default 50 Minimum number of iterations. approx : bool, default False Boolean which indicates whether the approximation method should be used for finding the Lagrangian multipliers. Returns ------- D : array_like of shape (Nf,Ntheta) Directional spreading function. flag : array_like of shape (Nf,) Boolean flag indicating unconverged frequencies (``flag = 0`` for unconverged) L1, L2, L3, L4 : array_like of shape (Nf,) Optimized Lagrange multipliers. See Also -------- cross_spec2Fourier_coef : Computes the Fourier coefficients from the cross-spectra of a heave-East-North wave buoy. netsse.analys.emep.emep : Extended Maximum Entropy Principle (EMEP) method for reconstructing the directional spreading function based on the cross-power spectra of measured wave-induced responses. References ---------- 1. Benoit, M., Frigaard, P., & Schäffer, H. A. (1997). *Analysing Multidirectional Wave Spectra: A tentative classification of available methods*. Proceedings of the 27th IAHR Congress, San Francisco, CA, USA (pp. 131–158). Canadian Government Publishing. 2. Hashimoto, N. (1997). *Analysis of the Directional Wave Spectrum from Field Data*. Advances in coastal and ocean engineering. 3:103-44. 3. Kim, T., Lin, L.-H., & Wang, H. (1994). *Application of Maximum Entropy Method to the Real Sea Data*. In Coastal Engineering (pp. 340–355). Example ------- >>> D, flag, L1, L2, L3, L4 = ... Shannon_MEMII_Newton(a1,a2,b1,b2,freq,theta,maxiter,tol_error,miniter=50,False) """ Nf = len(freq) Ntheta = len(theta) theta_rad = theta * np.pi / 180 D = np.zeros((Nf, Ntheta)) flag = np.zeros((Nf,)) L1, L2, L3, L4 = ( np.zeros((Nf, maxiter)), np.zeros((Nf, maxiter)), np.zeros((Nf, maxiter)), np.zeros((Nf, maxiter)), ) # Changing the Fourier coefficients symbols for ease: B_all = np.array([a1, b1, a2, b2]) A = np.array( [ np.cos(theta_rad), np.sin(theta_rad), np.cos(2 * theta_rad), np.sin(2 * theta_rad), ] ) # A values # Make calculations frequency per frequency for a in range(Nf): # Fourier coefficients for current frequency: B = B_all[:, a] # Initialisation: # Approximate solutions to the Lagrange multipliers (Kim et al. 1995): L1[a, 0] = 2 * B[0] * B[2] + 2 * B[1] * B[3] - 2 * B[0] * (1 + np.sum(B**2)) L2[a, 0] = 2 * B[0] * B[3] - 2 * B[1] * B[2] - 2 * B[1] * (1 + np.sum(B**2)) L3[a, 0] = B[0] ** 2 - B[1] ** 2 - 2 * B[2] * (1 + np.sum(B**2)) L4[a, 0] = 2 * B[0] * B[1] - 2 * B[3] * (1 + np.sum(B**2)) b = 0 if not approx: # Apply Newton's technique of local linearization and iteration # (Hashimoto 1997): for b in range(maxiter - 1): # Iteration loop L = np.array([L1[a, b], L2[a, b], L3[a, b], L4[a, b]]).reshape((4, 1)) A_bar = np.zeros((4, 4)) B_bar = np.zeros((4,)) for ii in range(4): for jj in range(4): A_bar[ii, jj] = trapezoid( (A[ii, :] - B[ii,]) * A[jj, :] * np.exp(-np.sum(L * A, axis=0)), theta_rad, ) B_bar[ii,] = trapezoid( (A[ii, :] - B[ii,]) * np.exp(-np.sum(L * A, axis=0)), theta_rad, ) # Solving linear system of equations to get the error: e = np.reshape(np.linalg.inv(A_bar).dot(B_bar), (4, 1)) # Next value of Lagrange multipliers for next iteration: [L1[a, b + 1], L2[a, b + 1], L3[a, b + 1], L4[a, b + 1]] = L + e # Break loop if converged early: if b > miniter: rel_err1 = np.abs(L1[a, b + 1] - L1[a, b]) / L1[a, b] rel_err2 = np.abs(L2[a, b + 1] - L2[a, b]) / L2[a, b] rel_err3 = np.abs(L3[a, b + 1] - L3[a, b]) / L3[a, b] rel_err4 = np.abs(L4[a, b + 1] - L4[a, b]) / L4[a, b] # Check convergence for the particular frequency: if ( rel_err1 < tol_error and rel_err2 < tol_error and rel_err3 < tol_error and rel_err4 < tol_error ): flag[a,] = 1 b += 1 break # Extract the optimal values of the Lagrangian multipliers: L1_star = L1[a, b] L2_star = L2[a, b] L3_star = L3[a, b] L4_star = L4[a, b] L = np.array([L1_star, L2_star, L3_star, L4_star]).reshape((4, 1)) # Calculate L0 L0 = np.log(trapezoid(np.exp(-np.sum(L * A, axis=0)), theta_rad)) # Compute the directional spreading function D: D[a, :] = np.exp(-L0 - np.sum(L * A, axis=0)) return D, flag, L1, L2, L3, L4
[docs] def Fourier2spread_dist_params(a1, b1, a2, b2): """Infers the parameters of the directional spreading distribution function from the Fourier coefficients, as per Kuik et al. (1988). Parameters ---------- a1, b1, a2, b2 : array_like of shape (Nf,) First four Fourier coefficients of the directional wave spectrum. Returns ------- alpha : array_like of shape (Nf,) Wave direction [rad]. sigma : array_like of shape (Nf,) Directional spread [rad]. gamma : array_like of shape (Nf,) Skewness of the directional distribution [-]. delta : array_like of shape (Nf,) Kurtosis of the directional distribution [-]. See Also -------- spread_dist_params2Fourier : Infers the Fourier coefficients from the parameters of the directional spreading distribution function. References ---------- Kuik, A. J., van Vledder, G. P., & Holthuijsen, L. H. (1988). *A Method for the Routine Analysis of Pitch-and-Roll Buoy Wave Data*. Journal of Physical Oceanography, 18(7), 1020–1034. Example ------- >>> [alpha, sigma, gamma, delta] = Fourier2spread_dist_params(a1,b1,a2,b2) """ alpha = np.atan2(b1, a1) m1 = np.sqrt(a1**2 + b1**2) m2 = a2 * np.cos(2 * alpha) + b2 * np.sin(2 * alpha) n2 = b2 * np.cos(2 * alpha) - a2 * np.sin(2 * alpha) sigma = np.sqrt(2 * (1 - m1)) gamma = -n2 / ((1 - m2) / 2) ** (3 / 2) delta = (6 - 8 * m1 + 2 * m2) / (2 * (1 - m1)) ** 2 return alpha, sigma, gamma, delta
[docs] def spread_dist_params2Fourier(alpha, sigma, gamma, delta, unit="rad"): """Infers the Fourier coefficients from the parameters of the directional spreading distribution function, as per Kuik et al. (1988). Parameters ---------- alpha : array_like of shape (Nf,) Wave direction [rad, or deg]. sigma : array_like of shape (Nf,) Directional spread [rad, or deg]. gamma : array_like of shape (Nf,) Skewness of the directional distribution [-]. delta : array_like of shape (Nf,) Kurtosis of the directional distribution [-]. unit : {'rad','deg'}, optional Unit of the wave direction and directional spread: ``'deg'`` or ``'rad'`` (default). Returns ------- a1, b1, a2, b2 : array_like of shape (Nf,) First four Fourier coefficients of the directional wave spectrum. See Also -------- Fourier2spread_dist_params : Infers the parameters of the directional spreading distribution function from the Fourier coefficients. Shannon_MEMII_Newton : Reconstructs the directional spreading function based on the first four Fourier coefficients of a directional wave spectrum. References ---------- Kuik, A. J., van Vledder, G. P., & Holthuijsen, L. H. (1988). *A Method for the Routine Analysis of Pitch-and-Roll Buoy Wave Data*. Journal of Physical Oceanography, 18(7), 1020–1034. Example ------- >>> [a1,b1,a2,b2] = spread_dist_params2Fourier(alpha,sigma,gamma,delta,unit) """ if unit == "deg": alpha = np.radians(alpha) sigma = np.radians(sigma) m1 = np.abs(1 - sigma**2 / 2) a1 = np.sign(np.cos(alpha)) * m1 * (1 + np.tan(alpha) ** 2) ** (-1 / 2) b1 = a1 * np.tan(alpha) m2 = 4 * m1 + 2 * delta * (1 - m1) ** 2 - 3 n2 = -gamma * ((1 - m2) / 2) ** (3 / 2) a2 = m2 * np.cos(2 * alpha) - n2 * np.sin(2 * alpha) b2 = m2 * np.sin(2 * alpha) + n2 * np.cos(2 * alpha) return a1, b1, a2, b2