Signal Processing
This example demonstrates digital filter design, matched filtering, and CFAR detection.
Overview
Signal processing fundamentals for radar and tracking:
Digital filters: FIR and IIR filter design
Matched filtering: Optimal detection in noise
CFAR detection: Constant False Alarm Rate processing
Spectral analysis: FFT, power spectrum, spectrograms
Digital Filters
- FIR Filters
Finite Impulse Response
Linear phase available
Always stable
- IIR Filters (Butterworth)
Infinite Impulse Response
Efficient implementation
Maximally flat passband
Matched Filtering
Matched filters maximize SNR for known waveforms:
Correlates signal with template
Optimal for white Gaussian noise
Pulse compression for radar
CFAR Detection
CFAR maintains constant false alarm rate:
CA-CFAR: Cell-averaging
GO-CFAR: Greatest-of
OS-CFAR: Ordered-statistic
Adaptive threshold estimation
Code Highlights
The example demonstrates:
Butterworth filter design with
butter_design()FIR filter design with
fir_design()Matched filtering with
matched_filter()CA-CFAR with
cfar_ca()Power spectrum estimation
Source Code
1"""
2Signal Processing Example.
3
4This example demonstrates:
51. Digital filter design (Butterworth, Chebyshev, FIR)
62. Matched filtering for pulse detection
73. CFAR (Constant False Alarm Rate) detection
84. Power spectrum analysis
9
10Run with: python examples/signal_processing.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.signal_processing import ( # noqa: E402
25 apply_filter,
26 butter_design,
27 cfar_ca,
28 cfar_go,
29 cfar_os,
30 cfar_so,
31 cheby1_design,
32 detection_probability,
33 filtfilt,
34 fir_design,
35 frequency_response,
36 matched_filter,
37 pulse_compression,
38 threshold_factor,
39)
40
41SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
42OUTPUT_DIR = Path(__file__).resolve().parent / "output"
43
44
45def filter_design_demo() -> None:
46 """Demonstrate digital filter design."""
47 print("=" * 60)
48 print("1. DIGITAL FILTER DESIGN")
49 print("=" * 60)
50
51 # Sample rate
52 fs = 1000.0 # Hz
53
54 # Design Butterworth lowpass filter
55 print("\nButterworth Lowpass Filter (order=4, cutoff=50 Hz):")
56 butter_filt = butter_design(order=4, cutoff=50.0, fs=fs, btype="low")
57 print(f" Numerator coefficients (b): {butter_filt.b[:3]}...")
58 print(f" Denominator coefficients (a): {butter_filt.a[:3]}...")
59
60 # Design Chebyshev Type I filter
61 print("\nChebyshev Type I Lowpass (order=4, ripple=1dB, cutoff=50 Hz):")
62 cheby_filt = cheby1_design(order=4, ripple=1.0, cutoff=50.0, fs=fs, btype="low")
63 print(f" Numerator coefficients (b): {cheby_filt.b[:3]}...")
64
65 # Design FIR filter
66 print("\nFIR Lowpass Filter (numtaps=51, cutoff=50 Hz):")
67 fir_coeffs = fir_design(numtaps=51, cutoff=50.0, fs=fs, window="hamming")
68 print(f" Number of taps: {len(fir_coeffs)}")
69 print(f" Center tap value: {fir_coeffs[25]:.6f}")
70
71 # Compare frequency responses
72 print("\nFrequency Response Comparison:")
73 butter_resp = frequency_response(butter_filt.b, butter_filt.a, fs)
74 cheby_resp = frequency_response(cheby_filt.b, cheby_filt.a, fs)
75
76 # Find -3dB point for Butterworth
77 butter_mag_db = 20 * np.log10(np.maximum(butter_resp.magnitude, 1e-10))
78 idx_3db = np.argmin(np.abs(butter_mag_db + 3))
79 print(f" Butterworth -3dB frequency: {butter_resp.frequencies[idx_3db]:.1f} Hz")
80
81 # Find -3dB point for Chebyshev
82 cheby_mag_db = 20 * np.log10(np.maximum(cheby_resp.magnitude, 1e-10))
83 idx_3db = np.argmin(np.abs(cheby_mag_db + 3))
84 print(f" Chebyshev -3dB frequency: {cheby_resp.frequencies[idx_3db]:.1f} Hz")
85
86
87def filtering_demo() -> None:
88 """Demonstrate signal filtering."""
89 print("\n" + "=" * 60)
90 print("2. SIGNAL FILTERING")
91 print("=" * 60)
92
93 np.random.seed(42)
94
95 # Create a test signal: 10 Hz sine + 60 Hz noise
96 fs = 1000.0
97 t = np.linspace(0, 1, int(fs), endpoint=False)
98 signal_clean = np.sin(2 * np.pi * 10 * t) # 10 Hz signal
99 noise = 0.5 * np.sin(2 * np.pi * 60 * t) # 60 Hz noise
100 signal_noisy = signal_clean + noise + 0.1 * np.random.randn(len(t))
101
102 print("\nTest signal: 10 Hz sine wave + 60 Hz interference + white noise")
103 print(f" Sample rate: {fs} Hz")
104 print(f" Duration: 1 second ({len(t)} samples)")
105
106 # Design 30 Hz lowpass filter to remove 60 Hz noise
107 filt = butter_design(order=4, cutoff=30.0, fs=fs, btype="low")
108
109 # Apply filter using different methods
110 print("\nFiltering methods:")
111
112 # Standard filter (has phase shift)
113 _filtered_standard = apply_filter(filt, signal_noisy) # noqa: F841
114 print(" Standard filtering: introduces phase shift")
115
116 # Zero-phase filter (no phase shift)
117 filtered_zerophase = filtfilt(filt, signal_noisy)
118 print(" Zero-phase filtering: no phase shift (filtfilt)")
119
120 # Compute SNR improvement
121 noise_power_before = np.var(signal_noisy - signal_clean)
122 noise_power_after = np.var(filtered_zerophase - signal_clean)
123 snr_improvement = 10 * np.log10(noise_power_before / noise_power_after)
124 print(f"\n SNR improvement: {snr_improvement:.1f} dB")
125
126 # RMS error
127 rms_before = np.sqrt(np.mean((signal_noisy - signal_clean) ** 2))
128 rms_after = np.sqrt(np.mean((filtered_zerophase - signal_clean) ** 2))
129 print(f" RMS error before filtering: {rms_before:.4f}")
130 print(f" RMS error after filtering: {rms_after:.4f}")
131
132
133def matched_filter_demo() -> None:
134 """Demonstrate matched filtering for pulse detection."""
135 print("\n" + "=" * 60)
136 print("3. MATCHED FILTERING")
137 print("=" * 60)
138
139 np.random.seed(42)
140
141 # Create a chirp pulse
142 fs = 10000.0 # Sample rate
143 T = 0.01 # Pulse duration (10 ms)
144 f0 = 500 # Start frequency
145 f1 = 2000 # End frequency
146
147 t_pulse = np.linspace(0, T, int(T * fs), endpoint=False)
148 # Linear frequency sweep (chirp)
149 chirp = np.sin(2 * np.pi * (f0 + (f1 - f0) / (2 * T) * t_pulse) * t_pulse)
150
151 print("\nChirp pulse parameters:")
152 print(f" Duration: {T * 1000:.1f} ms")
153 print(f" Frequency sweep: {f0} Hz to {f1} Hz")
154 print(f" Bandwidth: {f1 - f0} Hz")
155 print(f" Time-bandwidth product: {(f1 - f0) * T:.1f}")
156
157 # Create received signal with delayed pulse in noise
158 n_samples = int(0.1 * fs) # 100 ms of data
159 received = 0.5 * np.random.randn(n_samples) # Noise
160
161 # Add pulse at known location with attenuation
162 pulse_location = 500
163 pulse_amplitude = 0.3
164 received[pulse_location : pulse_location + len(chirp)] += pulse_amplitude * chirp
165
166 # Matched filter
167 result = matched_filter(received, chirp, normalize=True)
168
169 print("\nMatched filter results:")
170 print(f" True pulse location: sample {pulse_location}")
171 print(f" Detected peak location: sample {result.peak_index}")
172 print(f" Detection error: {abs(result.peak_index - pulse_location)} samples")
173 print(f" Peak correlation value: {result.peak_value:.4f}")
174 print(f" SNR gain: {result.snr_gain:.1f} dB")
175
176 # Pulse compression
177 pc_result = pulse_compression(received, chirp)
178 compressed_peak = np.argmax(np.abs(pc_result.output))
179 print("\nPulse compression:")
180 print(f" Compressed pulse peak: sample {compressed_peak}")
181 print(f" Compression ratio: {pc_result.compression_ratio:.0f}:1")
182
183
184def cfar_detection_demo() -> None:
185 """Demonstrate CFAR detection algorithms."""
186 print("\n" + "=" * 60)
187 print("4. CFAR DETECTION")
188 print("=" * 60)
189
190 np.random.seed(42)
191
192 # Create a range profile with targets
193 n_cells = 200
194 noise_power = 1.0
195 noise = np.sqrt(noise_power) * np.abs(np.random.randn(n_cells))
196
197 # Add targets at known locations
198 targets = [
199 (50, 15.0), # Location, amplitude (strong target)
200 (100, 8.0), # Medium target
201 (150, 5.0), # Weak target
202 ]
203
204 signal = noise.copy()
205 for loc, amp in targets:
206 signal[loc] = amp
207
208 print("\nSimulated range profile:")
209 print(f" {n_cells} range cells")
210 print(f" Noise power: {noise_power:.1f}")
211 print(f" Targets at cells: {[t[0] for t in targets]}")
212 print(f" Target amplitudes: {[t[1] for t in targets]}")
213
214 # CFAR parameters
215 guard_cells = 2
216 ref_cells = 8
217 pfa = 1e-4 # Probability of false alarm
218
219 print("\nCFAR parameters:")
220 print(f" Guard cells: {guard_cells}")
221 print(f" Reference cells: {ref_cells}")
222 print(f" Pfa: {pfa}")
223
224 # Compute threshold factor
225 alpha = threshold_factor(pfa, ref_cells, method="ca")
226 print(f" Threshold factor (CA-CFAR): {alpha:.2f}")
227
228 # Run different CFAR algorithms
229 print("\nCFAR Detection Results:")
230 print("-" * 60)
231
232 # Cell-Averaging CFAR
233 ca_result = cfar_ca(signal, guard_cells, ref_cells, pfa)
234 ca_detections = ca_result.detection_indices
235 print("\nCA-CFAR (Cell-Averaging):")
236 print(f" Detections: {ca_detections.tolist()}")
237 print(
238 f" Targets detected: {len(set(ca_detections) & {t[0] for t in targets})}/{len(targets)}"
239 )
240
241 # Greatest-Of CFAR (good at clutter edges)
242 go_result = cfar_go(signal, guard_cells, ref_cells, pfa)
243 go_detections = go_result.detection_indices
244 print("\nGO-CFAR (Greatest-Of):")
245 print(f" Detections: {go_detections.tolist()}")
246
247 # Smallest-Of CFAR (good in clutter)
248 so_result = cfar_so(signal, guard_cells, ref_cells, pfa)
249 so_detections = so_result.detection_indices
250 print("\nSO-CFAR (Smallest-Of):")
251 print(f" Detections: {so_detections.tolist()}")
252
253 # Order-Statistic CFAR
254 k = int(0.75 * ref_cells) # Use 75th percentile
255 os_result = cfar_os(signal, guard_cells, ref_cells, pfa, k)
256 os_detections = os_result.detection_indices
257 print(f"\nOS-CFAR (Order-Statistic, k={k}):")
258 print(f" Detections: {os_detections.tolist()}")
259
260 # Detection probability analysis
261 print("\nDetection Probability vs SNR:")
262 print("-" * 40)
263 snr_values = [5, 10, 15, 20]
264 for snr in snr_values:
265 pd = detection_probability(snr, pfa, ref_cells, method="ca")
266 print(f" SNR = {snr:2d} dB: Pd = {pd:.4f}")
267
268
269def spectrum_analysis_demo() -> None:
270 """Demonstrate power spectrum analysis."""
271 print("\n" + "=" * 60)
272 print("5. SPECTRUM ANALYSIS")
273 print("=" * 60)
274
275 np.random.seed(42)
276
277 # Create a multi-tone signal
278 fs = 1000.0
279 t = np.linspace(0, 1, int(fs), endpoint=False)
280
281 # Signal with known frequency components
282 frequencies = [50, 120, 200] # Hz
283 amplitudes = [1.0, 0.5, 0.3]
284
285 signal = np.zeros_like(t)
286 for f, a in zip(frequencies, amplitudes):
287 signal += a * np.sin(2 * np.pi * f * t)
288
289 # Add noise
290 signal += 0.2 * np.random.randn(len(t))
291
292 print("\nTest signal:")
293 print(f" Sample rate: {fs} Hz")
294 print(" Duration: 1 second")
295 print(f" Frequency components: {frequencies} Hz")
296 print(f" Amplitudes: {amplitudes}")
297
298 # Compute power spectrum using FFT
299 from pytcl.mathematical_functions.transforms import power_spectrum
300
301 ps = power_spectrum(signal, fs, window="hann", nperseg=256)
302
303 print("\nPower spectrum analysis:")
304 print(f" Frequency resolution: {fs / 256:.2f} Hz")
305
306 # Find peaks in spectrum
307 psd_db = 10 * np.log10(np.maximum(ps.psd, 1e-10))
308 peak_threshold = np.max(psd_db) - 20 # Peaks within 20 dB of max
309
310 print(f"\nDetected frequency peaks (>{peak_threshold:.1f} dB):")
311 for i in range(1, len(psd_db) - 1):
312 if psd_db[i] > psd_db[i - 1] and psd_db[i] > psd_db[i + 1]:
313 if psd_db[i] > peak_threshold:
314 print(f" {ps.frequencies[i]:.1f} Hz: {psd_db[i]:.1f} dB")
315
316
317def main() -> None:
318 """Run signal processing demonstrations."""
319 print("\nSignal Processing Examples")
320 print("=" * 60)
321 print("Demonstrating pytcl signal processing capabilities")
322
323 filter_design_demo()
324 filtering_demo()
325 matched_filter_demo()
326 cfar_detection_demo()
327 spectrum_analysis_demo()
328
329 # Visualization
330 visualize_filter_response()
331
332 print("\n" + "=" * 60)
333 print("Done!")
334 print("=" * 60)
335
336
337def visualize_filter_response() -> None:
338 """Visualize digital filter frequency response."""
339 print("\nGenerating filter response visualization...")
340
341 fs = 1000.0
342 butter_filt = butter_design(order=4, cutoff=50.0, fs=fs, btype="low")
343 fir_filt_coeffs = fir_design(numtaps=64, cutoff=50.0, fs=fs)
344
345 # Compute magnitude responses
346 butter_resp = frequency_response(butter_filt, fs)
347 fir_resp = frequency_response(fir_filt_coeffs, fs)
348
349 fig = make_subplots(specs=[[{"secondary_y": False}]])
350
351 fig.add_trace(
352 go.Scatter(
353 x=butter_resp.frequencies,
354 y=20 * np.log10(np.maximum(butter_resp.magnitude, 1e-10)),
355 mode="lines",
356 name="Butterworth (4th order)",
357 line=dict(color="blue", width=2),
358 )
359 )
360
361 fig.add_trace(
362 go.Scatter(
363 x=fir_resp.frequencies,
364 y=20 * np.log10(np.maximum(fir_resp.magnitude, 1e-10)),
365 mode="lines",
366 name="FIR (order 64)",
367 line=dict(color="red", width=2, dash="dash"),
368 )
369 )
370
371 fig.update_layout(
372 title="Digital Filter Frequency Response Comparison",
373 xaxis_title="Frequency (Hz)",
374 yaxis_title="Magnitude (dB)",
375 height=500,
376 width=800,
377 hovermode="x unified",
378 )
379
380 if SHOW_PLOTS:
381 fig.show()
382 else:
383 OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
384 fig.write_html(
385 str(OUTPUT_DIR / "signal_processing.html"),
386 include_plotlyjs="cdn",
387 div_id="signal_processing",
388 )
389
390
391if __name__ == "__main__":
392 main()
Running the Example
python examples/signal_processing.py
See Also
Transforms - FFT and spectral analysis