Skip to content

Quick Start

This guide walks through the main DSPkit workflows on a simulated 2DOF structural system — the same test signal used throughout the example scripts.

The test signal

DSPkit ships with a built-in 2DOF spring-mass-damper simulator:

from dspkit._testing import generate_2dof, natural_frequencies_2dof

fs = 1000.0   # Hz
t, a1, a2 = generate_2dof(
    duration=60.0,
    fs=fs,
    noise_std=1.0,
    output="acceleration",   # "displacement" | "velocity" | "acceleration"
    seed=42,
)

fn1, fn2 = natural_frequencies_2dof()
# fn1 ≈ 8.6 Hz,  fn2 ≈ 20.8 Hz

Spectral analysis

import dspkit as dsp

# Single-sided FFT amplitude spectrum
freqs, amp = dsp.fft_spectrum(a1, fs, window="hann", scaling="amplitude")
dsp.plot_fft(freqs, amp, xlim=(0, 80), title="FFT spectrum")

# Welch PSD
freqs, Pxx = dsp.psd(a1, fs, nperseg=4096)
dsp.plot_psd(freqs, Pxx, xlim=(0, 80))

# Magnitude-squared coherence between channels
freqs, Cxy = dsp.coherence(a1, a2, fs, nperseg=4096)
dsp.plot_coherence(freqs, Cxy, xlim=(0, 80))

# Autocorrelation
lags, acf = dsp.autocorrelation(a1, fs=fs, normalize=True, max_lag=0.5)
dsp.plot_autocorrelation(lags, acf, n_samples=len(a1), xlabel="Lag [s]")

Choosing the segment length, and when to stop using Welch

nperseg can be wrong in two opposite directions, and only one of them is obvious. Too few averages leaves a noisy spectrum and an inflated coherence; too short a segment to resolve the sharpest peak smears it, which biases coherence down and fabricates residual power. segment_advice weighs both on your own record and says which — including "this record cannot do both", which is a statement about the data rather than the parameters.

a = dsp.segment_advice(x1, fs, nperseg=1024)
print(a["verdict"], "—", a["advice"])
# resolution bandwidth, averages, bias floor and the sharpest peak found

When the record is genuinely too short, an autoregressive spectrum fits the whole of it at once instead of averaging segments, and can separate peaks Welch cannot:

freqs, Pxx, info = dsp.ar_psd(x1, fs)          # order chosen by AIC
freqs, Pxx, info = dsp.ar_psd(x1, fs, order=30)
print(info["order"], info["reflection_stable"])

The order is part of the answer, not a tuning knob: too low merges peaks, too high invents them. Check any peak that only the AR estimate shows against psd before believing it.

What one signal cannot explain about another

r = dsp.error_spectrum(target, predictors, fs, nperseg=2048)
r["error_spectrum"]        # (1 - coherence) * target PSD, in the target's units
r["unexplained_fraction"]  # the same thing as one number

Coherence says where a model fails; only this says whether that matters. A band where coherence collapses costs nothing if there is no power in it, which is the usual case.

Filtering

# Zero-phase Butterworth bandpass around first mode
a1_bp = dsp.bandpass(a1, fs, low=fn1 - 3, high=fn1 + 3, order=4)

# Notch filter to remove 50 Hz mains hum
a1_notched = dsp.notch(a1, fs, freq=50.0, q=30.0)

# Decimate to 100 Hz with anti-aliasing
a1_dec, fs_dec = dsp.decimate(a1, fs, target_fs=100.0)

Time-frequency analysis

import numpy as np

# Short-Time Fourier Transform
f, t, Zxx = dsp.stft(a1[:4096], fs, nperseg=256)
dsp.plot_spectrogram(f, t, Zxx, ylim=(0, 80))

# CWT scalogram (analytic Morlet, FFT-based)
analysis_freqs = np.geomspace(2.0, fs / 4, num=100)
f, t, W = dsp.cwt_scalogram(a1[:4096], fs, freqs=analysis_freqs)
dsp.plot_scalogram(f, t, W, ylim=(2, 80))

# Wigner-Ville (O(N²) — keep N small)
f, t, WVD = dsp.wigner_ville(a1[:512], fs)
dsp.plot_wvd(f, t, WVD, ylim=(0, 80))

Hilbert transform

# All three attributes in one call
env, phase, fi = dsp.hilbert_attributes(a1_bp, fs)

import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1, figsize=(10, 5), constrained_layout=True)
dsp.plot_signal(t, a1_bp, ax=axes[0], envelope=env, title="Bandpass + envelope")
axes[1].plot(t, fi); axes[1].set_ylabel("Instantaneous freq [Hz]")
plt.show()

EMD and HHT

# Decompose into IMFs
imfs, residue = dsp.emd(a1, max_imfs=8)

# Hilbert-Huang Transform
envs, inst_freqs = dsp.hht(imfs, fs)

# Marginal spectrum (adaptive PSD analogue)
freq_bins, marginal = dsp.hht_marginal_spectrum(envs, inst_freqs, fs, n_bins=512)

Peak detection

# Detect peaks in a PSD
peak_freqs, peak_vals, prominences = dsp.find_peaks(
    freqs, Pxx, distance_hz=5.0, max_peaks=5,
)
dsp.plot_peaks(freqs, Pxx, peak_freqs, peak_vals, db=True, xlim=(0, 80))

# Bandwidth and Q-factor
pf, bw, Q = dsp.peak_bandwidth(freqs, Pxx, peak_freqs=peak_freqs)

# Harmonic detection
hf, hv, orders = dsp.find_harmonics(freqs, amp, fundamental=25.0, n_harmonics=5)

SHM indicators

# Scalar indicators
se = dsp.spectral_entropy(freqs, Pxx)  # 0 = tonal, 1 = white noise
k = dsp.kurtosis(a1)                   # excess kurtosis (0 for Gaussian)
s = dsp.skewness(a1)                   # 0 for symmetric

# Time-varying indicators
times, rms_vals = dsp.rms_variation(a1, fs, segment_duration=10.0)
times, dom_freqs = dsp.frequency_shift(a1, fs, segment_duration=10.0)
times, energies = dsp.energy_variation(a1, fs, segment_duration=10.0)

dsp.plot_indicators(times, rms_vals, title="RMS Variation", ylabel="RMS")

Multi-sensor analysis

data = np.vstack([a1, a2])  # shape (n_channels, N)

# Correlation and coherence matrices
R = dsp.correlation_matrix(data)
freqs, C = dsp.coherence_matrix(data, fs, nperseg=4096)

# PSD matrix (input to FDD)
freqs, G = dsp.psd_matrix(data, fs, nperseg=4096)

dsp.plot_correlation_matrix(R, labels=["Ch1", "Ch2"])

FDD — Frequency Domain Decomposition

# Full OMA workflow
freqs, S, U = dsp.fdd_svd(data, fs, nperseg=4096)
dsp.plot_singular_values(freqs, S, db=True, xlim=(0, 80))

# Pick peaks → natural frequencies
peak_freqs, peak_idx = dsp.fdd_peak_picking(freqs, S, distance_hz=5.0, max_peaks=2)

# Extract mode shapes
modes = dsp.fdd_mode_shapes(U, peak_idx)
dsp.plot_mode_shape(modes[0], sensor_labels=["Mass 1", "Mass 2"])

# EFDD damping estimation
zeta, fn = dsp.efdd_damping(freqs, S, U, peak_idx, fs)

Probability and joint statistics

# PDF estimation
xi, density = dsp.pdf_estimate(a1)
dsp.plot_pdf(xi, density, hist_data=a1)

# Joint distribution
xc, yc, H = dsp.joint_histogram(a1, a2, bins=60)
dsp.plot_joint_histogram(xc, yc, H, xlabel="Ch1", ylabel="Ch2")

# Covariance and Mahalanobis distance
cov = dsp.covariance_matrix(data)
distances = dsp.mahalanobis(data)

Embedding plots in your own figures

Every plot_* function accepts an ax parameter:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(12, 8), constrained_layout=True)
gs = gridspec.GridSpec(2, 2, figure=fig)

dsp.plot_psd(freqs, Pxx, ax=fig.add_subplot(gs[0, 0]))
dsp.plot_coherence(freqs, Cxy, ax=fig.add_subplot(gs[0, 1]))
dsp.plot_spectrogram(f, t, Zxx, ax=fig.add_subplot(gs[1, :]))
plt.show()