Skip to content

Commit 263d770

Browse files
enh: add Torsethaugen wave spectrum (#49)
1 parent 1801d5a commit 263d770

7 files changed

Lines changed: 478 additions & 1 deletion

File tree

docs/api_ref/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ functions and methods.
2424
JONSWAP
2525
ModifiedPiersonMoskowitz
2626
OchiHubble
27+
Torsethaugen
2728
multiply
2829
polar_to_complex
2930
RAO

docs/user_guide/concepts_utils/directional_spectrum.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,4 @@ and a quantile, ``q``.
8888
duration = 3 * 3600 # 3 hours
8989
9090
mpm = spectrum.extreme(duration, q=0.37) # most probable maximum (MPM)
91-
q90 = spectrum.extreme(duration, q=0.99) # 90-th quantile
91+
q90 = spectrum.extreme(duration, q=0.90) # 90-th quantile

docs/user_guide/standardized_spectra.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,65 @@ and :math:`q`). The total spectrum is obtained by adding together the two wave c
163163
spectrum.
164164

165165

166+
Torsethaugen
167+
------------
168+
The *Torsethaugen* spectrum allows you to set up a double-peaked spectrum that represents
169+
sea states which includes both a remotely generated swell component (with low frequency energy)
170+
and a locally generated wind component (with high frequency energy). The spectral spectral model
171+
was developed based on average measured spectra for Norwegian waters (Haltenbanken and Statfjord).
172+
The Torsethaugen spectrum is described by two parameter (i.e. :math:`H_s` and :math:`T_p`), and
173+
is given by:
174+
175+
.. math::
176+
177+
S(\omega) = \alpha_{\gamma}S_1(\omega; H_{s1}, T_{p1})\gamma^{exp\left( -\frac{(\omega - \omega_{p1})^2}{2\sigma^2\omega_{p1}^2} \right)} + S_2(\omega; H_{s2}, T_{p2})
178+
179+
where,
180+
181+
.. math::
182+
183+
S_i(\omega) = \frac{A_{i}}{\omega^4} \exp(-\frac{B_{i}}{\omega^4})
184+
185+
Furthermore,
186+
187+
- :math:`A_i = \frac{3.26}{16} H_{si}^2 \omega_{pi}^3`
188+
- :math:`B_i = \omega_{pi}^4`
189+
- :math:`H_{si}` is the significant wave height for wave component :math:`i`.
190+
- :math:`\omega_{pi} = \frac{2\pi}{T_{pi}}` is the angular spectral peak frequency for wave component :math:`i`.
191+
- :math:`\gamma` is a peak enhancement factor.
192+
- :math:`\alpha_{\gamma}` is a normalizing factor.
193+
- :math:`\sigma` is the spectral width parameter, given by:
194+
195+
.. math::
196+
\sigma =
197+
\begin{cases}
198+
\sigma_a & \quad \text{if } \omega \leq \omega_{p1}\\
199+
\sigma_b & \quad \text{if } \omega > \omega_{p1}
200+
\end{cases}
201+
202+
.. note::
203+
204+
The Torsethaugen spectrum is implemented according to the `original paper <https://www.sintef.no/globalassets/upload/fiskeri_og_havbruk/havbruksteknologi/2004-jsc-193.pdf/>`_
205+
by Torsethaugen and Haver published in 2004. Refer to this paper for full implementation
206+
details.
207+
208+
The :class:`~waveresponse.Torsethaugen` class provides functionality for generating a 1-D
209+
Torsethaugen spectrum given two parameters (i.e., :math:`H_s`, :math:`T_p`):
210+
211+
.. code:: python
212+
213+
import numpy as np
214+
import waveresponse as wr
215+
216+
217+
freq = np.arange(0.01, 1, 0.01)
218+
spectrum = wr.Torsethaugen(freq, freq_hz=True)
219+
220+
hs = 3.5
221+
tp = 10.0
222+
freq, vals = spectrum(hs, tp)
223+
224+
166225
Directional spectrum
167226
####################
168227
The directional spectrum is usually standardized in a similar way as the 1-D frequency

src/waveresponse/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
BaseSpectrum1d,
1818
ModifiedPiersonMoskowitz,
1919
OchiHubble,
20+
Torsethaugen,
2021
)
2122
from ._transform import (
2223
rigid_transform,
@@ -47,5 +48,6 @@
4748
"rigid_transform_heave",
4849
"rigid_transform_surge",
4950
"rigid_transform_sway",
51+
"Torsethaugen",
5052
"WaveSpectrum",
5153
]

src/waveresponse/_standardized1d.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,203 @@ def _C(self, hs, tp, q):
390390
def _d(self, tp, q):
391391
omega_p = 2.0 * np.pi / tp
392392
return (4 * q + 1) * omega_p**4 / 4.0
393+
394+
395+
class Torsethaugen(BaseSpectrum1d):
396+
"""
397+
Torsethaugen spectrum, given as:
398+
399+
``S(w) = alpha * S_1(w; Hs_1, Tp_1) * gamma ** b + S_2(w; Hs_2, Tp_2)``
400+
401+
where,
402+
403+
``S_i(w) = A_i / w ** 4 * exp(-B_i / w ** 4)``
404+
405+
and,
406+
407+
``b = exp(-(w - wp_1) ** 2 / (2 * sigma ** 2 * wp_1 ** 2))``
408+
409+
Furthermore,
410+
411+
- ``A_i = 3.26 / 16 * Hs_i ** 2 * wp_i ** 3``
412+
- ``B_i = wp_i ** 4``.
413+
- ``Hs_i`` is the significant wave height of the primary and the secondary system.
414+
- ``wp_i = 2pi / Tp_i`` is the angular spectral peak frequency of the primary and the secondary system.
415+
- ``gamma`` is a peak enhancement factor.
416+
- ``alpha = (1 + 1.1 * log(gamma) ** 1.19) / gamma`` is a normalizing factor.
417+
- ``sigma`` is the spectral width parameter (established from experimental data):
418+
- ``sigma = 0.07`` for ``w <= wp_1``
419+
- ``sigma = 0.09`` for ``w > wp_1``
420+
421+
Parameters
422+
----------
423+
freq : array-like
424+
Sequence of frequencies to use when generating the spectrum.
425+
freq_hz : bool
426+
Whether the provided frequencies are in rad/s (default) or Hz.
427+
428+
Notes
429+
-----
430+
The Torsethaugen spectrum is implemented as described in reference [1]_. All the
431+
intermediate parameters are applied as defined in Table 1 in reference [1]_.
432+
433+
References
434+
----------
435+
.. [1] Torsethaugen K and Haver S, 2004. Simplified double peak spectral model
436+
for ocean waves, Paper No. 2004-JSC-193, ISOPE 2004 Touson, France.
437+
438+
See Also
439+
--------
440+
ModifiedPiersonMoskowitz : Modified Pierson-Moskowitz wave spectrum.
441+
JONSWAP : JONSWAP wave spectrum.
442+
OchiHubble : Ochi-Hubble (three-parameter) wave spectrum.
443+
444+
"""
445+
446+
# Parameters as specified in Table 1 in reference [1].
447+
_af = 6.6
448+
_ae = 2.0
449+
_au = 25
450+
_a10 = 0.7
451+
_a1 = 0.5
452+
_kg = 35.0
453+
_b1 = 2.0
454+
_a20 = 0.6
455+
_a2 = 0.3
456+
_a3 = 6.0
457+
_g = 9.80665
458+
459+
def __call__(self, hs, tp, freq_hz=None):
460+
"""
461+
Generate wave spectrum.
462+
463+
Parameters
464+
----------
465+
hs : float
466+
Significant wave height, Hs.
467+
tp : float
468+
Peak period, Tp.
469+
freq_hz : bool, optional
470+
Whether to return the frequencies and spectrum in terms of Hz (`True`)
471+
or rad/s (`False`). If `None` (default), the original units of `freq` is
472+
preserved.
473+
474+
Return
475+
------
476+
freq : 1-D array
477+
Frequencies corresponding to the spectrum values. Unit is set according
478+
to `freq_hz`.
479+
spectrum : 1-D array
480+
Spectrum values. Unit is set according to `freq_hz`.
481+
482+
Notes
483+
-----
484+
The scaling between wave spectrum in terms of Hz and rad/s is defined
485+
as:
486+
487+
``S(f) = 2*pi*S(w)``
488+
489+
where ``S(f)`` and ``S(w)`` are the same spectrum but expressed
490+
in terms of Hz and rad/s, respectively.
491+
"""
492+
493+
return super().__call__(hs, tp, freq_hz=freq_hz)
494+
495+
def _spectrum(self, omega, hs, tp):
496+
tpf = self._tpf(hs)
497+
498+
if tp <= tpf: # wind dominated
499+
hs_primary = hs * self._rw(hs, tp)
500+
tp_primary = tp
501+
gamma = self._gamma(hs_primary, tp_primary)
502+
503+
hs_secondary = hs * np.sqrt(1.0 - self._rw(hs, tp) ** 2)
504+
tp_secondary = tpf + self._b1
505+
506+
else: # swell dominated
507+
hs_primary = hs * self._rs(hs, tp)
508+
tp_primary = tp
509+
gamma = self._gamma(hs, tpf) * (1.0 + self._a3 * self._eps_u(hs, tp))
510+
511+
hs_secondary = hs * np.sqrt(1.0 - self._rs(hs, tp) ** 2)
512+
tp_secondary = self._af * hs_secondary ** (1.0 / 3.0)
513+
514+
omega_p_primary = 2.0 * np.pi / tp_primary
515+
A_primary = (3.26 / 16.0) * hs_primary**2 * omega_p_primary**3
516+
B_primary = omega_p_primary**4
517+
518+
spectrum_primary = (
519+
self._alpha(gamma)
520+
* A_primary
521+
/ omega**4
522+
* np.exp(-B_primary / omega**4)
523+
* gamma ** self._b(tp_primary)
524+
)
525+
526+
omega_p_secondary = 2.0 * np.pi / tp_secondary
527+
A_secondary = (3.26 / 16.0) * hs_secondary**2 * omega_p_secondary**3
528+
B_secondary = omega_p_secondary**4
529+
530+
spectrum_secondary = (
531+
A_secondary / omega**4 * np.exp(-B_secondary / omega**4)
532+
)
533+
534+
return spectrum_primary + spectrum_secondary
535+
536+
def _tpf(self, hs):
537+
return self._af * hs ** (1.0 / 3.0)
538+
539+
def _tl(self, hs):
540+
return self._ae * np.sqrt(hs)
541+
542+
def _eps_l(self, hs, tp):
543+
tpf = self._tpf(hs)
544+
tl = self._tl(hs)
545+
546+
eps_l = (tpf - tp) / (tpf - tl)
547+
548+
if eps_l < 0.0:
549+
raise ValueError("`eps_l` not defined.")
550+
551+
return min(eps_l, 1.0)
552+
553+
def _eps_u(self, hs, tp):
554+
tpf = self._tpf(hs)
555+
tu = self._au
556+
557+
eps_u = (tp - tpf) / (tu - tpf)
558+
559+
if eps_u < 0.0:
560+
raise ValueError("`eps_u` not defined.")
561+
562+
return min(eps_u, 1.0)
563+
564+
def _rw(self, hs, tp):
565+
eps_l = self._eps_l(hs, tp)
566+
return self._a10 + (1.0 - self._a10) * np.exp(-((eps_l / self._a1) ** 2.0))
567+
568+
def _rs(self, hs, tp):
569+
eps_u = self._eps_u(hs, tp)
570+
return self._a20 + (1.0 - self._a20) * np.exp(-((eps_u / self._a2) ** 2.0))
571+
572+
def _gamma(self, hs_, tp_):
573+
s = (2.0 * np.pi / self._g) * hs_ / tp_**2.0
574+
return self._kg * s ** (6.0 / 7.0)
575+
576+
def _alpha(self, gamma):
577+
return (1.0 + 1.1 * np.log(gamma) ** 1.19) / gamma
578+
579+
def _b(self, tp):
580+
omega_p = 2.0 * np.pi / tp
581+
sigma = self._sigma(omega_p)
582+
return np.exp(-0.5 * ((self._freq - omega_p) / (sigma * omega_p)) ** 2)
583+
584+
def _sigma(self, omega_p):
585+
"""
586+
Spectral width.
587+
"""
588+
arg = self._freq <= omega_p
589+
sigma = np.empty_like(self._freq)
590+
sigma[arg] = 0.07
591+
sigma[~arg] = 0.09
592+
return sigma

0 commit comments

Comments
 (0)