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