Skip to content

Commit

Permalink
Merge pull request #151 from MShabara/upcrossing
Browse files Browse the repository at this point in the history
Feature: Add Upcrossing Analysis Module (Ported from MHKiT-Python PR #252)
  • Loading branch information
simmsa authored Feb 4, 2025
2 parents d8ba5b5 + 9808c7f commit c8cbd99
Show file tree
Hide file tree
Showing 11 changed files with 541 additions and 0 deletions.
83 changes: 83 additions & 0 deletions examples/upcrossing_example.html

Large diffs are not rendered by default.

Binary file added examples/upcrossing_example.mlx
Binary file not shown.
164 changes: 164 additions & 0 deletions mhkit/tests/upcrossing_Test.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
classdef upcrossing_Test < matlab.unittest.TestCase
properties
t
signal
zeroCrossApprox
end

methods (TestClassSetup)
% Shared setup for the entire test class
function setupTestClass(testCase)
% Define time vector
testCase.t = linspace(0, 4, 1000);

% Define signal
testCase.signal = testCase.exampleWaveform_(testCase.t);

% Approximate zero crossings
testCase.zeroCrossApprox = [0, 2.1, 3, 3.8];
end

end

methods
function signal = exampleWaveform_(~, t)
% Generate a simple waveform form to analyse
% This has been created to perform
% a simple independent calcuation that
% the mhkit functions can be tested against.
A = [0.5, 0.6, 0.3];
T = [3, 2, 1];
w = 2 * pi ./ T;

signal = zeros(size(t));
for i = 1:length(A)
signal = signal + A(i) * sin(w(i) * t);
end
end

function [crests, troughs, heights, periods] = exampleAnalysis_(testCase, signal)
% NB: This only works due to the construction
% of our test signal. It is not suitable as
% a general approach.

% Gradient-based turning point analysis
grad = diff(signal);

% +1 to get the index at turning point
turningPoints = find(grad(1:end-1) .* grad(2:end) < 0) + 1;

crestInds = turningPoints(signal(turningPoints) > 0);
troughInds = turningPoints(signal(turningPoints) < 0);

crests = signal(crestInds);
troughs = signal(troughInds);
heights = crests - troughs;

% Numerical zero-crossing solution
zeroCross = zeros(size(testCase.zeroCrossApprox));
for i = 1:length(testCase.zeroCrossApprox)
zeroCross(i) = fzero(@(x) testCase.exampleWaveform_(x), ...
testCase.zeroCrossApprox(i));
end

periods = diff(zeroCross);
end
end

methods (Test)
%% Test functions without indices (inds)
function test_peaks(testCase)
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
got = uc_peaks(testCase.t, testCase.signal);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_troughs(testCase)
[~, want, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
got = uc_troughs(testCase.t, testCase.signal);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_heights(testCase)
[~, ~, want, ~] = testCase.exampleAnalysis_(testCase.signal);

got = uc_heights(testCase.t, testCase.signal);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_periods(testCase)
[~, ~, ~, want] = testCase.exampleAnalysis_(testCase.signal);

got = uc_periods(testCase.t, testCase.signal);

testCase.verifyEqual(got, want, 'AbsTol', 2e-3);
end

function test_custom(testCase)
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);

% create a similar function to finding the peaks
f = @(ind1, ind2) max(testCase.signal(ind1:ind2));

got = uc_custom(testCase.t, testCase.signal, f);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

%% Test functions with indcies
function test_peaks_with_inds(testCase)
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);

inds = upcrossing(testCase.t, testCase.signal);

got = uc_peaks(testCase.t, testCase.signal, inds);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_trough_with_inds(testCase)
[~, want, ~, ~] = testCase.exampleAnalysis_(testCase.signal);

inds = upcrossing(testCase.t, testCase.signal);

got = uc_troughs(testCase.t, testCase.signal, inds);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_heights_with_inds(testCase)
[~, ~, want, ~] = testCase.exampleAnalysis_(testCase.signal);

inds = upcrossing(testCase.t, testCase.signal);

got = uc_heights(testCase.t, testCase.signal, inds);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

function test_periods_with_inds(testCase)
[~, ~, ~, want] = testCase.exampleAnalysis_(testCase.signal);
inds = upcrossing(testCase.t, testCase.signal);

got = uc_periods(testCase.t, testCase.signal,inds);

testCase.verifyEqual(got, want, 'AbsTol', 2e-3);
end

function test_custom_with_inds(testCase)
[want, ~, ~, ~] = testCase.exampleAnalysis_(testCase.signal);
inds = upcrossing(testCase.t, testCase.signal);

% create a similar function to finding the peaks
f = @(ind1, ind2) max(testCase.signal(ind1:ind2));

got = uc_custom(testCase.t, testCase.signal, f, inds);

testCase.verifyEqual(got, want, 'AbsTol', 1e-3);
end

end
end
91 changes: 91 additions & 0 deletions mhkit/utils/upcrossing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Upcrossing Analysis Functions

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.

## Key Functions

### `upcrossing(t, data)`
Finds the zero upcrossing points in the given time-series data.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.

**Returns:**
- `inds` (array): Indices of zero upcrossing points.

---

### `peaks(t, data, inds)`
Finds the peaks between zero upcrossings.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.
- `inds` (array, optional): Indices of the upcrossing points.

**Returns:**
- `peaks` (array): Peak values between the zero upcrossings.

---

### `troughs(t, data, inds)`
Finds the troughs between zero upcrossings.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.
- `inds` (array, optional): Indices of the upcrossing points.

**Returns:**
- `troughs` (array): Trough values between the zero upcrossings.

---

### `heights(t, data, inds)`
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.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.
- `inds` (array, optional): Indices of the upcrossing points.

**Returns:**
- `heights` (array): Height values between the zero upcrossings.

---

### `periods(t, data, inds)`
Calculates the period between zero upcrossings. The period is the difference in time between each pair of consecutive upcrossings.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.
- `inds` (array, optional): Indices of the upcrossing points.

**Returns:**
- `periods` (array): Period values between the zero upcrossings.

---

### `custom(t, data, func, inds)`
Applies a custom user-defined function between zero upcrossing points.

**Parameters:**
- `t` (array): Time array.
- `data` (array): Signal time-series data.
- `func` (function handle): A custom function that will be applied between the zero crossing periods.
- `inds` (array, optional): Indices of the upcrossing points.

**Returns:**
- `custom_vals` (array): Custom values calculated between the zero crossings.

---

## Author(s)
- **mbruggs** - Python
- **akeeste** - Python
- **mshabara** - Matlab

## Date
- 12/12/2024
37 changes: 37 additions & 0 deletions mhkit/utils/upcrossing/uc_apply_.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function vals = uc_apply_(t, data, f, inds)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Apply a function over defined intervals in time series data.
%
% Parameters
% ------------
% t: array
% Array of time values.
% data: array
% Array of data values.
% f: function handle
% Function that takes two indices (start, end) and returns a scalar value.
% inds: array, optional
% Indices array defining the intervals. If not provided, intervals will be
% computed using the upcrossing function.
%
% Returns
% ---------
% vals: array
% Array of values resulting from applying the function over the defined intervals.
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

if nargin < 4 % nargin: returns the number of function input arguments given in the call
% If inds is not provided, compute using upcrossing
inds = upcrossing(t, data);
end

n = numel(inds) - 1; % Number of intervals
vals = NaN(1, n); % Initialize the output array

for i = 1:n
vals(i) = f(inds(i), inds(i+1)); % Apply the function to each pair of indices
end

end
29 changes: 29 additions & 0 deletions mhkit/utils/upcrossing/uc_custom.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function custom_vals = uc_custom(t, data, func, inds)
% Applies a custom function to the time-series data between upcrossing points.
%
% Parameters:
%------------
% t: array
% Array of time values.
% data: array
% Array of data values.
% func: function handle
% Function to apply between the zero crossing periods.
% inds: Array, optional
% Indices for the upcrossing.
%
% Returns:
% ---------
% custom_vals: array
% Custom values of the time-series
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

if nargin < 4
inds = upcrossing(t, data);
end
if ~isa(func, 'function_handle')
error('func must be a function handle');
end

custom_vals = uc_apply_(t, data, func, inds);
end
29 changes: 29 additions & 0 deletions mhkit/utils/upcrossing/uc_heights.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function heights = uc_heights(t, data, inds)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Calculates the height between zero crossings.
%
% The height is defined as the max value - min value
% between the zero crossing points.
%
% Parameters
% ------------
% t: array
% Array of time values.
% data: array
% Array of data values.
% inds: array, optional
% Indices for the upcrossing.
%
% Returns:
% ------------
% heights: Height values of the time-series
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

if nargin < 3
inds = upcrossing(t, data);
end

heights = uc_apply_(t, data, @(ind1, ind2) max(data(ind1:ind2)) - min(data(ind1:ind2)), inds);
end

27 changes: 27 additions & 0 deletions mhkit/utils/upcrossing/uc_peaks.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function peaks = uc_peaks(t, data, inds)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Finds the peaks between zero crossings.
%
% Parameters:
% ------------
% t: array
% Time array.
% data: array
% Signal time-series.
% inds: Optional, array
% indices for the upcrossing.
%
% Returns:
% ------------
% peaks: array
% Peak values of the time-series
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

if nargin < 3
inds = upcrossing(t, data);
end

peaks = uc_apply_(t, data, @(ind1, ind2) max(data(ind1:ind2)), inds);
end

Loading

0 comments on commit c8cbd99

Please sign in to comment.