Skip to content

Commit 1801d5a

Browse files
enh: add functionality for computing extreme values to DirectionalSpectrum class (#45)
1 parent 77ebda9 commit 1801d5a

4 files changed

Lines changed: 149 additions & 38 deletions

File tree

docs/user_guide/concepts_utils/directional_spectrum.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,20 @@ method with the desired order, `n`.
7272
m2 = spectrum.moment(2)
7373
7474
# Etc.
75+
76+
Calculate the mean zero-crossing period, Tz:
77+
78+
.. code-block:: python
79+
80+
spectrum.tz
81+
82+
Calculate extreme values using the :meth:`~waveresponse.DirectionalSpectrum.extreme`
83+
method. The method takes two arguments: the duration of the process (in seconds),
84+
and a quantile, ``q``.
85+
86+
.. code-block:: python
87+
88+
duration = 3 * 3600 # 3 hours
89+
90+
mpm = spectrum.extreme(duration, q=0.37) # most probable maximum (MPM)
91+
q90 = spectrum.extreme(duration, q=0.99) # 90-th quantile

docs/user_guide/concepts_utils/wave_spectrum.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,6 @@ Calculate the wave peak period, Tp:
8080
8181
wave.tp
8282
83-
Calculate the mean crossing period, Tz:
84-
85-
.. code-block:: python
86-
87-
wave.tz
88-
8983
Calculate the wave peak direction:
9084

9185
.. code-block:: python

src/waveresponse/_core.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,56 @@ def moment(self, n, freq_hz=None):
14331433
m_n = trapz((f**n) * spectrum, f)
14341434
return m_n
14351435

1436+
@property
1437+
def tz(self):
1438+
"""
1439+
Mean zero-crossing period, Tz, in 'seconds'.
1440+
1441+
Calculated from the zeroth- and second-order spectral moments according to:
1442+
1443+
``tz = sqrt(m0 / m2)``
1444+
1445+
where the spectral moments are calculated by integrating over frequency in Hz.
1446+
"""
1447+
m0 = self.moment(0, freq_hz=True)
1448+
m2 = self.moment(2, freq_hz=True)
1449+
return np.sqrt(m0 / m2)
1450+
1451+
def extreme(self, t, q=0.37):
1452+
"""
1453+
Compute the q-th quantile extreme value (assuming a Gaussian process).
1454+
1455+
The extreme value, ``x``, is calculated according to:
1456+
1457+
``x = sigma * sqrt(2 * ln((t / tz) / ln(1 / q)))``
1458+
1459+
where ``sigma`` is the standard deviation of the process, ``t`` is the duration
1460+
of the process, and ``q`` is the quantile. Setting ``q=0.37`` yields the
1461+
most probable maximum (MPM).
1462+
1463+
The extreme value indicates a level which the maximum value of the process
1464+
amplitudes will be below with a certain probability. Note that the method
1465+
only computes extreme values for the maxima, and does not consider the minima.
1466+
1467+
Parameters
1468+
----------
1469+
t : float
1470+
Time/duration in seconds for which the of the process is observed.
1471+
q : float or array-like
1472+
Quantile or sequence of quantiles to compute. Must be between 0 and 1
1473+
(inclusive).
1474+
1475+
Returns
1476+
-------
1477+
x : float or array
1478+
Extreme value(s). During the given time period, the maximum value of
1479+
the process amplitudes will be below the returned value with a given
1480+
probability.
1481+
1482+
"""
1483+
q = np.asarray_chkfinite(q)
1484+
return self.std() * np.sqrt(2.0 * np.log((t / self.tz) / np.log(1.0 / q)))
1485+
14361486

14371487
class WaveSpectrum(DirectionalSpectrum):
14381488
def __repr__(self):
@@ -1445,24 +1495,11 @@ def hs(self):
14451495
14461496
Calculated from the zeroth-order spectral moment according to:
14471497
1448-
``hs = 4.0 * np.sqrt(m0)``
1498+
``hs = 4.0 * sqrt(m0)``
14491499
"""
14501500
m0 = self.moment(0)
14511501
return 4.0 * np.sqrt(m0)
14521502

1453-
@property
1454-
def tz(self):
1455-
"""
1456-
Mean crossing period, Tz, (sometimes called the mean wave period) in 'seconds'.
1457-
1458-
Calculated from the zeroth- and second-order spectral moments according to:
1459-
1460-
``tz = np.sqrt(m0 / m2)``
1461-
"""
1462-
m0 = self.moment(0, freq_hz=False)
1463-
m2 = self.moment(2, freq_hz=True)
1464-
return np.sqrt(m0 / m2)
1465-
14661503
@property
14671504
def tp(self):
14681505
"""

tests/test_core.py

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,6 +3284,24 @@ def test_moment_m2_rads(self):
32843284
# not exactly same due to error in trapz for higher order functions
32853285
assert m_out == pytest.approx(m_expect, rel=0.1)
32863286

3287+
def test_tz(self):
3288+
f0 = 0.0
3289+
f1 = 2.0
3290+
3291+
freq = np.linspace(f0, f1, 20)
3292+
dirs = np.arange(5, 360, 10)
3293+
vals = np.ones((len(freq), len(dirs)))
3294+
spectrum = DirectionalSpectrum(freq, dirs, vals, freq_hz=True, degrees=True)
3295+
3296+
tz_out = spectrum.tz
3297+
3298+
m0 = (0.0 - 360.0) * (f0 - f1)
3299+
m2 = (1.0 / 3.0) * (0.0 - 360.0) * (f0**3 - f1**3)
3300+
3301+
tz_expect = np.sqrt(m0 / m2)
3302+
3303+
assert tz_out == pytest.approx(tz_expect, rel=0.1)
3304+
32873305
def test_from_grid(self):
32883306
freq = np.linspace(0, 1.0, 10)
32893307
dirs = np.linspace(0, 360.0, 15, endpoint=False)
@@ -3352,6 +3370,69 @@ def test_imag_raises(self, directional_spectrum):
33523370
with pytest.raises(AttributeError):
33533371
directional_spectrum.imag
33543372

3373+
def test_extreme_float(self):
3374+
f0 = 0.0
3375+
f1 = 2.0
3376+
3377+
freq = np.linspace(f0, f1, 20)
3378+
dirs = np.arange(5, 360, 10)
3379+
vals = np.ones((len(freq), len(dirs)))
3380+
spectrum = wr.DirectionalSpectrum(freq, dirs, vals, freq_hz=True, degrees=True)
3381+
3382+
sigma = spectrum.std()
3383+
tz = spectrum.tz
3384+
3385+
T = 360 * 24 * 60.0**2
3386+
q = 0.99
3387+
extreme_out = spectrum.extreme(T, q=q)
3388+
3389+
extreme_expect = sigma * np.sqrt(2.0 * np.log((T / tz) / np.log(1.0 / q)))
3390+
3391+
assert extreme_out == pytest.approx(extreme_expect)
3392+
3393+
def test_extreme_list(self):
3394+
f0 = 0.0
3395+
f1 = 2.0
3396+
3397+
freq = np.linspace(f0, f1, 20)
3398+
dirs = np.arange(5, 360, 10)
3399+
vals = np.ones((len(freq), len(dirs)))
3400+
spectrum = wr.DirectionalSpectrum(freq, dirs, vals, freq_hz=True, degrees=True)
3401+
3402+
sigma = spectrum.std()
3403+
tz = spectrum.tz
3404+
3405+
T = 360 * 24 * 60.0**2
3406+
q = [0.1, 0.5, 0.99]
3407+
extreme_out = spectrum.extreme(T, q=q)
3408+
3409+
extreme_expect = [
3410+
sigma * np.sqrt(2.0 * np.log((T / tz) / np.log(1.0 / q[0]))),
3411+
sigma * np.sqrt(2.0 * np.log((T / tz) / np.log(1.0 / q[1]))),
3412+
sigma * np.sqrt(2.0 * np.log((T / tz) / np.log(1.0 / q[2]))),
3413+
]
3414+
3415+
np.testing.assert_array_almost_equal(extreme_out, extreme_expect)
3416+
3417+
def test_extreme_mpm(self):
3418+
f0 = 0.0
3419+
f1 = 2.0
3420+
3421+
freq = np.linspace(f0, f1, 20)
3422+
dirs = np.arange(5, 360, 10)
3423+
vals = np.ones((len(freq), len(dirs)))
3424+
spectrum = wr.DirectionalSpectrum(freq, dirs, vals, freq_hz=True, degrees=True)
3425+
3426+
sigma = spectrum.std()
3427+
tz = spectrum.tz
3428+
3429+
T = 360 * 24 * 60.0**2
3430+
extreme_out = spectrum.extreme(T, q=0.37)
3431+
3432+
extreme_expect = sigma * np.sqrt(2.0 * np.log(T / tz))
3433+
3434+
assert extreme_out == pytest.approx(extreme_expect, rel=1e-3)
3435+
33553436

33563437
class Test_WaveSpectrum:
33573438
def test__init__(self):
@@ -3428,24 +3509,6 @@ def test_hs(self):
34283509

34293510
assert hs_out == pytest.approx(hs_expect)
34303511

3431-
def test_tz(self):
3432-
f0 = 0.0
3433-
f1 = 2.0
3434-
3435-
freq = np.linspace(f0, f1, 20)
3436-
dirs = np.arange(5, 360, 10)
3437-
vals = np.ones((len(freq), len(dirs)))
3438-
wave = WaveSpectrum(freq, dirs, vals, freq_hz=True, degrees=True)
3439-
3440-
tz_out = wave.tz
3441-
3442-
m0 = (0.0 - 360.0) * (f0 - f1)
3443-
m2 = (1.0 / 3.0) * (0.0 - 360.0) * (f0**3 - f1**3)
3444-
3445-
tz_expect = np.sqrt(m0 / m2)
3446-
3447-
assert tz_out == pytest.approx(tz_expect, rel=0.1)
3448-
34493512
def test_tp_hz(self):
34503513
freq = np.linspace(0, 2, 20)
34513514
dirs = np.arange(5, 360, 10)

0 commit comments

Comments
 (0)