Skip to content

Commit 007b845

Browse files
mbruggsakeeste
andauthored
Introduce upcrossing analysis module (#252)
* Add test for loads.extreme.global_peaks function The loads_extreme.global_peaks function was previously missing a test. The test uses a simple function which can be independently analysed. The results of global_peaks and the independent analysis are then compared. * Introduce upcrossing module Previously there was no general means of performing an upcrossing analysis. The load.extreme.global_peaks function could only calculate peaks. The module provides some common methods but also the ability for the user to define their own function over the zero crossing points. * Implement loads.extreme.global_peaks in terms of upcrossing module With the recent addition of the upcrossing module, we can implement the loads.extreme.global_peaks function using it. * minor linting, module docstring, update parameter validation * fix upcrossing docstring * move upcrossing module to utils * update upcrossing import in example * fix last upcrossing docstring and move test to test/utils * update description in the upcrossing notebook * update import of upcrossing into test_upcrossing * typo in test file * final typo fix --------- Co-authored-by: akeeste <[email protected]>
1 parent abc4395 commit 007b845

File tree

6 files changed

+632
-18
lines changed

6 files changed

+632
-18
lines changed

examples/upcrossing_example.ipynb

Lines changed: 182 additions & 0 deletions
Large diffs are not rendered by default.

mhkit/loads/extreme.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pandas as pd
33
from scipy import stats, optimize, signal
44
from mhkit.wave.resource import frequency_moment
5+
from mhkit.utils import upcrossing, custom
56

67
def _peaks_over_threshold(peaks, threshold, sampling_rate):
78
threshold_unit = np.percentile(peaks, 100*threshold, method='hazen')
@@ -61,24 +62,23 @@ def global_peaks(t, data):
6162
assert isinstance(t, np.ndarray), 't must be of type np.ndarray'
6263
assert isinstance(data, np.ndarray), 'data must be of type np.ndarray'
6364

64-
# eliminate zeros
65-
zeroMask = (data == 0)
66-
data[zeroMask] = 0.5 * np.min(np.abs(data))
67-
# zero up-crossings
68-
diff = np.diff(np.sign(data))
69-
zeroUpCrossings_mask = (diff == 2) | (diff == 1)
70-
zeroUpCrossings_index = np.where(zeroUpCrossings_mask)[0]
71-
zeroUpCrossings_index = np.append(zeroUpCrossings_index, len(data) - 1)
72-
# global peaks
73-
npeaks = len(zeroUpCrossings_index)
74-
peaks = np.array([])
75-
t_peaks = np.array([])
76-
for i in range(npeaks - 1):
77-
peak_index = np.argmax(
78-
data[zeroUpCrossings_index[i]:zeroUpCrossings_index[i + 1]])
79-
t_peaks = np.append(t_peaks, t[zeroUpCrossings_index[i] + peak_index])
80-
peaks = np.append(peaks, data[zeroUpCrossings_index[i] + peak_index])
81-
return t_peaks, peaks
65+
# Find zero up-crossings
66+
inds = upcrossing(t, data)
67+
68+
# We also include the final point in the dataset
69+
inds = np.append(inds, len(data)-1)
70+
71+
# As we want to return both the time and peak
72+
# values, look for the index at the peak.
73+
# The call to argmax gives us the index within the
74+
# upcrossing period. Therefore to get the index in the
75+
# original array we need to add on the index that
76+
# starts the zero crossing period, ind1.
77+
func = lambda ind1, ind2: np.argmax(data[ind1:ind2]) + ind1
78+
79+
peak_inds = np.array(custom(t, data, func, inds), dtype=int)
80+
81+
return t[peak_inds], data[peak_inds]
8282

8383

8484
def number_of_short_term_peaks(n, t, t_st):

mhkit/tests/loads/test_extreme.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import numpy as np
2+
import unittest
3+
import mhkit.loads as loads
4+
from numpy.testing import assert_allclose
5+
6+
7+
class TestExtreme(unittest.TestCase):
8+
@classmethod
9+
def setUpClass(self):
10+
self.t, self.signal = self._example_waveform(self)
11+
12+
def _example_waveform(self):
13+
# Create simple wave form to analyse.
14+
# This has been created to perform
15+
# a simple independent calcuation that
16+
# the mhkit functions can be tested against.
17+
18+
A = np.array([0.5, 0.6, 0.3])
19+
T = np.array([3, 2, 1])
20+
w = 2 * np.pi / T
21+
22+
t = np.linspace(0, 4.5, 100)
23+
24+
signal = np.zeros(t.size)
25+
for i in range(A.size):
26+
signal += A[i] * np.sin(w[i] * t)
27+
28+
return t, signal
29+
30+
def _example_crest_analysis(self, t, signal):
31+
# NB: This only works due to the construction
32+
# of our test signal. It is not suitable as
33+
# a general approach.
34+
grad = np.diff(signal)
35+
36+
# +1 to get the index at turning point
37+
turning_points = np.flatnonzero(grad[1:] * grad[:-1] < 0) + 1
38+
39+
crest_inds = turning_points[signal[turning_points] > 0]
40+
crests = signal[crest_inds]
41+
42+
return crests, crest_inds
43+
44+
def test_global_peaks(self):
45+
peaks_t, peaks_val = loads.extreme.global_peaks(self.t, self.signal)
46+
47+
test_crests, test_crests_ind = self._example_crest_analysis(
48+
self.t, self.signal)
49+
50+
assert_allclose(peaks_t, self.t[test_crests_ind])
51+
assert_allclose(peaks_val, test_crests)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from mhkit.utils import upcrossing, peaks, troughs, heights, periods, custom
2+
import unittest
3+
from numpy.testing import assert_allclose
4+
import numpy as np
5+
from scipy.optimize import fsolve
6+
7+
8+
class TestUpcrossing(unittest.TestCase):
9+
@classmethod
10+
def setUpClass(self):
11+
self.t = np.linspace(0, 4, 1000)
12+
13+
self.signal = self._example_waveform(self, self.t)
14+
15+
# Approximiate points for the zero crossing,
16+
# used as starting points in numerical
17+
# solution.
18+
self.zero_cross_approx = [0, 2.1, 3, 3.8]
19+
20+
def _example_waveform(self, t):
21+
# Create simple wave form to analyse.
22+
# This has been created to perform
23+
# a simple independent calcuation that
24+
# the mhkit functions can be tested against.
25+
26+
A = np.array([0.5, 0.6, 0.3])
27+
T = np.array([3, 2, 1])
28+
w = 2 * np.pi / T
29+
30+
signal = np.zeros(t.size)
31+
for i in range(A.size):
32+
signal += A[i] * np.sin(w[i] * t)
33+
34+
return signal
35+
36+
def _example_analysis(self, t, signal):
37+
# NB: This only works due to the construction
38+
# of our test signal. It is not suitable as
39+
# a general approach.
40+
grad = np.diff(signal)
41+
42+
# +1 to get the index at turning point
43+
turning_points = np.flatnonzero(grad[1:] * grad[:-1] < 0) + 1
44+
45+
crest_inds = turning_points[signal[turning_points] > 0]
46+
trough_inds = turning_points[signal[turning_points] < 0]
47+
48+
crests = signal[crest_inds]
49+
troughs = signal[trough_inds]
50+
51+
heights = crests - troughs
52+
53+
zero_cross = fsolve(self._example_waveform, self.zero_cross_approx)
54+
periods = np.diff(zero_cross)
55+
56+
return crests, troughs, heights, periods
57+
58+
def test_peaks(self):
59+
want, _, _, _ = self._example_analysis(self.t, self.signal)
60+
61+
got = peaks(self.t, self.signal)
62+
63+
assert_allclose(got, want)
64+
65+
def test_troughs(self):
66+
_, want, _, _ = self._example_analysis(self.t, self.signal)
67+
68+
got = troughs(self.t, self.signal)
69+
70+
assert_allclose(got, want)
71+
72+
def test_heights(self):
73+
_, _, want, _ = self._example_analysis(self.t, self.signal)
74+
75+
got = heights(self.t, self.signal)
76+
77+
assert_allclose(got, want)
78+
79+
def test_periods(self):
80+
_, _, _, want = self._example_analysis(self.t, self.signal)
81+
82+
got = periods(self.t, self.signal)
83+
84+
assert_allclose(got, want, rtol=1e-3, atol=1e-3)
85+
86+
def test_custom(self):
87+
want, _, _, _ = self._example_analysis(self.t, self.signal)
88+
89+
# create a similar function to finding the peaks
90+
def f(ind1, ind2): return np.max(self.signal[ind1:ind2])
91+
92+
got = custom(self.t, self.signal, f)
93+
94+
assert_allclose(got, want)
95+
96+
def test_peaks_with_inds(self):
97+
want, _, _, _ = self._example_analysis(self.t, self.signal)
98+
99+
inds = upcrossing(self.t, self.signal)
100+
101+
got = peaks(self.t, self.signal, inds)
102+
103+
assert_allclose(got, want)
104+
105+
def test_trough_with_inds(self):
106+
_, want, _, _ = self._example_analysis(self.t, self.signal)
107+
108+
inds = upcrossing(self.t, self.signal)
109+
110+
got = troughs(self.t, self.signal, inds)
111+
112+
assert_allclose(got, want)
113+
114+
def test_heights_with_inds(self):
115+
_, _, want, _ = self._example_analysis(self.t, self.signal)
116+
117+
inds = upcrossing(self.t, self.signal)
118+
119+
got = heights(self.t, self.signal, inds)
120+
121+
assert_allclose(got, want)
122+
123+
def test_periods_with_inds(self):
124+
_, _, _, want = self._example_analysis(self.t, self.signal)
125+
126+
inds = upcrossing(self.t, self.signal)
127+
128+
got = periods(self.t, self.signal, inds)
129+
130+
assert_allclose(got, want, rtol=1e-3, atol=1e-3)
131+
132+
def test_custom_with_inds(self):
133+
want, _, _, _ = self._example_analysis(self.t, self.signal)
134+
135+
inds = upcrossing(self.t, self.signal)
136+
137+
# create a similar function to finding the peaks
138+
def f(ind1, ind2): return np.max(self.signal[ind1:ind2])
139+
140+
got = custom(self.t, self.signal, f, inds)
141+
142+
assert_allclose(got, want)

mhkit/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .time_utils import matlab_to_datetime, excel_to_datetime, index_to_datetime
22
from .stat_utils import get_statistics, vector_statistics, unwrap_vector, magnitude_phase, unorm
33
from .cache import handle_caching, clear_cache
4+
from .upcrossing import upcrossing, peaks, troughs, heights, periods, custom
45

56
_matlab = False # Private variable indicating if mhkit is run through matlab

0 commit comments

Comments
 (0)