Qu

Signal processing

Peak finding

findpeaks() on a noisy multi-tone signal + spectrum.

catalog/qu_peak_finding.qu

Builds a three-tone signal buried in noise, finds its time-domain peaks with `findpeaks`, and separately confirms the three tones show up as spectral peaks in the FFT magnitude. Also exercises `figure_background` and the `len`/`shape` spellings of `length`/`size` added alongside `findpeaks` in this pass.

What it exercises

How it works

Setup — a noisy three-tone signal

Fs = 500
N  = 1000
t  = (0 to N - 1) / Fs

x = 1.0 * sin(2*pi*5*t) + 0.6 * sin(2*pi*17*t + 0.3) + 0.3 * sin(2*pi*41*t)
x = x + 0.15 * randn(1, N)

print("len(x)    = {len(x)}")       # alias for length(x)
print("shape(x) = {shape(x)}")      # same builtin as size(x)

Time-domain peaks

pk = findpeaks(x, min_peak_height=0.5, min_peak_distance=10)
print("found {len(pk.peaks)} time-domain peaks")
print("first 5 peak heights   = {pk.peaks[0:4]}")
print("first 5 peak locations = {pk.locations[0:4]}")

Spectral peaks (cross-check via FFT)

X   = abs(fft(x, N))
half = X[0:N/2 - 1]
freq = range(0, N/2 - 1) * (Fs / N)

spec_pk = findpeaks(half, min_peak_height=max(half) * 0.2, min_peak_distance=5)
print("spectral peak frequencies ~ {freq[spec_pk.locations]} Hz")   # ~ 5, 17, 41 Hz

Plot

subplot(2, 1, 1)
plot(t, x, label="signal", color="blue")
scatter(t[pk.locations], pk.peaks, marker="o", color="red", label="findpeaks")
xlabel("Time (s)")
ylabel("Amplitude")
title("Noisy multi-tone signal with detected peaks")
legend()

subplot(2, 1, 2)
plot(freq, half, label="|X(f)|", color="orange")
scatter(freq[spec_pk.locations], half[spec_pk.locations], marker="x", color="black", label="spectral peaks")
xlabel("Frequency (Hz)")
ylabel("|X(f)|")
title("Spectrum with detected tone peaks")
legend()

figure_background("#f5f5f0")
show plot

Results

printed
len(x)    = 1000
shape(x) = [1000, 1]
found 37 time-domain peaks
first 5 peak heights   = [1.103247, 0.827927, 1.383918, 1.525973, 0.562222]
first 5 peak locations = [4, 17, 28, 39, 101]
spectral peak frequencies ~ [5, 17, 41] Hz
-2 -1 0 1 2 0 0.4 0.8 1.2 1.6 2.0 x=0.008, y=1.103 x=0.034, y=0.828 x=0.056, y=1.384 x=0.078, y=1.526 x=0.202, y=0.562 x=0.226, y=0.802 x=0.248, y=1.912 x=0.27, y=0.516 x=0.298, y=0.938 x=0.422, y=1.666 x=0.446, y=1.04 x=0.47, y=1.235 x=0.494, y=0.882 x=0.592, y=0.651 x=0.614, y=0.839 x=0.66, y=1.918 x=0.712, y=0.585 x=0.834, y=1.849 x=0.86, y=0.93 x=0.884, y=1.327 x=1.008, y=1.222 x=1.052, y=1.197 x=1.076, y=1.47 x=1.202, y=0.566 x=1.228, y=0.81 x=1.248, y=1.982 x=1.274, y=0.738 x=1.3, y=0.872 x=1.424, y=1.667 x=1.446, y=0.886 x=1.472, y=1.242 x=1.614, y=0.832 x=1.66, y=1.873 x=1.714, y=0.612 x=1.832, y=1.812 x=1.858, y=0.864 x=1.886, y=1.242 Noisy multi-tone signal with detected peaks Time (s) Amplitude signal findpeaks 0 100 200 300 400 500 0 40 80 120 160 200 240 Spectrum with detected tone peaks Frequency (Hz) |X(f)| |X(f)| spectral peaks
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_peak_finding.qu

# qu_peak_finding.qu — findpeaks() on a noisy multi-tone signal + spectrum.

#

# Builds a three-tone signal buried in noise, finds its time-domain peaks

# with `findpeaks`, and separately confirms the three tones show up as

# spectral peaks in the FFT magnitude. Also exercises `figure_background`

# and the `len`/`shape` spellings of `length`/`size` added alongside

# `findpeaks` in this pass.

#

# Constructs exercised (all real, verified-working builtins):

#   findpeaks(x, min_peak_height=, min_peak_distance=) -> Model(.peaks, .locations)

#   figure_background(color)

#   len(x) / shape(x)   -- shorter spellings of length(x) / size(x)


backend auto
seed(42)

#%% Setup — a noisy three-tone signal

Fs = 500
N  = 1000
t  = (0 to N - 1) / Fs

x = 1.0 * sin(2*pi*5*t) + 0.6 * sin(2*pi*17*t + 0.3) + 0.3 * sin(2*pi*41*t)
x = x + 0.15 * randn(1, N)

print("len(x)    = {len(x)}")       # alias for length(x)

print("shape(x) = {shape(x)}")      # same builtin as size(x)


#%% Time-domain peaks

pk = findpeaks(x, min_peak_height=0.5, min_peak_distance=10)
print("found {len(pk.peaks)} time-domain peaks")
print("first 5 peak heights   = {pk.peaks[0:4]}")
print("first 5 peak locations = {pk.locations[0:4]}")

#%% Spectral peaks (cross-check via FFT)

X   = abs(fft(x, N))
half = X[0:N/2 - 1]
freq = range(0, N/2 - 1) * (Fs / N)

spec_pk = findpeaks(half, min_peak_height=max(half) * 0.2, min_peak_distance=5)
print("spectral peak frequencies ~ {freq[spec_pk.locations]} Hz")   # ~ 5, 17, 41 Hz


#%% Plot

subplot(2, 1, 1)
plot(t, x, label="signal", color="blue")
scatter(t[pk.locations], pk.peaks, marker="o", color="red", label="findpeaks")
xlabel("Time (s)")
ylabel("Amplitude")
title("Noisy multi-tone signal with detected peaks")
legend()

subplot(2, 1, 2)
plot(freq, half, label="|X(f)|", color="orange")
scatter(freq[spec_pk.locations], half[spec_pk.locations], marker="x", color="black", label="spectral peaks")
xlabel("Frequency (Hz)")
ylabel("|X(f)|")
title("Spectrum with detected tone peaks")
legend()

figure_background("#f5f5f0")
show plot