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()andifft()Power spectrum with
power_spectrum()Spectrogram with
spectrogram()Wavelet transforms with
cwt()anddwt()
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
Signal Processing - Filter design and detection