diff --git a/examples/upcrossing_example.html b/examples/upcrossing_example.html new file mode 100644 index 00000000..cf5e06a4 --- /dev/null +++ b/examples/upcrossing_example.html @@ -0,0 +1,83 @@ + +MHKit Upcrossing Analysis Example

MHKit Upcrossing Analysis Example

The following example shows how to perform an upcrossing analysis on a surface elevation trace and plot quantities of interest (such as wave heights, periods, and crest exceedance probabilities).
% Clear variables and close figures
clear; close all;

Compute the Surface Elevation

% Define Peak Period and Significant Wave Height
Tp = 10; % Peak period in seconds
Hs = 2.5; % Significant wave height in meters
gamma = 3.3; % JONSWAP
 
% Define Frequency Vector using a return period of 1 hour
Tr = 3600; % Return period in seconds (1 hour)
df = 1 / Tr; % Frequency step size in Hz
f = 0:df:1; % Frequency vector
 
% Calculate JONSWAP Spectrum
spec = jonswap_spectrum(f, Tp, Hs, gamma);
 
% Sampling frequency for time vector
fs = 10.0; % Sampling frequency in Hz
t = 0:1/fs:Tr-1/fs; % Time vector
 
% Generate Surface Elevation
eta = surface_elevation(spec, t, "seed", 123);
 
% Plot Surface Elevation
figure();
plot(eta.time, eta.elevation);
xlabel('Time (s)','interpreter','latex');
ylabel('$\eta (m)$','interpreter','latex');
title(['Surface Elevation for Tp = ' num2str(Tp) 's, Hs = ' num2str(Hs) 'm']);
grid on; set(gca,'TickLabelInterpreter','latex')
xlim([eta.time(1) eta.time(end)])

Plot the Individual Wave Heights and Periods

waveHeights = uc_heights(t, eta.elevation);
wavePeriods = uc_periods(t, eta.elevation);
 
figure();
plot(wavePeriods, waveHeights, 'o');
xlabel('Zero Crossing Period (s)','interpreter','latex');
ylabel('Wave Height (m)','interpreter','latex');
grid on;set(gca,'TickLabelInterpreter','latex')

Plot the Crest Probability of Exceedance Distribution

crests = uc_peaks(t, eta.elevation);
crests_sorted = sort(crests);
 
% Number of crests
N = numel(crests_sorted);
 
% Exceedance probability (ascending crests)
Q = (N:-1:1) / N;
 
% Plot Exceedance Probability
figure();
semilogy(crests_sorted, Q, 'o');
xlabel('Crest Height (m)','interpreter','latex');
ylabel('P(Exceedance)','interpreter','latex');
grid on; set(gca,'TickLabelInterpreter','latex');
+
+ +
\ No newline at end of file diff --git a/examples/upcrossing_example.mlx b/examples/upcrossing_example.mlx new file mode 100644 index 00000000..71e9017d Binary files /dev/null and b/examples/upcrossing_example.mlx differ diff --git a/mhkit/tests/upcrossing_Test.m b/mhkit/tests/upcrossing_Test.m new file mode 100644 index 00000000..d9fb06e6 --- /dev/null +++ b/mhkit/tests/upcrossing_Test.m @@ -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 diff --git a/mhkit/utils/upcrossing/README.md b/mhkit/utils/upcrossing/README.md new file mode 100644 index 00000000..4d65e799 --- /dev/null +++ b/mhkit/utils/upcrossing/README.md @@ -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 diff --git a/mhkit/utils/upcrossing/uc_apply_.m b/mhkit/utils/upcrossing/uc_apply_.m new file mode 100644 index 00000000..e3e9503a --- /dev/null +++ b/mhkit/utils/upcrossing/uc_apply_.m @@ -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 diff --git a/mhkit/utils/upcrossing/uc_custom.m b/mhkit/utils/upcrossing/uc_custom.m new file mode 100644 index 00000000..61bb7e98 --- /dev/null +++ b/mhkit/utils/upcrossing/uc_custom.m @@ -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 diff --git a/mhkit/utils/upcrossing/uc_heights.m b/mhkit/utils/upcrossing/uc_heights.m new file mode 100644 index 00000000..32c027d9 --- /dev/null +++ b/mhkit/utils/upcrossing/uc_heights.m @@ -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 + diff --git a/mhkit/utils/upcrossing/uc_peaks.m b/mhkit/utils/upcrossing/uc_peaks.m new file mode 100644 index 00000000..5bbe9303 --- /dev/null +++ b/mhkit/utils/upcrossing/uc_peaks.m @@ -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 + diff --git a/mhkit/utils/upcrossing/uc_periods.m b/mhkit/utils/upcrossing/uc_periods.m new file mode 100644 index 00000000..f824b99f --- /dev/null +++ b/mhkit/utils/upcrossing/uc_periods.m @@ -0,0 +1,25 @@ +function periods = uc_periods(t, data, inds) +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Calculates the period between zero crossings. +% +% Parameters +% ------------ +% t: array +% Array of time values. +% data: array +% Array of data values. +% inds - array, optional +% Indices for the upcrossing. +% +% Returns: +% ------------ +% periods: array, +% Period values of the time-series +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 3 + inds = upcrossing(t, data); +end + +periods = uc_apply_(t, data, @(ind1, ind2) t(ind2) - t(ind1), inds); +end \ No newline at end of file diff --git a/mhkit/utils/upcrossing/uc_troughs.m b/mhkit/utils/upcrossing/uc_troughs.m new file mode 100644 index 00000000..80bffb45 --- /dev/null +++ b/mhkit/utils/upcrossing/uc_troughs.m @@ -0,0 +1,27 @@ +function troughs = uc_troughs(t, data, inds) +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Finds the troughs between zero crossings. +% +% Parameters +% ---------- +% t : array +% Time array. +% data : array +% Signal time-series. +% inds : array, optional +% Indices for the upcrossing. +% +% Returns +% ------- +% troughs : array +% Trough values of the time-series. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 3 + inds = upcrossing(t, data); +end + +troughs = uc_apply_(t, data, @(ind1, ind2) min(data(ind1:ind2)), inds); +end + + diff --git a/mhkit/utils/upcrossing/upcrossing.m b/mhkit/utils/upcrossing/upcrossing.m new file mode 100644 index 00000000..f264e3cf --- /dev/null +++ b/mhkit/utils/upcrossing/upcrossing.m @@ -0,0 +1,29 @@ +function zeroUpCrossings_index = upcrossing(t, data) +%% Finds the zero upcrossing points. +% +% Parameters +% ---------- +% t : array +% Time array. +% data : array +% Signal time-series. +% +% Returns +% ------- +% zeroUpCrossings_index : array +% Zero crossing indices. + +if ~isvector(t) || ~isvector(data) + error('t and data must be 1D arrays'); +end + +% eliminate zeros +zeroMask = (data == 0); +data(zeroMask) = 0.5 * min(abs(data)); + +% zero up-crossings +diff_data = diff(sign(data)); +zeroUpCrossings_mask = (diff_data == 2) | (diff_data == 1); +zeroUpCrossings_index = find(zeroUpCrossings_mask); +end +