Finding peaks and valleys, and associated indexes, of time-shifted noisy sinosidal waves

18 views (last 30 days)
I want to automate a calibration process, but I'm really struggling with the code.
I have a signal that I give my instrument (second column of attached sample data), and then two probes which record the instrument's response, independently of each other (last two columns of attached sample data). The calibration signal is a series of sine waves of varying amplitudes and periods, see below:
I need to find the following for my calibrations:
-The signal value at each peak and trough
-Whether each probe produces sinosidal waves at each period/amplitude combination
-If so, the response value for each probe at its peaks and troughs (both as individual datapoints and as the mean value) and ideally the corresponding periods and amplitudes
The first is quite easy to do and the second can be done manually without too much trouble. Where I'm running into trouble with is the third step.
I tried the findpeaks function and it's quite noisy:
I did attempt to use the MinPeakHeight argument, use a high-pass filter, and smooth my probe data using moving averages, but none of those attempts worked very well. I also tried to use a Button Down Callback Function that would store values and indexes, but that ended up being super prone to user error in terms of the exact point to click on and not very fast. Solutions like this one don't work because the two sinusoidal response aren't quite fully phase shifted, they're time-shifted by an amount of time that's not consistent either across probes or over time.
Does anyone have any suggestions for functions to look into which might help me do what I want? I'm quite stuck.

Accepted Answer

William Rose
William Rose on 24 Sep 2025
You write that you want (and I have added numbers to your three desires),
1. The signal value at each peak and trough
2. Whether each probe produces sinosidal waves at each period/amplitude combination
3. If so, the response value for each probe at its peaks and troughs (both as individual datapoints and as the mean value) and ideally the corresponding periods and amplitudes
And you write "The first is quite easy to do and the second can be done manually without too much trouble. Where I'm running into trouble with is the third step."
I am glad that you can determine manually and without much trouble whether the waves are sinusoidal. That is beyond me. I would have to use function thd() or something like it to quantify sinusoidal-ness.
The interval between samples varies, which will complicate the analysis. The plots below show the three signals (top) and the sampling interval (middle). The input signal is labelled "x" and the outputs are "y1" and "y2". I will discuss the bottom panel later.
data=load('data.mat');
t=data.M(:,1); x=data.M(:,2); y1=data.M(:,3); y2=data.M(:,4);
figure
subplot(311), plot(t,x,'-r.',t,y1,'-g.',t,y2,'-b.');
grid on; xlabel('Time (s)'); legend('x','y1','y2');
subplot(312), plot(t(2:end),diff(t),'-k.');
grid on; xlabel('Time (s)'); ylabel('Sampling Interval (s)')
If the long intervals only occur between the pulses, then it would not be a big problem, but that is not the case. For example, if you zoom in on the time period 2020<=t<=2060, you will see that the long time intervals sometime occur in the middle of a sinusoidal pulse.
The bottom panel is a zoomed-in view of the first pulse. The plot shows that the signal is not sampled rapidly enough to determine if it is sinusoidal or not.
subplot(313)
plot(t,x,'-r.',t,y1,'-g.',t,y2,'-b.');
grid on; xlabel('Time'); legend('x','y1','y2');
xlim([73,76]); ylim([16.6,17.4])
The bottom panel shows 10 data points for 4 cycles of oscillation, i.e. the frerquency is 0.4 cycles per sample. If this wave is non-sinusoidal, i.e. if it has power at harmonics of the fundamental frequency, then those harmonics will be above the Nyquist frequency (0.5 cycles/sample), so they will be aliased down to lower frequencies, in the range 0 to 0.5 cycles/sample, and you will not be able to un-alias them or do a reliable analysis of the signal.
Can you resample the signals faster and at a constant rate?
To accomplish your third aim, which is to find the period and amplitude of the signal and probe responses for the sinusoidal pulses, I recommend fitting a sinusoid to the signal pulse and to the pulses recorded by the probes. There is no doubt a clever way to determine where the pulses start and end*, but for the sake of simlicity I will simply determine the pulse start and end times by inspecting the plot. Then I wil fit the data in those windows. Start and end times of first four pulses and last four pulses:
% Enter pulse start, end times, determined by inspecting the plots
Ts=[73.8,112,152,193.9,2213.8,2257.3,2308.2,2362.7];
% pulse start times (<= time of first point in each pulse)
Te=[75.6,115.6,157.4,201.1,2221,2271.7,2326.4,2391.4];
% pulse end times (>= time of last point in each pulse)
The next chunk of code is a funciton that fits a sinusoid to x,y data:
function [f,gof] = fitSinusoid(x,y)
% FITSINUSOID Fit sinusoid to data
% Model: y=a*sin(b*x+c)+d
% Make a good initial guess for parameters in order to increase
% chance of getting a good fit.
a0=(max(y)-min(y))/2; % amplitude initial guess
nzc=length(find(diff(sign(y-mean(y)))==-2));
% number of negative-going zero crossings
b0=2*pi*nzc/(max(x)-min(x)); % radian freq initial guess
c0=0; % phase initial guess
d0=mean(y); % offset initial guess
myModel=@(a,b,c,d,x) a*sin(b*x+c)+d;
[f1,gof1]=fit(x,y,myModel,...
'StartPoint', [a0,b0,c0,d0], ...
'Lower', [0, 0, 0, -Inf], ...
'Upper', [Inf, Inf, max(x)-min(x), Inf]);
[f2,gof2]=fit(x,y,myModel,...
'StartPoint', [2*a0,b0,c0,d0], ...
'Lower', [0, 0, 0, -Inf], ...
'Upper', [Inf, Inf, max(x)-min(x), Inf]);
if gof1.sse<=gof2.sse
f=f1; gof=gof1;
else
f=f2; gof=gof2;
end
end
You may be wondering why I didn't just use the built-in fittype 'sin1'. The reason is that 'sin1' does not include a mean offset, as far as I can tell.
The next chunk of code fits the signal and probe repsonses during each pulse, and displays the best-fit amplitude and frequency and r^2 on the console. It also displays plots of the data and the fits.
% Analyze each pulse
Np=length(Ts); % number of pulses
for i=1:Np
[fx,gofx] =fitSinusoid(t(t>=Ts(i) & t<=Te(i)),x(t>=Ts(i) & t<=Te(i)));
[fy1,gofy1]=fitSinusoid(t(t>=Ts(i) & t<=Te(i)),y1(t>=Ts(i) & t<=Te(i)));
[fy2,gofy2]=fitSinusoid(t(t>=Ts(i) & t<=Te(i)),y2(t>=Ts(i) & t<=Te(i)));
fprintf('Pulse %d:\n',i);
fprintf(' Signal: ampl=%.3f, freq=%.3f, r^2=%.2f.\n',...
fx.a,fx.b/(2*pi),gofx.rsquare)
fprintf(' Probe1: ampl=%.3f, freq=%.3f, r^2=%.2f.\n',...
fy1.a,fy1.b/(2*pi),gofy1.rsquare)
fprintf(' Probe2: ampl=%.3f, freq=%.3f, r^2=%.2f.\n',...
fy2.a,fy2.b/(2*pi),gofy2.rsquare)
figure
subplot(311)
plot(fx,t(t>=Ts(i) & t<=Te(i)),x(t>=Ts(i) & t<=Te(i)))
xlabel('Time'); grid on
subplot(312)
plot(fy1,t(t>=Ts(i) & t<=Te(i)),y1(t>=Ts(i) & t<=Te(i)))
xlabel('Time'); grid on
subplot(313)
plot(fy2,t(t>=Ts(i) & t<=Te(i)),y2(t>=Ts(i) & t<=Te(i)))
xlabel('Time'); grid on
end
Pulse 1:
Signal: ampl=0.252, freq=2.407, r^2=1.00.
Probe1: ampl=0.000, freq=0.000, r^2=NaN.
Probe2: ampl=0.000, freq=0.000, r^2=0.00.
Pulse 2:
Signal: ampl=0.249, freq=1.151, r^2=1.00.
Probe1: ampl=0.016, freq=0.291, r^2=0.66.
Probe2: ampl=0.030, freq=0.000, r^2=0.00.
Pulse 3:
Signal: ampl=0.250, freq=0.756, r^2=1.00.
Probe1: ampl=0.012, freq=0.379, r^2=0.23.
Probe2: ampl=0.050, freq=0.000, r^2=0.00.
Pulse 4:
Signal: ampl=0.250, freq=0.562, r^2=1.00.
Probe1: ampl=0.003, freq=0.421, r^2=0.01.
Probe2: ampl=0.002, freq=0.278, r^2=0.03.
Pulse 5:
Signal: ampl=3.999, freq=0.567, r^2=1.00.
Probe1: ampl=0.125, freq=0.424, r^2=0.03.
Probe2: ampl=0.516, freq=0.000, r^2=0.00.
Pulse 6:
Signal: ampl=4.000, freq=0.280, r^2=1.00.
Probe1: ampl=1.551, freq=0.279, r^2=0.86.
Probe2: ampl=0.875, freq=0.140, r^2=-1.08.
Pulse 7:
Signal: ampl=3.995, freq=0.220, r^2=0.99.
Probe1: ampl=1.859, freq=0.219, r^2=0.87.
Probe2: ampl=0.164, freq=0.220, r^2=0.22.
Pulse 8:
Signal: ampl=0.282, freq=0.139, r^2=0.14.
Probe1: ampl=2.449, freq=0.138, r^2=0.93.
Probe2: ampl=1.385, freq=0.138, r^2=0.76.
The plots show that the fits are not always the best, but maybe with some tweaking, you can get this approach to be useful. Maybe use fmincon() instead of fit(). Also, the script reports r^2=-1.08 for pulse 6. That shouldn't happen, so it merits further investigation.
Good luck with your analysis.
  3 Comments

Sign in to comment.

More Answers (1)

Mathieu NOE
Mathieu NOE on 24 Sep 2025
Edited: Mathieu NOE on 24 Sep 2025
Following my first comment above , this could be another approach
  • extract blocks of data (you need to find the start and stop times , here I used islocalmax to find peaks
  • bandpass filter your probe signals , once you have measured the test signal frequency (to adapt the low and high corner frequencies of the filter
  • you could easily add a simple test based on the filtered probe signals :
  • amplitude ratio vs test signal amplitude
  • if the ratio is very low => this is not really a valid output
  • if the ratio is above 10% (for example) , again you can try to fit a sinus and extract the parameters (done in William's answer)
data=load('data.mat');
t=data.M(:,1); x=data.M(:,2); y1=data.M(:,3); y2=data.M(:,4);
samples = numel(t);
% figure
% subplot(211), plot(t,x,'-r.',t,y1,'-g.',t,y2,'-b.');
% grid on; xlabel('Time (s)'); legend('x','y1','y2');
% xlim([min(t) max(t)])
Fs = 1./mean(diff(t));
% define start/end time points of blocks of data
tmp = abs(detrend(x,1));
tmp = tmp -min(tmp); % meake tmp min equals 0
tf = islocalmax(tmp); % option 3 with islocalmax (you can also try with find peaks)
tt = t(tf);
env = tmp(tf);
figure
plot(t,tmp,tt,env,'*')
% find indexes of distant blocks
ind = find(diff(tt)>20);
tt_ind = tt(ind);
n_blocks = numel(ind);
for k= 1:n_blocks
tstop(k,1) = tt_ind(k); % end of k th block
tbefore = tt(tt<=tstop(k,1));
if k<2
tstart(k,1) = min(tbefore); % start of 1st block
else
tstart(k,1) = min(tbefore(tbefore>tstop(k-1,1))); % start of k th block
end
% add some time before and after (several seconds)
extra_time = 4; % seconds
tstop(k,1) = min(samples,tstop(k,1) + extra_time);
tstart(k,1) = max(0,tstart(k,1) - extra_time);
% now extract portion of data (k th block)
ind = (t>=tstart(k,1) & t<=tstop(k,1) );
ti = t(ind);
xx = x(ind);
yy1 = y1(ind);
yy2 = y2(ind);
% counts (input) test signal threshold crossing to get its frequency
threshold = mean(xx)+0.02; %
[t_pos,t_neg] = find_zc(ti,xx,threshold);
t_pos = unique(t_pos);
freq_input = mean(1./diff(t_pos))
% now create a bandpass filter
if freq_input<Fs/2
a = 0.5;
flow = a*freq_input;
fhigh = min(1/a*freq_input,Fs/2.56);
else
flow = 0;
fhigh = Fs/2.56;
end
[B,A] = butter(2,2/Fs*[flow fhigh]);
yy1m = mean(yy1);
yy1f = filtfilt(B,A,detrend(yy1,1));
yy1f = yy1f + yy1m;
yy2m = mean(yy2);
yy2f = filtfilt(B,A,detrend(yy2,1));
yy2f = yy2f + yy2m;
figure
plot(ti,xx,'-r.',ti,yy1f,'-g.',ti,yy2f,'-b.');
grid on; xlabel('Time (s)'); legend('x','y1','y2');
title(['Signal Freq = ' num2str(freq_input) ' Hz'])
end
freq_input = 2.3968
freq_input = 1.1509
freq_input = 0.7577
freq_input = 0.5639
freq_input = 0.2797
freq_input = 0.2189
freq_input = 0.1388
freq_input = 2.3914
freq_input = 1.1532
freq_input = 0.7566
freq_input = 0.4470
freq_input = 0.2796
freq_input = 0.2223
freq_input = 0.1384
freq_input = 2.3931
freq_input = 1.1468
freq_input = 0.7547
freq_input = 0.5647
freq_input = 0.2785
freq_input = 0.2224
freq_input = 0.1386
freq_input = 2.3883
freq_input = 1.1490
freq_input = 0.7582
freq_input = 0.5625
freq_input = 0.2796
freq_input = 0.2154
freq_input = 0.1387
freq_input = 2.3950
freq_input = 1.1506
freq_input = 0.7537
freq_input = 0.5666
freq_input = 0.2801
freq_input = 0.2230
freq_input = 0.1377
freq_input = 2.3951
freq_input = 1.1559
freq_input = 0.7595
freq_input = 0.5640
freq_input = 0.2798
freq_input = 0.2222
freq_input = 0.1242
freq_input = 2.3889
freq_input = 1.1559
freq_input = 0.7552
freq_input = 0.5666
freq_input = 0.2797
freq_input = 0.2196
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [ZxP,ZxN] = find_zc(x,y,threshold)
% put data in rows
x = x(:);
y = y(:);
% positive slope "zero" crossing detection, using linear interpolation
y = y - threshold;
zci = @(data) find(diff(sign(data))>0); %define function: returns indices of +ZCs
ix=zci(y); %find indices of + zero crossings of x
ZeroX = @(x0,y0,x1,y1) x0 - (y0.*(x0 - x1))./(y0 - y1); % Interpolated x value for Zero-Crossing
ZxP = ZeroX(x(ix),y(ix),x(ix+1),y(ix+1));
% negative slope "zero" crossing detection, using linear interpolation
zci = @(data) find(diff(sign(data))<0); %define function: returns indices of +ZCs
ix=zci(y); %find indices of + zero crossings of x
ZeroX = @(x0,y0,x1,y1) x0 - (y0.*(x0 - x1))./(y0 - y1); % Interpolated x value for Zero-Crossing
ZxN = ZeroX(x(ix),y(ix),x(ix+1),y(ix+1));
end
  5 Comments
Oshani
Oshani on 18 Nov 2025 at 17:17
thank you! ended up going with a combination of this answer & @William Rose 's, wish I could accept both!
Mathieu NOE
Mathieu NOE on 19 Nov 2025 at 8:01
hello @Oshani
no problem - glad you have now a working solution
you cannot accept multiple answers , but you can still vote for the other answers that have contributed to the team work

Sign in to comment.

Products


Release

R2022b

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!