Skip to content

Commit c8cbd99

Browse files
authored
Merge pull request MHKiT-Software#151 from MShabara/upcrossing
Feature: Add Upcrossing Analysis Module (Ported from MHKiT-Python PR #252)
2 parents d8ba5b5 + 9808c7f commit c8cbd99

File tree

11 files changed

+541
-0
lines changed

11 files changed

+541
-0
lines changed

examples/upcrossing_example.html

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

examples/upcrossing_example.mlx

142 KB
Binary file not shown.

mhkit/tests/upcrossing_Test.m

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
classdef upcrossing_Test < matlab.unittest.TestCase
2+
properties
3+
t
4+
signal
5+
zeroCrossApprox
6+
end
7+
8+
methods (TestClassSetup)
9+
% Shared setup for the entire test class
10+
function setupTestClass(testCase)
11+
% Define time vector
12+
testCase.t = linspace(0, 4, 1000);
13+
14+
% Define signal
15+
testCase.signal = testCase.exampleWaveform_(testCase.t);
16+
17+
% Approximate zero crossings
18+
testCase.zeroCrossApprox = [0, 2.1, 3, 3.8];
19+
end
20+
21+
end
22+
23+
methods
24+
function signal = exampleWaveform_(~, t)
25+
% Generate a simple waveform form to analyse
26+
% This has been created to perform
27+
% a simple independent calcuation that
28+
% the mhkit functions can be tested against.
29+
A = [0.5, 0.6, 0.3];
30+
T = [3, 2, 1];
31+
w = 2 * pi ./ T;
32+
33+
signal = zeros(size(t));
34+
for i = 1:length(A)
35+
signal = signal + A(i) * sin(w(i) * t);
36+
end
37+
end
38+
39+
function [crests, troughs, heights, periods] = exampleAnalysis_(testCase, signal)
40+
% NB: This only works due to the construction
41+
% of our test signal. It is not suitable as
42+
% a general approach.
43+
44+
% Gradient-based turning point analysis
45+
grad = diff(signal);
46+
47+
% +1 to get the index at turning point
48+
turningPoints = find(grad(1:end-1) .* grad(2:end) < 0) + 1;
49+
50+
crestInds = turningPoints(signal(turningPoints) > 0);
51+
troughInds = turningPoints(signal(turningPoints) < 0);
52+
53+
crests = signal(crestInds);
54+
troughs = signal(troughInds);
55+
heights = crests - troughs;
56+
57+
% Numerical zero-crossing solution
58+
zeroCross = zeros(size(testCase.zeroCrossApprox));
59+
for i = 1:length(testCase.zeroCrossApprox)
60+
zeroCross(i) = fzero(@(x) testCase.exampleWaveform_(x), ...
61+
testCase.zeroCrossApprox(i));
62+
end
63+
64+
periods = diff(zeroCross);
65+
end
66+
end
67+
68+
methods (Test)
69+
%% Test functions without indices (inds)
70+
function test_peaks(testCase)
71+
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
72+
got = uc_peaks(testCase.t, testCase.signal);
73+
74+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
75+
end
76+
77+
function test_troughs(testCase)
78+
[~, want, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
79+
got = uc_troughs(testCase.t, testCase.signal);
80+
81+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
82+
end
83+
84+
function test_heights(testCase)
85+
[~, ~, want, ~] = testCase.exampleAnalysis_(testCase.signal);
86+
87+
got = uc_heights(testCase.t, testCase.signal);
88+
89+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
90+
end
91+
92+
function test_periods(testCase)
93+
[~, ~, ~, want] = testCase.exampleAnalysis_(testCase.signal);
94+
95+
got = uc_periods(testCase.t, testCase.signal);
96+
97+
testCase.verifyEqual(got, want, 'AbsTol', 2e-3);
98+
end
99+
100+
function test_custom(testCase)
101+
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
102+
103+
% create a similar function to finding the peaks
104+
f = @(ind1, ind2) max(testCase.signal(ind1:ind2));
105+
106+
got = uc_custom(testCase.t, testCase.signal, f);
107+
108+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
109+
end
110+
111+
%% Test functions with indcies
112+
function test_peaks_with_inds(testCase)
113+
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
114+
115+
inds = upcrossing(testCase.t, testCase.signal);
116+
117+
got = uc_peaks(testCase.t, testCase.signal, inds);
118+
119+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
120+
end
121+
122+
function test_trough_with_inds(testCase)
123+
[~, want, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
124+
125+
inds = upcrossing(testCase.t, testCase.signal);
126+
127+
got = uc_troughs(testCase.t, testCase.signal, inds);
128+
129+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
130+
end
131+
132+
function test_heights_with_inds(testCase)
133+
[~, ~, want, ~] = testCase.exampleAnalysis_(testCase.signal);
134+
135+
inds = upcrossing(testCase.t, testCase.signal);
136+
137+
got = uc_heights(testCase.t, testCase.signal, inds);
138+
139+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
140+
end
141+
142+
function test_periods_with_inds(testCase)
143+
[~, ~, ~, want] = testCase.exampleAnalysis_(testCase.signal);
144+
inds = upcrossing(testCase.t, testCase.signal);
145+
146+
got = uc_periods(testCase.t, testCase.signal,inds);
147+
148+
testCase.verifyEqual(got, want, 'AbsTol', 2e-3);
149+
end
150+
151+
function test_custom_with_inds(testCase)
152+
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
153+
inds = upcrossing(testCase.t, testCase.signal);
154+
155+
% create a similar function to finding the peaks
156+
f = @(ind1, ind2) max(testCase.signal(ind1:ind2));
157+
158+
got = uc_custom(testCase.t, testCase.signal, f, inds);
159+
160+
testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
161+
end
162+
163+
end
164+
end

mhkit/utils/upcrossing/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Upcrossing Analysis Functions
2+
3+
This module contains a collection of functions that facilitate **upcrossing analysis** for time-series data. It provides tools to find zero upcrossings, peaks, troughs, heights, periods, and allows for custom user-defined calculations between zero crossings.
4+
5+
## Key Functions
6+
7+
### `upcrossing(t, data)`
8+
Finds the zero upcrossing points in the given time-series data.
9+
10+
**Parameters:**
11+
- `t` (array): Time array.
12+
- `data` (array): Signal time-series data.
13+
14+
**Returns:**
15+
- `inds` (array): Indices of zero upcrossing points.
16+
17+
---
18+
19+
### `peaks(t, data, inds)`
20+
Finds the peaks between zero upcrossings.
21+
22+
**Parameters:**
23+
- `t` (array): Time array.
24+
- `data` (array): Signal time-series data.
25+
- `inds` (array, optional): Indices of the upcrossing points.
26+
27+
**Returns:**
28+
- `peaks` (array): Peak values between the zero upcrossings.
29+
30+
---
31+
32+
### `troughs(t, data, inds)`
33+
Finds the troughs between zero upcrossings.
34+
35+
**Parameters:**
36+
- `t` (array): Time array.
37+
- `data` (array): Signal time-series data.
38+
- `inds` (array, optional): Indices of the upcrossing points.
39+
40+
**Returns:**
41+
- `troughs` (array): Trough values between the zero upcrossings.
42+
43+
---
44+
45+
### `heights(t, data, inds)`
46+
Calculates the height between zero upcrossings. The height is defined as the difference between the maximum and minimum values between each pair of zero crossings.
47+
48+
**Parameters:**
49+
- `t` (array): Time array.
50+
- `data` (array): Signal time-series data.
51+
- `inds` (array, optional): Indices of the upcrossing points.
52+
53+
**Returns:**
54+
- `heights` (array): Height values between the zero upcrossings.
55+
56+
---
57+
58+
### `periods(t, data, inds)`
59+
Calculates the period between zero upcrossings. The period is the difference in time between each pair of consecutive upcrossings.
60+
61+
**Parameters:**
62+
- `t` (array): Time array.
63+
- `data` (array): Signal time-series data.
64+
- `inds` (array, optional): Indices of the upcrossing points.
65+
66+
**Returns:**
67+
- `periods` (array): Period values between the zero upcrossings.
68+
69+
---
70+
71+
### `custom(t, data, func, inds)`
72+
Applies a custom user-defined function between zero upcrossing points.
73+
74+
**Parameters:**
75+
- `t` (array): Time array.
76+
- `data` (array): Signal time-series data.
77+
- `func` (function handle): A custom function that will be applied between the zero crossing periods.
78+
- `inds` (array, optional): Indices of the upcrossing points.
79+
80+
**Returns:**
81+
- `custom_vals` (array): Custom values calculated between the zero crossings.
82+
83+
---
84+
85+
## Author(s)
86+
- **mbruggs** - Python
87+
- **akeeste** - Python
88+
- **mshabara** - Matlab
89+
90+
## Date
91+
- 12/12/2024

mhkit/utils/upcrossing/uc_apply_.m

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
function vals = uc_apply_(t, data, f, inds)
2+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3+
%
4+
% Apply a function over defined intervals in time series data.
5+
%
6+
% Parameters
7+
% ------------
8+
% t: array
9+
% Array of time values.
10+
% data: array
11+
% Array of data values.
12+
% f: function handle
13+
% Function that takes two indices (start, end) and returns a scalar value.
14+
% inds: array, optional
15+
% Indices array defining the intervals. If not provided, intervals will be
16+
% computed using the upcrossing function.
17+
%
18+
% Returns
19+
% ---------
20+
% vals: array
21+
% Array of values resulting from applying the function over the defined intervals.
22+
%
23+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
24+
25+
if nargin < 4 % nargin: returns the number of function input arguments given in the call
26+
% If inds is not provided, compute using upcrossing
27+
inds = upcrossing(t, data);
28+
end
29+
30+
n = numel(inds) - 1; % Number of intervals
31+
vals = NaN(1, n); % Initialize the output array
32+
33+
for i = 1:n
34+
vals(i) = f(inds(i), inds(i+1)); % Apply the function to each pair of indices
35+
end
36+
37+
end

mhkit/utils/upcrossing/uc_custom.m

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
function custom_vals = uc_custom(t, data, func, inds)
2+
% Applies a custom function to the time-series data between upcrossing points.
3+
%
4+
% Parameters:
5+
%------------
6+
% t: array
7+
% Array of time values.
8+
% data: array
9+
% Array of data values.
10+
% func: function handle
11+
% Function to apply between the zero crossing periods.
12+
% inds: Array, optional
13+
% Indices for the upcrossing.
14+
%
15+
% Returns:
16+
% ---------
17+
% custom_vals: array
18+
% Custom values of the time-series
19+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
20+
21+
if nargin < 4
22+
inds = upcrossing(t, data);
23+
end
24+
if ~isa(func, 'function_handle')
25+
error('func must be a function handle');
26+
end
27+
28+
custom_vals = uc_apply_(t, data, func, inds);
29+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
function heights = uc_heights(t, data, inds)
2+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3+
%
4+
% Calculates the height between zero crossings.
5+
%
6+
% The height is defined as the max value - min value
7+
% between the zero crossing points.
8+
%
9+
% Parameters
10+
% ------------
11+
% t: array
12+
% Array of time values.
13+
% data: array
14+
% Array of data values.
15+
% inds: array, optional
16+
% Indices for the upcrossing.
17+
%
18+
% Returns:
19+
% ------------
20+
% heights: Height values of the time-series
21+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
22+
23+
if nargin < 3
24+
inds = upcrossing(t, data);
25+
end
26+
27+
heights = uc_apply_(t, data, @(ind1, ind2) max(data(ind1:ind2)) - min(data(ind1:ind2)), inds);
28+
end
29+

mhkit/utils/upcrossing/uc_peaks.m

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
function peaks = uc_peaks(t, data, inds)
2+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3+
4+
% Finds the peaks between zero crossings.
5+
%
6+
% Parameters:
7+
% ------------
8+
% t: array
9+
% Time array.
10+
% data: array
11+
% Signal time-series.
12+
% inds: Optional, array
13+
% indices for the upcrossing.
14+
%
15+
% Returns:
16+
% ------------
17+
% peaks: array
18+
% Peak values of the time-series
19+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
20+
21+
if nargin < 3
22+
inds = upcrossing(t, data);
23+
end
24+
25+
peaks = uc_apply_(t, data, @(ind1, ind2) max(data(ind1:ind2)), inds);
26+
end
27+

0 commit comments

Comments
 (0)