Transforms

This example demonstrates FFT, power spectrum, wavelets, and other transforms.

Overview

Transform methods convert signals between domains:

  • Fourier Transform: Time to frequency domain

  • Short-Time Fourier: Time-frequency analysis

  • Wavelets: Multi-resolution analysis

  • Power Spectrum: Signal power distribution

Fourier Analysis

FFT (Fast Fourier Transform)
  • O(n log n) algorithm

  • Frequency content of signals

  • Foundation for spectral analysis

Power Spectrum
  • Signal power vs frequency

  • Periodogram estimation

  • Welch’s method for noise reduction

Spectrogram
  • Time-frequency representation

  • Short-time Fourier Transform

  • Frequency changes over time

Wavelet Analysis

Continuous Wavelet Transform (CWT)
  • Multi-scale analysis

  • Good time-frequency localization

  • Various mother wavelets

Discrete Wavelet Transform (DWT)
  • Efficient decomposition

  • Signal compression

  • Denoising applications

Code Highlights

The example demonstrates:

  • FFT with fft() and ifft()

  • Power spectrum with power_spectrum()

  • Spectrogram with spectrogram()

  • Wavelet transforms with cwt() and dwt()

Source Code

  1"""
  2Transforms Example.
  3
  4This example demonstrates:
  51. FFT and power spectrum analysis
  62. Short-time Fourier Transform (STFT)
  73. Spectrogram visualization
  84. Wavelet transforms (CWT, DWT)
  9
 10Run with: python examples/transforms.py
 11"""
 12
 13import sys
 14from pathlib import Path
 15
 16sys.path.insert(0, str(Path(__file__).parent.parent))
 17
 18import os
 19
 20import numpy as np  # noqa: E402
 21import plotly.graph_objects as go  # noqa: E402
 22from plotly.subplots import make_subplots  # noqa: E402
 23
 24from pytcl.mathematical_functions.transforms import (  # noqa: E402
 25    coherence,
 26    cross_spectrum,
 27    cwt,
 28    dwt,
 29    fft,
 30    idwt,
 31    ifft,
 32    istft,
 33    morlet_wavelet,
 34    power_spectrum,
 35    rfft,
 36    ricker_wavelet,
 37    spectrogram,
 38    stft,
 39)
 40
 41SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 42OUTPUT_DIR = Path(__file__).resolve().parent / "output"
 43
 44
 45def fft_demo() -> None:
 46    """Demonstrate FFT operations."""
 47    print("=" * 60)
 48    print("1. FFT AND SPECTRAL ANALYSIS")
 49    print("=" * 60)
 50
 51    np.random.seed(42)
 52
 53    # Create a test signal with known frequency content
 54    fs = 1000.0  # Sample rate
 55    t = np.linspace(0, 1, int(fs), endpoint=False)
 56
 57    # Multi-tone signal
 58    f1, f2, f3 = 50, 120, 200  # Hz
 59    signal = np.sin(2 * np.pi * f1 * t) + 0.5 * np.sin(2 * np.pi * f2 * t)
 60    signal += 0.25 * np.sin(2 * np.pi * f3 * t)
 61
 62    print(f"\nTest signal: sum of sinusoids at {f1}, {f2}, {f3} Hz")
 63    print(f"  Sample rate: {fs} Hz")
 64    print(f"  Duration: 1 second ({len(t)} samples)")
 65
 66    # Compute FFT
 67    X = fft(signal)
 68    freqs = np.fft.fftfreq(len(signal), 1 / fs)
 69
 70    print("\nFFT Results:")
 71    print(f"  FFT length: {len(X)}")
 72    print(f"  Frequency resolution: {fs / len(signal):.2f} Hz")
 73
 74    # Find peaks in positive frequencies
 75    pos_mask = freqs >= 0
 76    pos_freqs = freqs[pos_mask]
 77    pos_mag = np.abs(X[pos_mask])
 78
 79    # Find significant peaks
 80    peak_threshold = 0.1 * np.max(pos_mag)
 81    print("\nSignificant frequency peaks:")
 82    for i in range(1, len(pos_mag) - 1):
 83        if pos_mag[i] > pos_mag[i - 1] and pos_mag[i] > pos_mag[i + 1]:
 84            if pos_mag[i] > peak_threshold:
 85                print(f"  {pos_freqs[i]:.1f} Hz: magnitude = {pos_mag[i]:.2f}")
 86
 87    # Verify inverse FFT
 88    signal_recovered = ifft(X).real
 89    reconstruction_error = np.max(np.abs(signal - signal_recovered))
 90    print(f"\nIFFT reconstruction error: {reconstruction_error:.2e}")
 91
 92    # Real FFT (more efficient for real signals)
 93    print("\nReal FFT (rfft):")
 94    X_real = rfft(signal)
 95    print(f"  rfft length: {len(X_real)} (vs {len(X)} for full fft)")
 96    print(f"  Memory savings: {100 * (1 - len(X_real) / len(X)):.1f}%")
 97
 98
 99def power_spectrum_demo() -> None:
100    """Demonstrate power spectrum estimation."""
101    print("\n" + "=" * 60)
102    print("2. POWER SPECTRUM ESTIMATION")
103    print("=" * 60)
104
105    np.random.seed(42)
106
107    # Create a noisy signal with known spectrum
108    fs = 1000.0
109    t = np.linspace(0, 2, int(2 * fs), endpoint=False)
110
111    # Signal with two frequency components plus noise
112    signal = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t)
113    signal += 0.5 * np.random.randn(len(t))
114
115    print("\nSignal: 50 Hz + 120 Hz sinusoids in noise")
116    print(f"  SNR (approx): {10 * np.log10(1.25 / 0.25):.1f} dB")
117
118    # Welch's method power spectrum
119    ps = power_spectrum(signal, fs, window="hann", nperseg=256)
120
121    print("\nPower Spectrum (Welch's method):")
122    print(f"  Number of frequency bins: {len(ps.frequencies)}")
123    print(f"  Frequency resolution: {ps.frequencies[1] - ps.frequencies[0]:.2f} Hz")
124
125    # Find peaks
126    psd_db = 10 * np.log10(np.maximum(ps.psd, 1e-10))
127    print("\nPeak frequencies:")
128    for i in range(1, len(psd_db) - 1):
129        if psd_db[i] > psd_db[i - 1] and psd_db[i] > psd_db[i + 1]:
130            if psd_db[i] > np.max(psd_db) - 20:
131                print(f"  {ps.frequencies[i]:.1f} Hz: {psd_db[i]:.1f} dB")
132
133
134def cross_spectrum_demo() -> None:
135    """Demonstrate cross-spectrum and coherence."""
136    print("\n" + "=" * 60)
137    print("3. CROSS-SPECTRUM AND COHERENCE")
138    print("=" * 60)
139
140    np.random.seed(42)
141
142    # Create two related signals
143    fs = 1000.0
144    t = np.linspace(0, 2, int(2 * fs), endpoint=False)
145
146    # Common component
147    common = np.sin(2 * np.pi * 50 * t)
148
149    # Independent noise
150    noise1 = 0.5 * np.random.randn(len(t))
151    noise2 = 0.5 * np.random.randn(len(t))
152
153    # Signal 1: common + independent noise
154    x = common + noise1
155    # Signal 2: common (delayed) + independent noise
156    delay_samples = 10
157    y = np.roll(common, delay_samples) + noise2
158
159    print("\nTwo signals with common 50 Hz component:")
160    print("  Signal x: 50 Hz + noise")
161    print(f"  Signal y: 50 Hz (delayed {delay_samples} samples) + noise")
162
163    # Cross-spectrum
164    csd = cross_spectrum(x, y, fs, window="hann")
165    print("\nCross-spectrum:")
166    print(f"  Frequency bins: {len(csd.frequencies)}")
167
168    # Find phase at 50 Hz
169    idx_50hz = np.argmin(np.abs(csd.frequencies - 50))
170    phase_50hz = np.angle(csd.csd[idx_50hz])
171    expected_phase = -2 * np.pi * 50 * delay_samples / fs
172    print("\nPhase at 50 Hz:")
173    print(f"  Measured: {np.degrees(phase_50hz):.1f} deg")
174    print(f"  Expected: {np.degrees(expected_phase):.1f} deg")
175
176    # Coherence
177    coh = coherence(x, y, fs, nperseg=256)
178    print("\nCoherence at 50 Hz:")
179    idx_coh = np.argmin(np.abs(coh.frequencies - 50))
180    print(f"  Coherence: {coh.coherence[idx_coh]:.3f}")
181    print("  (1.0 = perfectly correlated, 0.0 = uncorrelated)")
182
183
184def stft_demo() -> None:
185    """Demonstrate Short-Time Fourier Transform."""
186    print("\n" + "=" * 60)
187    print("4. SHORT-TIME FOURIER TRANSFORM (STFT)")
188    print("=" * 60)
189
190    np.random.seed(42)
191
192    # Create a chirp signal (frequency varies with time)
193    fs = 1000.0
194    duration = 2.0
195    t = np.linspace(0, duration, int(duration * fs), endpoint=False)
196
197    # Linear chirp from 50 Hz to 200 Hz
198    f0, f1 = 50, 200
199    chirp = np.sin(2 * np.pi * (f0 + (f1 - f0) / (2 * duration) * t) * t)
200
201    print(f"\nChirp signal: frequency sweeps from {f0} to {f1} Hz")
202    print(f"  Duration: {duration} seconds")
203    print(f"  Sample rate: {fs} Hz")
204
205    # Compute STFT
206    nperseg = 128
207    noverlap = nperseg // 2
208    result = stft(chirp, fs, window="hann", nperseg=nperseg, noverlap=noverlap)
209
210    print("\nSTFT Results:")
211    print(f"  Segment length: {nperseg} samples")
212    print(f"  Overlap: {noverlap} samples")
213    print(f"  Number of time frames: {len(result.times)}")
214    print(f"  Number of frequency bins: {len(result.frequencies)}")
215    print(f"  Time resolution: {result.times[1] - result.times[0]:.3f} s")
216    print(
217        f"  Frequency resolution: {result.frequencies[1] - result.frequencies[0]:.2f} Hz"
218    )
219
220    # Verify reconstruction with inverse STFT
221    t_rec, reconstructed = istft(
222        result.Zxx, fs, window="hann", nperseg=nperseg, noverlap=noverlap
223    )
224    min_len = min(len(chirp), len(reconstructed))
225    recon_error = np.sqrt(np.mean((chirp[:min_len] - reconstructed[:min_len]) ** 2))
226    print(f"\nReconstruction RMS error: {recon_error:.6f}")
227
228    # Track instantaneous frequency over time
229    print("\nInstantaneous frequency tracking:")
230    for i in range(0, len(result.times), len(result.times) // 5):
231        # Find peak frequency at this time
232        mag = np.abs(result.Zxx[:, i])
233        peak_idx = np.argmax(mag)
234        peak_freq = result.frequencies[peak_idx]
235        expected_freq = f0 + (f1 - f0) * result.times[i] / duration
236        print(
237            f"  t={result.times[i]:.2f}s: measured={peak_freq:.1f} Hz, "
238            f"expected={expected_freq:.1f} Hz"
239        )
240
241
242def spectrogram_demo() -> None:
243    """Demonstrate spectrogram computation."""
244    print("\n" + "=" * 60)
245    print("5. SPECTROGRAM")
246    print("=" * 60)
247
248    np.random.seed(42)
249
250    # Create a signal with time-varying frequency content
251    fs = 1000.0
252    duration = 3.0
253    t = np.linspace(0, duration, int(duration * fs), endpoint=False)
254
255    # Three segments with different frequencies
256    signal = np.zeros_like(t)
257    signal[t < 1] = np.sin(2 * np.pi * 50 * t[t < 1])  # 50 Hz
258    signal[(t >= 1) & (t < 2)] = np.sin(
259        2 * np.pi * 100 * t[(t >= 1) & (t < 2)]
260    )  # 100 Hz
261    signal[t >= 2] = np.sin(2 * np.pi * 150 * t[t >= 2])  # 150 Hz
262
263    print("\nTime-varying signal:")
264    print("  0-1 s: 50 Hz")
265    print("  1-2 s: 100 Hz")
266    print("  2-3 s: 150 Hz")
267
268    # Compute spectrogram
269    spec = spectrogram(signal, fs, window="hann", nperseg=256, noverlap=128)
270
271    print("\nSpectrogram dimensions:")
272    print(f"  Time bins: {len(spec.times)}")
273    print(f"  Frequency bins: {len(spec.frequencies)}")
274
275    # Find dominant frequency in each time segment
276    print("\nDominant frequencies by segment:")
277    time_points = [0.5, 1.5, 2.5]  # Center of each segment
278    for tp in time_points:
279        idx = np.argmin(np.abs(spec.times - tp))
280        power_slice = spec.power[:, idx]
281        peak_idx = np.argmax(power_slice)
282        print(f"  t={tp}s: {spec.frequencies[peak_idx]:.1f} Hz")
283
284
285def wavelet_demo() -> None:
286    """Demonstrate wavelet transforms."""
287    print("\n" + "=" * 60)
288    print("6. WAVELET TRANSFORMS")
289    print("=" * 60)
290
291    np.random.seed(42)
292
293    # Create a signal with a transient event
294    fs = 1000.0
295    t = np.linspace(0, 1, int(fs), endpoint=False)
296
297    # Background oscillation + transient pulse
298    signal = 0.5 * np.sin(2 * np.pi * 10 * t)  # 10 Hz background
299    # Add transient at t=0.5s
300    transient_center = 0.5
301    transient_width = 0.02
302    transient = np.exp(-((t - transient_center) ** 2) / (2 * transient_width**2))
303    signal += transient
304
305    print("\nTest signal: 10 Hz oscillation + Gaussian pulse at t=0.5s")
306
307    # Generate wavelet shapes
308    print("\nWavelet shapes:")
309    morlet = morlet_wavelet(64, w=5.0)
310    ricker = ricker_wavelet(64, a=8.0)
311    print(f"  Morlet wavelet: {len(morlet)} points")
312    print(f"  Ricker (Mexican hat) wavelet: {len(ricker)} points")
313
314    # Continuous Wavelet Transform
315    scales = np.arange(1, 64)
316    cwt_result = cwt(signal, scales, wavelet="morlet", fs=fs)
317
318    print("\nContinuous Wavelet Transform (CWT):")
319    print(f"  Number of scales: {len(cwt_result.scales)}")
320    print(
321        f"  Frequency range: {cwt_result.frequencies[-1]:.1f} to {cwt_result.frequencies[0]:.1f} Hz"
322    )
323
324    # Find the transient in CWT
325    cwt_mag = np.abs(cwt_result.coefficients)
326    max_idx = np.unravel_index(np.argmax(cwt_mag), cwt_mag.shape)
327    peak_time = t[max_idx[1]]
328    peak_freq = cwt_result.frequencies[max_idx[0]]
329    print("\nTransient detection:")
330    print(f"  Peak at time: {peak_time:.3f} s (true: {transient_center} s)")
331    print(f"  Peak frequency: {peak_freq:.1f} Hz")
332
333    # Discrete Wavelet Transform
334    print("\nDiscrete Wavelet Transform (DWT):")
335    dwt_result = dwt(signal, wavelet="db4", level=4)
336    print("  Wavelet: Daubechies 4 (db4)")
337    print(f"  Decomposition levels: {dwt_result.levels}")
338    print(f"  Approximation coefficients: {len(dwt_result.cA)} samples")
339    for i, cD in enumerate(dwt_result.cD):
340        print(f"  Detail level {i + 1}: {len(cD)} samples")
341
342    # Verify reconstruction
343    reconstructed = idwt(dwt_result)
344    min_len = min(len(signal), len(reconstructed))
345    recon_error = np.sqrt(np.mean((signal[:min_len] - reconstructed[:min_len]) ** 2))
346    print(f"\nDWT reconstruction RMS error: {recon_error:.6f}")
347
348
349def main() -> None:
350    """Run transform demonstrations."""
351    print("\nTransforms Examples")
352    print("=" * 60)
353    print("Demonstrating pytcl transform capabilities")
354
355    fft_demo()
356    power_spectrum_demo()
357    cross_spectrum_demo()
358    stft_demo()
359    spectrogram_demo()
360    wavelet_demo()
361
362    # Visualization
363    visualize_fft_analysis()
364
365    print("\n" + "=" * 60)
366    print("Done!")
367    print("=" * 60)
368
369
370def visualize_fft_analysis() -> None:
371    """Visualize FFT analysis of multi-frequency signal."""
372    print("\nGenerating FFT analysis visualization...")
373
374    # Create test signal
375    fs = 1000.0
376    t = np.linspace(0, 1, int(fs), endpoint=False)
377    f1, f2, f3 = 50, 120, 200
378
379    signal = (
380        np.sin(2 * np.pi * f1 * t)
381        + 0.5 * np.sin(2 * np.pi * f2 * t)
382        + 0.25 * np.sin(2 * np.pi * f3 * t)
383    )
384    signal += 0.1 * np.random.randn(len(t))
385
386    # Compute FFT
387    X = fft(signal)
388    freqs = np.fft.fftfreq(len(signal), 1 / fs)
389
390    # Only positive frequencies
391    pos_mask = freqs >= 0
392    pos_freqs = freqs[pos_mask]
393    pos_mag = np.abs(X[pos_mask])
394
395    fig = make_subplots(
396        rows=1,
397        cols=2,
398        subplot_titles=("Time Domain Signal", "Frequency Domain (FFT)"),
399    )
400
401    # Time domain
402    fig.add_trace(
403        go.Scatter(
404            x=t,
405            y=signal,
406            mode="lines",
407            name="Signal",
408            line=dict(color="blue", width=1),
409        ),
410        row=1,
411        col=1,
412    )
413
414    # Frequency domain
415    fig.add_trace(
416        go.Scatter(
417            x=pos_freqs[:500],
418            y=pos_mag[:500],
419            mode="lines",
420            name="Magnitude",
421            line=dict(color="red", width=1),
422        ),
423        row=1,
424        col=2,
425    )
426
427    fig.update_xaxes(title_text="Time (s)", row=1, col=1)
428    fig.update_yaxes(title_text="Amplitude", row=1, col=1)
429    fig.update_xaxes(title_text="Frequency (Hz)", row=1, col=2)
430    fig.update_yaxes(title_text="Magnitude", row=1, col=2)
431
432    fig.update_layout(
433        title="FFT Analysis: Multi-Frequency Signal",
434        height=500,
435        width=1000,
436        showlegend=False,
437    )
438
439    if SHOW_PLOTS:
440        fig.show()
441    else:
442        OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
443        fig.write_html(
444            str(OUTPUT_DIR / "transforms.html"),
445            include_plotlyjs="cdn",
446            div_id="transforms",
447        )
448
449
450if __name__ == "__main__":
451    main()

Running the Example

python examples/transforms.py

See Also