Last updated on Jun 03, 2026.

Source code for netsse.analys.enc_spec

# -*- coding: utf-8 -*-
"""
Functions to compute spectra in a **body-fixed** reference system.

.. 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.interpolate import interp1d
from netsse.tools.misc_func import areSame_vec, re_range, wrap


[docs] def resp_spec_1d(f0, ws, TRF1, TRF2, fe, U, beta): """Computes a response cross-spectrum in both the absolute and encountered frequency domains, in a case of long-crested seas. Parameters ---------- f0 : array_like of shape (Nf0,1) Vector of absolute frequencies [Hz]. ws : array_like of shape (Nf0,1) in 1-D 1-D wave spectrum, as a function of (absolute) wave frequency ``f0`` [Hz]. TRF1, TRF2 : array_like of shape (Nf0,1) Transfer functions of two response components, as a function of frequency ``f0`` [Hz]. fe : array_like of shape (Nfe,1) Vector of encounter frequencies [Hz]. U : float Ship speed [m/s]. beta : float Ship heading, relative to the wave direction [deg]. Returns ------- RspecAbs: array_like of shape (Nf0,1) Response spectrum, as a function of absolute wave frequency ``f0`` [Hz]. RspecEnc: array_like of shape (Nfe,1) Response spectrum, as a function of encountered frequency ``fe`` [Hz]. See Also -------- resp_spec_2d : Computes a response cross-spectrum in the encounter-frequency domain, in case of short-crested sea. Example ------- >>> RspecAbs, RspecEnc = resp_spec_1d(f0,ws,TRF1,TRF2,fe,U,beta) """ g = 9.81 # Gravitational acceleration [m/s^2] df0 = f0[1] - f0[0] # Step size in absolute frequencies [Hz] omega0 = 2 * np.pi * f0 # Angular wave frequency [rad/s] Nf0 = len(f0) Nfe = len(fe) A = 2 * np.pi * U / g * np.cos(np.radians(beta)) # Response spectrum in the absolute frequency domain RspecAbs = np.reshape(TRF1 * np.conj(TRF2) * ws, (-1,)) if areSame_vec(TRF1, TRF2): RspecAbs = np.real(RspecAbs) # Compute the encounter domain response spectrum if A == 0.0 or (beta >= 90 and beta <= 270): # Response spectrum in the encounter domain fe_resp = ( 1 / (2 * np.pi) * (omega0 - omega0**2 * U / g * np.cos(np.pi / 180 * beta)) ) RspecEnc = RspecAbs * (g / (g - 2 * omega0 * U * np.cos(np.pi / 180 * beta))) # Interpolate the response spectrum at the user-specified range of # encounter frequencies: f1 = interp1d(fe_resp, RspecEnc, bounds_error=False, fill_value=0.0) RspecEnc = f1(fe) else: # Following sea cases RspecEnc = np.zeros((Nfe)) for j in range(Nfe): f03 = (1 + np.sqrt(1 + 4 * A * fe[j])) / ( 2 * A ) # This wave freq. always exists df3de = 1 / np.sqrt(1 + 4 * A * fe[j]) if f03 > f0[0] and f03 < f0[Nf0 - 1]: m1_3 = np.int32((f03 - f0[0]) / df0) m2_3 = m1_3 + 1 if m2_3 > Nf0 - 1: continue b = df3de * (f03 - f0[m1_3]) / df0 a = df3de * (f0[m2_3] - f03) / df0 RspecEnc[j] += ( ws[m1_3] * np.abs(TRF1[m1_3] * np.conj(TRF2[m1_3])) ) * b + (ws[m2_3] * np.abs(TRF1[m2_3] * np.conj(TRF2[m2_3]))) * a if fe[j] < 1 / (4 * A): f01 = (1 - np.sqrt(1 - 4 * A * fe[j])) / ( 2 * A ) # 1st conditional wave freq. df1de = 1 / np.sqrt(1 - 4 * A * fe[j]) f02 = (1 + np.sqrt(1 - 4 * A * fe[j])) / ( 2 * A ) # 2nd conditional wave freq. df2de = df1de if f01 > f0[0] and f01 < f0[Nf0 - 1]: m1_1 = np.int32((f01 - f0[0]) / df0) m2_1 = m1_1 + 1 if m2_1 > Nf0 - 1: continue b = df1de * (f01 - f0[m1_1]) / df0 a = df1de * (f0[m2_1] - f01) / df0 RspecEnc[j] += ( ws[m1_1] * np.abs(TRF1[m1_1] * np.conj(TRF2[m1_1])) ) * b + (ws[m2_1] * np.abs(TRF1[m2_1] * np.conj(TRF2[m2_1]))) * a if f02 > f0[0] and f02 < f0[Nf0 - 1]: m1_2 = np.int32((f02 - f0[0]) / df0) m2_2 = m1_2 + 1 if m2_2 > Nf0 - 1: continue b = df2de * (f02 - f0[m1_2]) / df0 a = df2de * (f0[m2_2] - f02) / df0 RspecEnc[j] += ( ws[m1_2] * np.abs(TRF1[m1_2] * np.conj(TRF2[m1_2])) ) * b + (ws[m2_2] * np.abs(TRF1[m2_2] * np.conj(TRF2[m2_2]))) * a return RspecAbs, RspecEnc
[docs] def resp_spec_2d( freq0, mu, ws0, beta_TRF, TRF1, TRF2, freqE, U, psi, conv="from", unit_freq="rad/s" ): """Computes a response cross-spectrum in the encounter-frequency domain, in a case of short-crested seas. .. note:: This is a Python implementation of the algorithm given in Nielsen et al. (2021). Parameters ---------- freq0 : array_like of shape (Nf0,) Vector of absolute frequencies, in which the wave spectrum and the transfer functions are expressed. mu : array_like of shape (Nmu,) Vector of wave directions [deg], relative to North (in a NED-reference frame). ws0 : array_like of shape (Nf0,Nmu) in 2-D 2-D wave spectrum, as a function of (absolute) wave frequency ``freq0`` and wave direction ``mu`` [rad]. beta_TRF : array_like of shape (Nbeta,) Vector of relative wave headings [deg], in which the transfer functions are expressed. TRF1, TRF2 : array_like of shape (Nf0,Nbeta) Transfer function of the first and second response component, respectively, as a function of frequency ``freq0`` and relative wave heading. freqE : array_like of shape (Nfe,) Vector of encounter frequencies for the output response spectrum. U : float Ship speed [m/s] psi : float Ship heading [deg], relative to North (in a NED-reference frame). conv: {'from','to'}, optional The convention that is used to express the direction ``mu`` of wave spectrum: - 'from' : The naval architecture convention, indicating where the waves are COMING FROM. - 'to' : The oceanographic convention, indicating where the waves are GOING TO. The default is "from" direction. unit_freq : {'rad/s','Hz'}, optional Unit of the frequencies: - 'rad/s' : The variables ``freq0`` and ``freqE`` denote the angular frequencies in radians per second. - 'Hz' : The 2-D wave spectrum ``ws0`` is expressed as a function of frequency in Hertz. The default is 'rad/s'. Returns ------- RspecEnc: array_like of shape (Nfe,) Response spectrum, as a function of encounter frequency ``freqE``. See Also -------- resp_spec_1d : Computes a response cross-spectrum in both the absolute and encountered frequency domains, in a case of long-crested seas. References ---------- Nielsen, U. D., Mounet, R. E. G., & Brodtkorb, A. H. (2021). Tuning of transfer functions for analysis of wave-ship interactions. Marine Structures, 79, 103029. https://doi.org/10.1016/j.marstruc.2021.103029 Example ------- >>> RspecEnc = ... resp_spec_2d(freq0,mu,ws0,beta_TRF,TRF1,TRF2,freqE,U,psi,'from','rad/s') """ g = 9.81 # Gravitational acceleration [m/s^2] if unit_freq == "rad/s": f0 = freq0 / (2 * np.pi) fe = freqE / (2 * np.pi) ws = ws0 * (2 * np.pi) else: f0 = freq0 fe = freqE ws = ws0 df0 = f0[1] - f0[0] # Step size in absolute frequencies [Hz] Nf0 = len(f0) Nfe = len(fe) Ndir = len(mu) if mu[0,] % 360 == mu[-1,] % 360: # The directions are actually wrapped around, so that mu[0,] = mu[-1,] # This is changed, to avoid counting directions twice! beta = mu[:-1,] - psi Ndir = Ndir - 1 else: beta = mu - psi if conv == "from": beta = re_range(180 + beta) elif conv == "to": beta = re_range(beta) else: raise NameError('Invalid direction convention. Only accepts "from" or "to".') dbeta = 2 * np.pi / Ndir A = 2 * np.pi * U / g * np.cos(np.radians(beta)) beta_TRF_new = re_range(beta_TRF) # To prepare the interpolation of the transfer functions at the directions # of the wave spectrum, make sure that the vector of directions at which # the transfer functions are expressed is in a 'wrapped around' format, # so that beta_TRF_new[0,] = beta_TRF_new[-1,] with a modulo 2*pi: if beta_TRF_new[0,] % 360 != beta_TRF_new[-1,] % 360: beta_TRF_new = wrap(beta_TRF_new, axis=0, add=360) TRF1 = wrap(TRF1, axis=1) TRF2 = wrap(TRF2, axis=1) # Interpolate the transfer functions at the vector of wave directions: finterp_TRF1 = interp1d(beta_TRF_new, TRF1, kind="linear", axis=1) TRF1_interp = finterp_TRF1(beta) finterp_TRF2 = interp1d(beta_TRF_new, TRF2, kind="linear", axis=1) TRF2_interp = finterp_TRF2(beta) # Initialize the response spectrum: RspecEnc = np.zeros((Nfe,)) for j in range(Nfe): fe_j = fe[j,] for k in range(Ndir): # Do not count the direction beta = 0 deg twice! beta_k = beta[k,] A_k = A[k,] if beta_k >= 90 and beta_k <= 270: # Head sea cases f01 = (1 - np.sqrt(1 - 4 * A_k * fe_j)) / (2 * A_k) df1de = 1 / np.sqrt(1 - 4 * A_k * fe_j) if f01 > f0[0,] and f01 < f0[Nf0 - 1,]: # Only consider absolute (wave) frequencies for which # transfer functions are calculated/defined m1 = np.int32((f01 - f0[0,]) / df0) m2 = m1 + 1 if m2 > Nf0 - 1: continue # 'a' and 'b' are the weights for interpolation to exact frequency b = dbeta * df1de * (f01 - f0[m1,]) / df0 a = dbeta * df1de * (f0[m2,] - f01) / df0 RspecEnc[j,] += ( ws[m1, k] * TRF1_interp[m1, k] * np.conj(TRF2_interp[m1, k]) ) * b + ( ws[m2, k] * TRF1_interp[m2, k] * np.conj(TRF2_interp[m2, k]) ) * a else: # Following sea cases f03 = (1 + np.sqrt(1 + 4 * A_k * fe_j)) / ( 2 * A_k ) # This wave freq. always exists df3de = 1 / np.sqrt(1 + 4 * A_k * fe_j) if f03 > f0[0,] and f03 < f0[Nf0 - 1,]: m1_3 = np.int32((f03 - f0[0,]) / df0) m2_3 = m1_3 + 1 if m2_3 > Nf0 - 1: continue b = dbeta * df3de * (f03 - f0[m1_3,]) / df0 a = dbeta * df3de * (f0[m2_3,] - f03) / df0 RspecEnc[j,] += ( ws[m1_3, k] * TRF1_interp[m1_3, k] * np.conj(TRF2_interp[m1_3, k]) ) * b + ( ws[m2_3, k] * TRF1_interp[m2_3, k] * np.conj(TRF2_interp[m2_3, k]) ) * a if fe_j < 1 / (4 * A_k): f01 = (1 - np.sqrt(1 - 4 * A_k * fe_j)) / ( 2 * A_k ) # 1st conditional wave freq. df1de = 1 / np.sqrt(1 - 4 * A_k * fe_j) f02 = (1 + np.sqrt(1 - 4 * A_k * fe_j)) / ( 2 * A_k ) # 2nd conditional wave freq. df2de = df1de if f01 > f0[0,] and f01 < f0[Nf0 - 1,]: m1_1 = np.int32((f01 - f0[0,]) / df0) m2_1 = m1_1 + 1 if m2_1 > Nf0 - 1: continue b = dbeta * df1de * (f01 - f0[m1_1]) / df0 a = dbeta * df1de * (f0[m2_1] - f01) / df0 RspecEnc[j,] += ( ws[m1_1, k] * TRF1_interp[m1_1, k] * np.conj(TRF2_interp[m1_1, k]) ) * b + ( ws[m2_1, k] * TRF1_interp[m2_1, k] * np.conj(TRF2_interp[m2_1, k]) ) * a if f02 > f0[0,] and f02 < f0[Nf0 - 1,]: m1_2 = np.int32((f02 - f0[0,]) / df0) m2_2 = m1_2 + 1 if m2_2 > Nf0 - 1: continue b = dbeta * df2de * (f02 - f0[m1_2,]) / df0 a = dbeta * df2de * (f0[m2_2,] - f02) / df0 RspecEnc[j,] += ( ws[m1_2, k] * TRF1_interp[m1_2, k] * np.conj(TRF2_interp[m1_2, k]) ) * b + ( ws[m2_2, k] * TRF1_interp[m2_2, k] * np.conj(TRF2_interp[m2_2, k]) ) * a if unit_freq == "rad/s": RspecEnc = RspecEnc / (2 * np.pi) return RspecEnc