Signal Processing
Digital signal processing functions for radar and sensor applications.
Signal processing utilities.
This module provides signal processing functions for target tracking and radar applications, including: - Digital filter design (IIR and FIR) - Matched filtering for signal detection - CFAR (Constant False Alarm Rate) detection algorithms
- class pytcl.mathematical_functions.signal_processing.FilterCoefficients(b, a, sos)[source]
Bases:
NamedTupleFilter coefficients from IIR filter design.
- b
Numerator (feedforward) coefficients.
- Type:
ndarray
- a
Denominator (feedback) coefficients.
- Type:
ndarray
- sos
Second-order sections representation (more numerically stable).
- Type:
ndarray or None
- class pytcl.mathematical_functions.signal_processing.FrequencyResponse(frequencies, magnitude, phase)[source]
Bases:
NamedTupleFrequency response of a digital filter.
- frequencies
Frequency values in Hz.
- Type:
ndarray
- magnitude
Magnitude response (linear scale).
- Type:
ndarray
- phase
Phase response in radians.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.butter_design(order, cutoff, fs, btype='low', output='sos')[source]
Design a Butterworth digital filter.
The Butterworth filter has maximally flat frequency response in the passband. It is often used as a “standard” filter when sharp transitions are not required.
- Parameters:
order (int) – Filter order.
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass/bandstop, provide (low, high).
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. ‘sos’ is recommended for stability. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients (b, a, sos).
- Return type:
Examples
>>> fs = 1000 # 1 kHz >>> coeffs = butter_design(4, 100, fs, btype='low') >>> coeffs.sos.shape[0] # Number of second-order sections 2
Notes
The Butterworth filter has no ripple in the passband or stopband. The -3 dB point occurs at the cutoff frequency.
- pytcl.mathematical_functions.signal_processing.cheby1_design(order, ripple, cutoff, fs, btype='low', output='sos')[source]
Design a Chebyshev Type I digital filter.
The Chebyshev Type I filter has equiripple in the passband and monotonic in the stopband. It provides a sharper transition than Butterworth for the same order.
- Parameters:
order (int) – Filter order.
ripple (float) – Maximum passband ripple in dB.
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass/bandstop, provide (low, high).
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = cheby1_design(4, 0.5, 100, fs) # 0.5 dB ripple >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.cheby2_design(order, attenuation, cutoff, fs, btype='low', output='sos')[source]
Design a Chebyshev Type II digital filter.
The Chebyshev Type II filter has equiripple in the stopband and monotonic in the passband. The cutoff frequency is the stopband edge.
- Parameters:
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = cheby2_design(4, 40, 100, fs) # 40 dB stopband attenuation >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.ellip_design(order, passband_ripple, stopband_attenuation, cutoff, fs, btype='low', output='sos')[source]
Design an elliptic (Cauer) digital filter.
The elliptic filter has equiripple in both passband and stopband. It achieves the sharpest transition for a given order but at the cost of ripple in both bands.
- Parameters:
order (int) – Filter order.
passband_ripple (float) – Maximum passband ripple in dB.
stopband_attenuation (float) – Minimum stopband attenuation in dB.
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = ellip_design(4, 0.5, 40, 100, fs) >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.bessel_design(order, cutoff, fs, btype='low', norm='phase', output='sos')[source]
Design a Bessel/Thomson digital filter.
The Bessel filter is designed for a maximally flat group delay, making it ideal for preserving the waveshape of filtered signals.
- Parameters:
order (int) – Filter order.
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
norm ({'phase', 'delay', 'mag'}, optional) – Normalization type. Default is ‘phase’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = bessel_design(4, 100, fs) >>> len(coeffs.b) > 0 True
Notes
The Bessel filter has a slower roll-off than Butterworth, but preserves the shape of signals in the passband due to its flat group delay.
- pytcl.mathematical_functions.signal_processing.fir_design(numtaps, cutoff, fs, window='hamming', pass_zero=True)[source]
Design an FIR filter using the window method.
- Parameters:
numtaps (int) – Length of the filter (number of coefficients). Must be odd for Type I filter (pass_zero=True, lowpass).
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass, provide (low, high).
fs (float) – Sampling frequency in Hz.
window (str, optional) – Window function to use. Default is ‘hamming’.
pass_zero (bool or {'bandpass', 'lowpass', 'highpass', 'bandstop'}, optional) – If True, gain at zero frequency is 1. Default is True.
- Returns:
h – FIR filter coefficients.
- Return type:
ndarray
Examples
>>> fs = 1000 >>> h = fir_design(101, 100, fs) # 100 Hz lowpass >>> len(h) 101
Notes
FIR filters are always stable and have linear phase when the coefficients are symmetric. However, they typically require higher order than IIR filters for the same transition sharpness.
- pytcl.mathematical_functions.signal_processing.fir_design_remez(numtaps, bands, desired, fs, weight=None)[source]
Design an optimal FIR filter using the Remez exchange algorithm.
The Parks-McClellan (Remez) algorithm designs an optimal equiripple FIR filter that minimizes the maximum error between the desired and actual frequency response.
- Parameters:
numtaps (int) – Length of the filter.
bands (array_like) – Band edges in Hz. Must be monotonically increasing with an even number of elements.
desired (array_like) – Desired gain in each band (one value per band).
fs (float) – Sampling frequency in Hz.
weight (array_like, optional) – Weight for each band. Default is equal weight.
- Returns:
h – FIR filter coefficients.
- Return type:
ndarray
Examples
>>> fs = 1000 >>> # Lowpass: passband 0-100 Hz, stopband 150-500 Hz >>> h = fir_design_remez(101, [0, 100, 150, 500], [1, 0], fs) >>> len(h) 101
- pytcl.mathematical_functions.signal_processing.apply_filter(coeffs, x, zi=None)[source]
Apply a digital filter to a signal.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients. Can be: - FilterCoefficients (uses sos if available) - Tuple (b, a) for IIR filter - 1D array for FIR filter
x (array_like) – Input signal.
zi (array_like, optional) – Initial filter state. If provided, returns (output, final_state).
- Returns:
y – Filtered signal. If zi is provided, returns (y, zf).
- Return type:
ndarray or tuple
Examples
>>> import numpy as np >>> fs = 1000 >>> t = np.arange(0, 1, 1/fs) >>> x = np.sin(2 * np.pi * 50 * t) + np.sin(2 * np.pi * 200 * t) >>> coeffs = butter_design(4, 100, fs) # 100 Hz lowpass >>> y = apply_filter(coeffs, x) >>> len(y) == len(x) True
- pytcl.mathematical_functions.signal_processing.filtfilt(coeffs, x, padtype='odd', padlen=None)[source]
Apply zero-phase forward-backward filtering.
The signal is filtered twice, once forward and once backward, which eliminates phase distortion but doubles the filter order.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
x (array_like) – Input signal.
padtype ({'odd', 'even', 'constant', None}, optional) – Type of padding to use. Default is ‘odd’.
padlen (int, optional) – Length of padding. Default is 3 * max(len(a), len(b)).
- Returns:
y – Zero-phase filtered signal.
- Return type:
ndarray
Examples
>>> import numpy as np >>> fs = 1000 >>> t = np.arange(0, 1, 1/fs) >>> x = np.sin(2 * np.pi * 50 * t) >>> coeffs = butter_design(4, 100, fs) >>> y = filtfilt(coeffs, x) >>> len(y) == len(x) True
Notes
Zero-phase filtering has no phase distortion but cannot be used for real-time applications since it requires the entire signal upfront.
- pytcl.mathematical_functions.signal_processing.frequency_response(coeffs, fs, n_points=512, whole=False)[source]
Compute the frequency response of a digital filter.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
fs (float) – Sampling frequency in Hz.
n_points (int, optional) – Number of frequency points. Default is 512.
whole (bool, optional) – If True, compute response from 0 to fs (instead of 0 to fs/2). Default is False.
- Returns:
response – Named tuple with frequencies, magnitude, and phase.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = butter_design(4, 100, fs) >>> response = frequency_response(coeffs, fs) >>> len(response.frequencies) == 512 True >>> response.magnitude[0] # DC gain 1.0
- pytcl.mathematical_functions.signal_processing.group_delay(coeffs, fs, n_points=512)[source]
Compute the group delay of a digital filter.
Group delay is the negative derivative of phase with respect to frequency, representing the time delay experienced by signal components at each frequency.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
fs (float) – Sampling frequency in Hz.
n_points (int, optional) – Number of frequency points. Default is 512.
- Returns:
frequencies (ndarray) – Frequency values in Hz.
gd (ndarray) – Group delay in samples.
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]]]
Examples
>>> fs = 1000 >>> h = fir_design(51, 100, fs) # Symmetric FIR >>> freqs, gd = group_delay(h, fs) >>> np.allclose(gd, 25) # Constant group delay = (N-1)/2 True
- pytcl.mathematical_functions.signal_processing.filter_order(passband_freq, stopband_freq, passband_ripple, stopband_attenuation, fs, filter_type='butter')[source]
Estimate the minimum filter order for given specifications.
- Parameters:
passband_freq (float) – Passband edge frequency in Hz.
stopband_freq (float) – Stopband edge frequency in Hz.
passband_ripple (float) – Maximum passband ripple in dB.
stopband_attenuation (float) – Minimum stopband attenuation in dB.
fs (float) – Sampling frequency in Hz.
filter_type ({'butter', 'cheby1', 'cheby2', 'ellip'}, optional) – Filter type. Default is ‘butter’.
- Returns:
order – Minimum filter order.
- Return type:
Examples
>>> order = filter_order(100, 150, 0.5, 40, 1000, 'butter') >>> order > 0 True
- pytcl.mathematical_functions.signal_processing.sos_to_zpk(sos)[source]
Convert second-order sections to zeros, poles, gain.
- Parameters:
sos (array_like) – Second-order sections array with shape (n_sections, 6).
- Returns:
z (ndarray) – Zeros.
p (ndarray) – Poles.
k (float) – Gain.
- Return type:
tuple[ndarray[tuple[Any, …], dtype[Any]], ndarray[tuple[Any, …], dtype[Any]], Any]
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... sos_to_zpk, butter_sos ... ) >>> # Design a Butterworth filter and convert to ZPK form >>> sos = butter_sos(4, 0.3) # 4th order Butterworth lowpass >>> z, p, k = sos_to_zpk(sos) >>> # Check filter stability: poles must be inside unit circle >>> np.all(np.abs(p) < 1.0) True >>> # Verify number of poles matches filter order >>> len(p) == 4 True
- pytcl.mathematical_functions.signal_processing.zpk_to_sos(z, p, k, pairing='nearest')[source]
Convert zeros, poles, gain to second-order sections.
- Parameters:
z (array_like) – Zeros.
p (array_like) – Poles.
k (float) – Gain.
pairing ({'nearest', 'keep_odd', 'minimal'}, optional) – Pole-zero pairing strategy. Default is ‘nearest’.
- Returns:
sos – Second-order sections array.
- Return type:
ndarray
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... zpk_to_sos ... ) >>> # Create a simple 2nd order Butterworth-like filter >>> z = np.array([-1.0, -1.0]) # Zeros at -1 >>> p = np.array([0.7071 * np.exp(1j * np.pi / 4), ... 0.7071 * np.exp(-1j * np.pi / 4)]) # Complex poles >>> k = 1.0 >>> sos = zpk_to_sos(z, p, k) >>> sos.shape (1, 6) >>> # Verify roundtrip conversion >>> from pytcl.mathematical_functions.signal_processing import sos_to_zpk >>> z2, p2, k2 = sos_to_zpk(sos) >>> np.allclose(z, z2) and np.allclose(p, p2) True
- class pytcl.mathematical_functions.signal_processing.MatchedFilterResult(output, peak_index, peak_value, snr_gain)[source]
Bases:
NamedTupleResult of matched filter operation.
- output
Matched filter output.
- Type:
ndarray
- class pytcl.mathematical_functions.signal_processing.PulseCompressionResult(output, peak_index, compression_ratio, peak_sidelobe_ratio)[source]
Bases:
NamedTupleResult of pulse compression.
- output
Compressed pulse output.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.matched_filter(signal, template, normalize=True, mode='same')[source]
Apply matched filtering in the time domain.
The matched filter maximizes the output signal-to-noise ratio (SNR) when the input contains a known signal plus white Gaussian noise.
- Parameters:
signal (array_like) – Input signal (may contain noise).
template (array_like) – Template signal to match (the known waveform).
normalize (bool, optional) – If True, normalize output by template energy. Default is True.
mode ({'full', 'same', 'valid'}, optional) – Convolution mode. Default is ‘same’.
- Returns:
result – Named tuple with filter output, peak location, and SNR gain.
- Return type:
Examples
>>> import numpy as np >>> # Create a signal with a pulse >>> template = np.array([1, 1, 1, 1, 1]) >>> signal = np.zeros(100) >>> signal[50:55] = template >>> result = matched_filter(signal, template) >>> 50 <= result.peak_index <= 54 True
Notes
The matched filter is the time-reversed, conjugated version of the template convolved with the signal. For real signals, this is equivalent to cross-correlation.
The theoretical SNR gain of a matched filter is equal to the number of samples in the template (for unit-energy template in unit-variance noise).
- pytcl.mathematical_functions.signal_processing.matched_filter_frequency(signal, template, fs=1.0, normalize=True)[source]
Apply matched filtering in the frequency domain.
This implementation uses FFT for efficient computation, especially for long signals and templates.
- Parameters:
- Returns:
result – Named tuple with filter output, peak location, and SNR gain.
- Return type:
Examples
>>> import numpy as np >>> template = np.sin(2 * np.pi * 0.1 * np.arange(50)) >>> signal = np.zeros(200) >>> signal[100:150] = template >>> result = matched_filter_frequency(signal, template) >>> 100 <= result.peak_index <= 150 True
Notes
For long signals, frequency-domain matched filtering is more efficient as it uses O(N log N) FFT operations instead of O(N^2) convolution.
- pytcl.mathematical_functions.signal_processing.optimal_filter(signal, template, noise_psd, fs=1.0)[source]
Apply optimal filtering for colored noise.
The optimal filter (also called the Wiener filter) maximizes SNR when the noise is not white (colored noise). It pre-whitens the noise before matched filtering.
- Parameters:
signal (array_like) – Input signal.
template (array_like) – Template signal to match.
noise_psd (array_like) – Noise power spectral density (must be same length as FFT).
fs (float, optional) – Sampling frequency. Default is 1.0.
- Returns:
output – Optimal filter output.
- Return type:
ndarray
Examples
>>> import numpy as np >>> # White noise case (simple matched filter) >>> signal = np.random.randn(256) >>> template = np.ones(16) >>> noise_psd = np.ones(256) # White noise (flat PSD) >>> output = optimal_filter(signal, template, noise_psd) >>> len(output) == len(signal) True >>> # Colored noise case (Wiener filtering optimal) >>> # Create signal with target embedded in colored noise >>> target = np.array([1, 2, 3, 2, 1]) >>> noise_freq = np.linspace(0, 1, 256) >>> colored_noise_psd = 1.0 + 2.0 * np.exp(-5 * noise_freq) # Red noise >>> colored_noise = np.random.randn(256) * np.sqrt(colored_noise_psd) >>> signal = np.concatenate([colored_noise, target, colored_noise]) >>> output = optimal_filter(signal, target, colored_noise_psd) >>> len(output) == len(signal) True
Notes
- The optimal filter in the frequency domain is:
H(f) = S*(f) / P_n(f)
where S(f) is the template spectrum and P_n(f) is the noise PSD.
- pytcl.mathematical_functions.signal_processing.pulse_compression(signal, reference, window=None)[source]
Perform pulse compression on a signal.
Pulse compression is used in radar to achieve the resolution of a short pulse while maintaining the energy of a long pulse. It is typically used with frequency-modulated (chirp) waveforms.
- Parameters:
signal (array_like) – Received signal (possibly containing the transmitted waveform).
reference (array_like) – Reference waveform (transmitted pulse, e.g., chirp).
window (str, optional) – Window function to apply to reduce sidelobes. Options: ‘hamming’, ‘hann’, ‘blackman’, ‘kaiser’. Default is None (no windowing).
- Returns:
result – Named tuple with compressed output, compression ratio, and sidelobes.
- Return type:
Examples
>>> import numpy as np >>> fs = 1000 >>> chirp = generate_lfm_chirp(0.1, 50, 200, fs) # 100 ms chirp >>> signal = np.zeros(2000) >>> signal[500:500+len(chirp)] = chirp >>> result = pulse_compression(signal, chirp) >>> result.compression_ratio > 1 True
Notes
The compression ratio is approximately equal to the time-bandwidth product of the chirp signal.
- pytcl.mathematical_functions.signal_processing.generate_lfm_chirp(duration, f0, f1, fs, amplitude=1.0, phase=0.0)[source]
Generate a linear frequency modulated (LFM) chirp signal.
- Parameters:
- Returns:
chirp – LFM chirp signal.
- Return type:
ndarray
Examples
>>> chirp = generate_lfm_chirp(0.001, 1000, 5000, 44100) >>> len(chirp) 44 >>> chirp[0] # Should start near amplitude (with phase=0) 1.0
Notes
The instantaneous frequency varies linearly from f0 to f1 over the duration. The time-bandwidth product is (f1 - f0) * duration, which determines the pulse compression ratio.
- pytcl.mathematical_functions.signal_processing.generate_nlfm_chirp(duration, f0, f1, fs, beta=1.0, amplitude=1.0)[source]
Generate a non-linear frequency modulated (NLFM) chirp signal.
NLFM chirps can provide lower sidelobes than LFM chirps without requiring windowing, at the cost of slightly reduced SNR.
- Parameters:
duration (float) – Duration of the chirp in seconds.
f0 (float) – Starting frequency in Hz.
f1 (float) – Ending frequency in Hz.
fs (float) – Sampling frequency in Hz.
beta (float, optional) – Non-linearity parameter. Higher values give more non-linearity. Default is 1.0.
amplitude (float, optional) – Signal amplitude. Default is 1.0.
- Returns:
chirp – NLFM chirp signal.
- Return type:
ndarray
Examples
>>> chirp = generate_nlfm_chirp(0.001, 1000, 5000, 44100, beta=2.0) >>> len(chirp) 44
- pytcl.mathematical_functions.signal_processing.ambiguity_function(signal, fs, max_delay=None, max_doppler=None, n_delay=256, n_doppler=256)[source]
Compute the ambiguity function of a signal.
The ambiguity function characterizes the resolution and ambiguity properties of a radar waveform in delay (range) and Doppler (velocity).
- Parameters:
signal (array_like) – Input signal (e.g., radar waveform).
fs (float) – Sampling frequency in Hz.
max_delay (float, optional) – Maximum delay in seconds. Default is signal duration.
max_doppler (float, optional) – Maximum Doppler shift in Hz. Default is fs/2.
n_delay (int, optional) – Number of delay bins. Default is 256.
n_doppler (int, optional) – Number of Doppler bins. Default is 256.
- Returns:
delays (ndarray) – Delay values in seconds.
dopplers (ndarray) – Doppler frequency values in Hz.
af (ndarray) – Ambiguity function (2D, complex).
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[complexfloating]]]
Examples
>>> chirp = generate_lfm_chirp(0.001, 1000, 5000, 44100) >>> delays, dopplers, af = ambiguity_function(chirp, 44100) >>> af.shape (256, 256)
- pytcl.mathematical_functions.signal_processing.cross_ambiguity(signal1, signal2, fs, max_delay=None, max_doppler=None, n_delay=256, n_doppler=256)[source]
Compute the cross-ambiguity function between two signals.
- Parameters:
signal1 (array_like) – First signal.
signal2 (array_like) – Second signal.
fs (float) – Sampling frequency in Hz.
max_delay (float, optional) – Maximum delay in seconds.
max_doppler (float, optional) – Maximum Doppler shift in Hz.
n_delay (int, optional) – Number of delay bins.
n_doppler (int, optional) – Number of Doppler bins.
- Returns:
delays (ndarray) – Delay values in seconds.
dopplers (ndarray) – Doppler frequency values in Hz.
caf (ndarray) – Cross-ambiguity function (2D, complex).
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[complexfloating]]]
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... cross_ambiguity, generate_lfm_chirp ... ) >>> # Generate two LFM chirps for correlation analysis >>> fs = 10000 # 10 kHz sampling >>> signal1 = generate_lfm_chirp(0.001, 1000, 2000, fs) # 1 ms chirp >>> signal2 = generate_lfm_chirp(0.001, 1000, 2000, fs) # Identical chirp >>> # Compute cross-ambiguity function >>> delays, dopplers, caf = cross_ambiguity( ... signal1, signal2, fs, n_delay=64, n_doppler=64 ... ) >>> # Auto-correlation should have peak near zero delay/Doppler >>> caf.shape (64, 64) >>> np.max(np.abs(caf)) > 0.9 # High correlation for identical signals True
- class pytcl.mathematical_functions.signal_processing.CFARResult(detections, threshold, detection_indices, noise_estimate)[source]
Bases:
NamedTupleResult of 1D CFAR detection.
- detections
Boolean array indicating detections.
- Type:
ndarray
- threshold
Adaptive threshold values.
- Type:
ndarray
- detection_indices
Indices of detection points.
- Type:
ndarray
- noise_estimate
Estimated noise level at each cell.
- Type:
ndarray
- class pytcl.mathematical_functions.signal_processing.CFARResult2D(detections, threshold, noise_estimate)[source]
Bases:
NamedTupleResult of 2D CFAR detection.
- detections
2D boolean array indicating detections.
- Type:
ndarray
- threshold
2D adaptive threshold values.
- Type:
ndarray
- noise_estimate
2D estimated noise level.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.cfar_ca(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Cell-Averaging CFAR detector.
CA-CFAR estimates the noise level by averaging the cells in the reference window (excluding guard cells around the cell under test).
- Parameters:
signal (array_like) – Input signal (typically power or magnitude).
guard_cells (int) – Number of guard cells on each side of the cell under test.
ref_cells (int) – Number of reference cells on each side.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
alpha (float, optional) – Threshold multiplier. If provided, overrides pfa calculation.
- Returns:
result – Named tuple with detections, threshold, indices, and noise estimate.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> # Noise with a few targets >>> signal = np.random.exponential(1.0, 1000) >>> signal[250] = 50 # Target 1 >>> signal[500] = 100 # Target 2 >>> signal[750] = 30 # Target 3 >>> result = cfar_ca(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> 250 in result.detection_indices True
Notes
The CA-CFAR is optimal for homogeneous noise (noise power constant across all cells). It suffers in heterogeneous environments and near closely-spaced targets.
- pytcl.mathematical_functions.signal_processing.cfar_go(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Greatest-Of CFAR detector.
GO-CFAR takes the maximum of the leading and lagging reference window averages. This provides better performance at clutter edges but increased loss against distributed targets.
- Parameters:
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> signal = np.random.exponential(1.0, 500) >>> signal[250] = 50 >>> result = cfar_go(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> len(result.detection_indices) >= 1 True
Notes
GO-CFAR reduces false alarms at clutter edges (where noise level changes abruptly) compared to CA-CFAR, at the cost of slightly reduced detection probability in homogeneous noise.
- pytcl.mathematical_functions.signal_processing.cfar_so(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Smallest-Of CFAR detector.
SO-CFAR takes the minimum of the leading and lagging reference window averages. This provides better detection near clutter edges but increased false alarms in some scenarios.
- Parameters:
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import cfar_so >>> # Create test signal with closely spaced targets in clutter >>> np.random.seed(42) >>> signal = np.random.exponential(1.0, 500) >>> signal[200:205] = [30, 40, 35, 45, 38] # Target cluster >>> signal[350] = 50 # Isolated target >>> # Detect using SO-CFAR >>> result = cfar_so(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> # SO-CFAR good for clutter edge detection >>> len(result.detection_indices) >= 2 # Should find multiple targets True
Notes
SO-CFAR is complementary to GO-CFAR. It is more sensitive near clutter edges but may produce more false alarms when interfering targets are present in the reference window.
- pytcl.mathematical_functions.signal_processing.cfar_os(signal, guard_cells, ref_cells, pfa=1e-06, k=None, alpha=None)[source]
Order-Statistic CFAR detector.
OS-CFAR uses an order statistic (k-th smallest value) of the reference cells instead of the mean. This makes it robust to interfering targets in the reference window.
- Parameters:
signal (array_like) – Input signal.
guard_cells (int) – Number of guard cells on each side.
ref_cells (int) – Number of reference cells on each side.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
k (int, optional) – Order statistic to use (1 = minimum, n_ref = maximum). Default is 0.75 * n_ref.
alpha (float, optional) – Threshold multiplier.
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> signal = np.random.exponential(1.0, 500) >>> signal[250] = 50 >>> signal[260] = 40 # Closely spaced target >>> result = cfar_os(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> len(result.detection_indices) >= 2 True
Notes
OS-CFAR is robust to interfering targets in the reference window because the order statistic ignores outliers. The choice of k trades off between: - Low k: Robust to multiple interferers, but sensitive to noise - High k: Less robust to interferers, but better in homogeneous noise
- pytcl.mathematical_functions.signal_processing.cfar_2d(image, guard_cells, ref_cells, pfa=1e-06, method='ca', alpha=None)[source]
Two-dimensional CFAR detector.
2D CFAR is used for range-Doppler maps or image detection where the reference window extends in both dimensions.
- Parameters:
image (array_like) – 2D input (e.g., range-Doppler map).
guard_cells (tuple) – (guard_rows, guard_cols) - guard cells in each direction.
ref_cells (tuple) – (ref_rows, ref_cols) - reference cells in each direction.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
method ({'ca', 'go', 'so'}, optional) – CFAR method. Default is ‘ca’.
alpha (float, optional) – Threshold multiplier.
- Returns:
result – Named tuple with 2D detections, threshold, and noise estimate.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> image = np.random.exponential(1.0, (100, 100)) >>> image[50, 50] = 100 # Target >>> result = cfar_2d(image, guard_cells=(2, 2), ref_cells=(8, 8), pfa=1e-4) >>> result.detections[50, 50] True
Notes
The 2D reference window forms a rectangular annulus around the cell under test. The total number of reference cells is:
(2*guard_rows + 2*ref_rows + 1) * (2*guard_cols + 2*ref_cols + 1) - (2*guard_rows + 1) * (2*guard_cols + 1)
- pytcl.mathematical_functions.signal_processing.threshold_factor(pfa, n_ref, method='ca', k=None)[source]
Compute the CFAR threshold multiplier for a given probability of false alarm.
- Parameters:
- Returns:
alpha – Threshold multiplier.
- Return type:
Examples
>>> alpha = threshold_factor(1e-6, 32, method='ca') >>> alpha > 1 True
Notes
For CA-CFAR with n_ref reference cells, the relationship between threshold factor alpha and Pfa is:
Pfa = (1 + alpha/n_ref)^(-n_ref)
- Solving for alpha:
alpha = n_ref * (Pfa^(-1/n_ref) - 1)
- pytcl.mathematical_functions.signal_processing.detection_probability(snr, pfa, n_ref, method='ca', swerling_case=0)[source]
Compute detection probability for a given SNR and Pfa.
- Parameters:
snr (float) – Signal-to-noise ratio (linear, not dB).
pfa (float) – Probability of false alarm.
n_ref (int) – Number of reference cells.
method ({'ca'}, optional) – CFAR method. Default is ‘ca’.
swerling_case ({0, 1, 2, 3, 4}, optional) – Swerling target model. 0 is non-fluctuating (Marcum). Default is 0.
- Returns:
pd – Probability of detection.
- Return type:
Examples
>>> pd = detection_probability(snr=10, pfa=1e-6, n_ref=32) >>> 0 < pd < 1 True
Notes
- For a non-fluctuating target (Swerling 0/Marcum case) with CA-CFAR:
Pd = (1 + alpha/(n_ref*(1+snr)))^(-n_ref)
where alpha is the threshold factor for the given Pfa.
- pytcl.mathematical_functions.signal_processing.cluster_detections(detections, min_separation=1)[source]
Cluster nearby detections and return peak indices.
- Parameters:
detections (array_like) – Boolean detection array or signal values at detection points.
min_separation (int, optional) – Minimum separation between distinct detections. Default is 1.
- Returns:
peak_indices – Indices of detection peaks after clustering.
- Return type:
ndarray
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import cluster_detections >>> # CFAR detection result with closely spaced detections >>> detections = np.zeros(100, dtype=bool) >>> detections[20:24] = True # Cluster 1 (4 adjacent detections) >>> detections[60] = True # Cluster 2 (single detection) >>> detections[62] = True # Close to cluster 2 >>> # Cluster with min_separation=1 (adjacent counts as same cluster) >>> peaks = cluster_detections(detections, min_separation=1) >>> len(peaks) # Should find 2 clusters 2 >>> peaks[0] # Center of first cluster (indices 20-23) 21
- pytcl.mathematical_functions.signal_processing.snr_loss(n_ref, method='ca')[source]
Compute the SNR loss due to CFAR processing.
CFAR detectors have an inherent SNR loss compared to an ideal detector with known noise level.
- Parameters:
n_ref (int) – Number of reference cells.
method ({'ca', 'go', 'so', 'os'}, optional) – CFAR method. Default is ‘ca’.
- Returns:
loss – SNR loss in dB.
- Return type:
Examples
>>> loss = snr_loss(32, method='ca') >>> 0 < loss < 1 # Small loss for many reference cells True
Filter Design
IIR and FIR digital filter design functions.
Digital filter design and application.
This module provides functions for designing and applying various types of digital filters including Butterworth, Chebyshev, elliptic, Bessel, and FIR.
Functions
butter_design: Butterworth filter design
cheby1_design: Chebyshev Type I filter design
cheby2_design: Chebyshev Type II filter design
ellip_design: Elliptic (Cauer) filter design
bessel_design: Bessel filter design
fir_design: FIR filter design using windowed sinc
apply_filter: Apply filter to signal
filtfilt: Zero-phase forward-backward filtering
frequency_response: Compute filter frequency response
References
- class pytcl.mathematical_functions.signal_processing.filters.FilterCoefficients(b, a, sos)[source]
Bases:
NamedTupleFilter coefficients from IIR filter design.
- b
Numerator (feedforward) coefficients.
- Type:
ndarray
- a
Denominator (feedback) coefficients.
- Type:
ndarray
- sos
Second-order sections representation (more numerically stable).
- Type:
ndarray or None
- class pytcl.mathematical_functions.signal_processing.filters.FrequencyResponse(frequencies, magnitude, phase)[source]
Bases:
NamedTupleFrequency response of a digital filter.
- frequencies
Frequency values in Hz.
- Type:
ndarray
- magnitude
Magnitude response (linear scale).
- Type:
ndarray
- phase
Phase response in radians.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.filters.butter_design(order, cutoff, fs, btype='low', output='sos')[source]
Design a Butterworth digital filter.
The Butterworth filter has maximally flat frequency response in the passband. It is often used as a “standard” filter when sharp transitions are not required.
- Parameters:
order (int) – Filter order.
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass/bandstop, provide (low, high).
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. ‘sos’ is recommended for stability. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients (b, a, sos).
- Return type:
Examples
>>> fs = 1000 # 1 kHz >>> coeffs = butter_design(4, 100, fs, btype='low') >>> coeffs.sos.shape[0] # Number of second-order sections 2
Notes
The Butterworth filter has no ripple in the passband or stopband. The -3 dB point occurs at the cutoff frequency.
- pytcl.mathematical_functions.signal_processing.filters.cheby1_design(order, ripple, cutoff, fs, btype='low', output='sos')[source]
Design a Chebyshev Type I digital filter.
The Chebyshev Type I filter has equiripple in the passband and monotonic in the stopband. It provides a sharper transition than Butterworth for the same order.
- Parameters:
order (int) – Filter order.
ripple (float) – Maximum passband ripple in dB.
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass/bandstop, provide (low, high).
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = cheby1_design(4, 0.5, 100, fs) # 0.5 dB ripple >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.filters.cheby2_design(order, attenuation, cutoff, fs, btype='low', output='sos')[source]
Design a Chebyshev Type II digital filter.
The Chebyshev Type II filter has equiripple in the stopband and monotonic in the passband. The cutoff frequency is the stopband edge.
- Parameters:
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = cheby2_design(4, 40, 100, fs) # 40 dB stopband attenuation >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.filters.ellip_design(order, passband_ripple, stopband_attenuation, cutoff, fs, btype='low', output='sos')[source]
Design an elliptic (Cauer) digital filter.
The elliptic filter has equiripple in both passband and stopband. It achieves the sharpest transition for a given order but at the cost of ripple in both bands.
- Parameters:
order (int) – Filter order.
passband_ripple (float) – Maximum passband ripple in dB.
stopband_attenuation (float) – Minimum stopband attenuation in dB.
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = ellip_design(4, 0.5, 40, 100, fs) >>> len(coeffs.b) > 0 True
- pytcl.mathematical_functions.signal_processing.filters.bessel_design(order, cutoff, fs, btype='low', norm='phase', output='sos')[source]
Design a Bessel/Thomson digital filter.
The Bessel filter is designed for a maximally flat group delay, making it ideal for preserving the waveshape of filtered signals.
- Parameters:
order (int) – Filter order.
fs (float) – Sampling frequency in Hz.
btype ({'low', 'high', 'band', 'bandstop'}, optional) – Filter type. Default is ‘low’.
norm ({'phase', 'delay', 'mag'}, optional) – Normalization type. Default is ‘phase’.
output ({'sos', 'ba'}, optional) – Output format. Default is ‘sos’.
- Returns:
coeffs – Filter coefficients.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = bessel_design(4, 100, fs) >>> len(coeffs.b) > 0 True
Notes
The Bessel filter has a slower roll-off than Butterworth, but preserves the shape of signals in the passband due to its flat group delay.
- pytcl.mathematical_functions.signal_processing.filters.fir_design(numtaps, cutoff, fs, window='hamming', pass_zero=True)[source]
Design an FIR filter using the window method.
- Parameters:
numtaps (int) – Length of the filter (number of coefficients). Must be odd for Type I filter (pass_zero=True, lowpass).
cutoff (float or tuple) – Cutoff frequency in Hz. For bandpass, provide (low, high).
fs (float) – Sampling frequency in Hz.
window (str, optional) – Window function to use. Default is ‘hamming’.
pass_zero (bool or {'bandpass', 'lowpass', 'highpass', 'bandstop'}, optional) – If True, gain at zero frequency is 1. Default is True.
- Returns:
h – FIR filter coefficients.
- Return type:
ndarray
Examples
>>> fs = 1000 >>> h = fir_design(101, 100, fs) # 100 Hz lowpass >>> len(h) 101
Notes
FIR filters are always stable and have linear phase when the coefficients are symmetric. However, they typically require higher order than IIR filters for the same transition sharpness.
- pytcl.mathematical_functions.signal_processing.filters.fir_design_remez(numtaps, bands, desired, fs, weight=None)[source]
Design an optimal FIR filter using the Remez exchange algorithm.
The Parks-McClellan (Remez) algorithm designs an optimal equiripple FIR filter that minimizes the maximum error between the desired and actual frequency response.
- Parameters:
numtaps (int) – Length of the filter.
bands (array_like) – Band edges in Hz. Must be monotonically increasing with an even number of elements.
desired (array_like) – Desired gain in each band (one value per band).
fs (float) – Sampling frequency in Hz.
weight (array_like, optional) – Weight for each band. Default is equal weight.
- Returns:
h – FIR filter coefficients.
- Return type:
ndarray
Examples
>>> fs = 1000 >>> # Lowpass: passband 0-100 Hz, stopband 150-500 Hz >>> h = fir_design_remez(101, [0, 100, 150, 500], [1, 0], fs) >>> len(h) 101
- pytcl.mathematical_functions.signal_processing.filters.apply_filter(coeffs, x, zi=None)[source]
Apply a digital filter to a signal.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients. Can be: - FilterCoefficients (uses sos if available) - Tuple (b, a) for IIR filter - 1D array for FIR filter
x (array_like) – Input signal.
zi (array_like, optional) – Initial filter state. If provided, returns (output, final_state).
- Returns:
y – Filtered signal. If zi is provided, returns (y, zf).
- Return type:
ndarray or tuple
Examples
>>> import numpy as np >>> fs = 1000 >>> t = np.arange(0, 1, 1/fs) >>> x = np.sin(2 * np.pi * 50 * t) + np.sin(2 * np.pi * 200 * t) >>> coeffs = butter_design(4, 100, fs) # 100 Hz lowpass >>> y = apply_filter(coeffs, x) >>> len(y) == len(x) True
- pytcl.mathematical_functions.signal_processing.filters.filtfilt(coeffs, x, padtype='odd', padlen=None)[source]
Apply zero-phase forward-backward filtering.
The signal is filtered twice, once forward and once backward, which eliminates phase distortion but doubles the filter order.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
x (array_like) – Input signal.
padtype ({'odd', 'even', 'constant', None}, optional) – Type of padding to use. Default is ‘odd’.
padlen (int, optional) – Length of padding. Default is 3 * max(len(a), len(b)).
- Returns:
y – Zero-phase filtered signal.
- Return type:
ndarray
Examples
>>> import numpy as np >>> fs = 1000 >>> t = np.arange(0, 1, 1/fs) >>> x = np.sin(2 * np.pi * 50 * t) >>> coeffs = butter_design(4, 100, fs) >>> y = filtfilt(coeffs, x) >>> len(y) == len(x) True
Notes
Zero-phase filtering has no phase distortion but cannot be used for real-time applications since it requires the entire signal upfront.
- pytcl.mathematical_functions.signal_processing.filters.frequency_response(coeffs, fs, n_points=512, whole=False)[source]
Compute the frequency response of a digital filter.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
fs (float) – Sampling frequency in Hz.
n_points (int, optional) – Number of frequency points. Default is 512.
whole (bool, optional) – If True, compute response from 0 to fs (instead of 0 to fs/2). Default is False.
- Returns:
response – Named tuple with frequencies, magnitude, and phase.
- Return type:
Examples
>>> fs = 1000 >>> coeffs = butter_design(4, 100, fs) >>> response = frequency_response(coeffs, fs) >>> len(response.frequencies) == 512 True >>> response.magnitude[0] # DC gain 1.0
- pytcl.mathematical_functions.signal_processing.filters.group_delay(coeffs, fs, n_points=512)[source]
Compute the group delay of a digital filter.
Group delay is the negative derivative of phase with respect to frequency, representing the time delay experienced by signal components at each frequency.
- Parameters:
coeffs (FilterCoefficients, tuple, or ndarray) – Filter coefficients.
fs (float) – Sampling frequency in Hz.
n_points (int, optional) – Number of frequency points. Default is 512.
- Returns:
frequencies (ndarray) – Frequency values in Hz.
gd (ndarray) – Group delay in samples.
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]]]
Examples
>>> fs = 1000 >>> h = fir_design(51, 100, fs) # Symmetric FIR >>> freqs, gd = group_delay(h, fs) >>> np.allclose(gd, 25) # Constant group delay = (N-1)/2 True
- pytcl.mathematical_functions.signal_processing.filters.filter_order(passband_freq, stopband_freq, passband_ripple, stopband_attenuation, fs, filter_type='butter')[source]
Estimate the minimum filter order for given specifications.
- Parameters:
passband_freq (float) – Passband edge frequency in Hz.
stopband_freq (float) – Stopband edge frequency in Hz.
passband_ripple (float) – Maximum passband ripple in dB.
stopband_attenuation (float) – Minimum stopband attenuation in dB.
fs (float) – Sampling frequency in Hz.
filter_type ({'butter', 'cheby1', 'cheby2', 'ellip'}, optional) – Filter type. Default is ‘butter’.
- Returns:
order – Minimum filter order.
- Return type:
Examples
>>> order = filter_order(100, 150, 0.5, 40, 1000, 'butter') >>> order > 0 True
- pytcl.mathematical_functions.signal_processing.filters.sos_to_zpk(sos)[source]
Convert second-order sections to zeros, poles, gain.
- Parameters:
sos (array_like) – Second-order sections array with shape (n_sections, 6).
- Returns:
z (ndarray) – Zeros.
p (ndarray) – Poles.
k (float) – Gain.
- Return type:
tuple[ndarray[tuple[Any, …], dtype[Any]], ndarray[tuple[Any, …], dtype[Any]], Any]
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... sos_to_zpk, butter_sos ... ) >>> # Design a Butterworth filter and convert to ZPK form >>> sos = butter_sos(4, 0.3) # 4th order Butterworth lowpass >>> z, p, k = sos_to_zpk(sos) >>> # Check filter stability: poles must be inside unit circle >>> np.all(np.abs(p) < 1.0) True >>> # Verify number of poles matches filter order >>> len(p) == 4 True
- pytcl.mathematical_functions.signal_processing.filters.zpk_to_sos(z, p, k, pairing='nearest')[source]
Convert zeros, poles, gain to second-order sections.
- Parameters:
z (array_like) – Zeros.
p (array_like) – Poles.
k (float) – Gain.
pairing ({'nearest', 'keep_odd', 'minimal'}, optional) – Pole-zero pairing strategy. Default is ‘nearest’.
- Returns:
sos – Second-order sections array.
- Return type:
ndarray
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... zpk_to_sos ... ) >>> # Create a simple 2nd order Butterworth-like filter >>> z = np.array([-1.0, -1.0]) # Zeros at -1 >>> p = np.array([0.7071 * np.exp(1j * np.pi / 4), ... 0.7071 * np.exp(-1j * np.pi / 4)]) # Complex poles >>> k = 1.0 >>> sos = zpk_to_sos(z, p, k) >>> sos.shape (1, 6) >>> # Verify roundtrip conversion >>> from pytcl.mathematical_functions.signal_processing import sos_to_zpk >>> z2, p2, k2 = sos_to_zpk(sos) >>> np.allclose(z, z2) and np.allclose(p, p2) True
Matched Filtering
Matched filter and pulse compression algorithms.
Matched filtering for signal detection.
Matched filtering is the optimal linear filter for maximizing the signal-to- noise ratio (SNR) of a known signal in the presence of additive white Gaussian noise. It is widely used in radar, sonar, and communications.
Functions
matched_filter: Time-domain matched filtering
matched_filter_frequency: Frequency-domain matched filtering
optimal_filter: Optimal filter for colored noise
pulse_compression: Pulse compression for chirp signals
generate_lfm_chirp: Generate linear frequency modulated chirp
References
Richards, M. A. (2014). Fundamentals of Radar Signal Processing (2nd ed.). McGraw-Hill.
Turin, G. L. (1960). An introduction to matched filters. IRE Transactions on Information Theory, 6(3), 311-329.
- class pytcl.mathematical_functions.signal_processing.matched_filter.MatchedFilterResult(output, peak_index, peak_value, snr_gain)[source]
Bases:
NamedTupleResult of matched filter operation.
- output
Matched filter output.
- Type:
ndarray
- class pytcl.mathematical_functions.signal_processing.matched_filter.PulseCompressionResult(output, peak_index, compression_ratio, peak_sidelobe_ratio)[source]
Bases:
NamedTupleResult of pulse compression.
- output
Compressed pulse output.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.matched_filter.matched_filter(signal, template, normalize=True, mode='same')[source]
Apply matched filtering in the time domain.
The matched filter maximizes the output signal-to-noise ratio (SNR) when the input contains a known signal plus white Gaussian noise.
- Parameters:
signal (array_like) – Input signal (may contain noise).
template (array_like) – Template signal to match (the known waveform).
normalize (bool, optional) – If True, normalize output by template energy. Default is True.
mode ({'full', 'same', 'valid'}, optional) – Convolution mode. Default is ‘same’.
- Returns:
result – Named tuple with filter output, peak location, and SNR gain.
- Return type:
Examples
>>> import numpy as np >>> # Create a signal with a pulse >>> template = np.array([1, 1, 1, 1, 1]) >>> signal = np.zeros(100) >>> signal[50:55] = template >>> result = matched_filter(signal, template) >>> 50 <= result.peak_index <= 54 True
Notes
The matched filter is the time-reversed, conjugated version of the template convolved with the signal. For real signals, this is equivalent to cross-correlation.
The theoretical SNR gain of a matched filter is equal to the number of samples in the template (for unit-energy template in unit-variance noise).
- pytcl.mathematical_functions.signal_processing.matched_filter.matched_filter_frequency(signal, template, fs=1.0, normalize=True)[source]
Apply matched filtering in the frequency domain.
This implementation uses FFT for efficient computation, especially for long signals and templates.
- Parameters:
- Returns:
result – Named tuple with filter output, peak location, and SNR gain.
- Return type:
Examples
>>> import numpy as np >>> template = np.sin(2 * np.pi * 0.1 * np.arange(50)) >>> signal = np.zeros(200) >>> signal[100:150] = template >>> result = matched_filter_frequency(signal, template) >>> 100 <= result.peak_index <= 150 True
Notes
For long signals, frequency-domain matched filtering is more efficient as it uses O(N log N) FFT operations instead of O(N^2) convolution.
- pytcl.mathematical_functions.signal_processing.matched_filter.optimal_filter(signal, template, noise_psd, fs=1.0)[source]
Apply optimal filtering for colored noise.
The optimal filter (also called the Wiener filter) maximizes SNR when the noise is not white (colored noise). It pre-whitens the noise before matched filtering.
- Parameters:
signal (array_like) – Input signal.
template (array_like) – Template signal to match.
noise_psd (array_like) – Noise power spectral density (must be same length as FFT).
fs (float, optional) – Sampling frequency. Default is 1.0.
- Returns:
output – Optimal filter output.
- Return type:
ndarray
Examples
>>> import numpy as np >>> # White noise case (simple matched filter) >>> signal = np.random.randn(256) >>> template = np.ones(16) >>> noise_psd = np.ones(256) # White noise (flat PSD) >>> output = optimal_filter(signal, template, noise_psd) >>> len(output) == len(signal) True >>> # Colored noise case (Wiener filtering optimal) >>> # Create signal with target embedded in colored noise >>> target = np.array([1, 2, 3, 2, 1]) >>> noise_freq = np.linspace(0, 1, 256) >>> colored_noise_psd = 1.0 + 2.0 * np.exp(-5 * noise_freq) # Red noise >>> colored_noise = np.random.randn(256) * np.sqrt(colored_noise_psd) >>> signal = np.concatenate([colored_noise, target, colored_noise]) >>> output = optimal_filter(signal, target, colored_noise_psd) >>> len(output) == len(signal) True
Notes
- The optimal filter in the frequency domain is:
H(f) = S*(f) / P_n(f)
where S(f) is the template spectrum and P_n(f) is the noise PSD.
- pytcl.mathematical_functions.signal_processing.matched_filter.pulse_compression(signal, reference, window=None)[source]
Perform pulse compression on a signal.
Pulse compression is used in radar to achieve the resolution of a short pulse while maintaining the energy of a long pulse. It is typically used with frequency-modulated (chirp) waveforms.
- Parameters:
signal (array_like) – Received signal (possibly containing the transmitted waveform).
reference (array_like) – Reference waveform (transmitted pulse, e.g., chirp).
window (str, optional) – Window function to apply to reduce sidelobes. Options: ‘hamming’, ‘hann’, ‘blackman’, ‘kaiser’. Default is None (no windowing).
- Returns:
result – Named tuple with compressed output, compression ratio, and sidelobes.
- Return type:
Examples
>>> import numpy as np >>> fs = 1000 >>> chirp = generate_lfm_chirp(0.1, 50, 200, fs) # 100 ms chirp >>> signal = np.zeros(2000) >>> signal[500:500+len(chirp)] = chirp >>> result = pulse_compression(signal, chirp) >>> result.compression_ratio > 1 True
Notes
The compression ratio is approximately equal to the time-bandwidth product of the chirp signal.
- pytcl.mathematical_functions.signal_processing.matched_filter.generate_lfm_chirp(duration, f0, f1, fs, amplitude=1.0, phase=0.0)[source]
Generate a linear frequency modulated (LFM) chirp signal.
- Parameters:
- Returns:
chirp – LFM chirp signal.
- Return type:
ndarray
Examples
>>> chirp = generate_lfm_chirp(0.001, 1000, 5000, 44100) >>> len(chirp) 44 >>> chirp[0] # Should start near amplitude (with phase=0) 1.0
Notes
The instantaneous frequency varies linearly from f0 to f1 over the duration. The time-bandwidth product is (f1 - f0) * duration, which determines the pulse compression ratio.
- pytcl.mathematical_functions.signal_processing.matched_filter.generate_nlfm_chirp(duration, f0, f1, fs, beta=1.0, amplitude=1.0)[source]
Generate a non-linear frequency modulated (NLFM) chirp signal.
NLFM chirps can provide lower sidelobes than LFM chirps without requiring windowing, at the cost of slightly reduced SNR.
- Parameters:
duration (float) – Duration of the chirp in seconds.
f0 (float) – Starting frequency in Hz.
f1 (float) – Ending frequency in Hz.
fs (float) – Sampling frequency in Hz.
beta (float, optional) – Non-linearity parameter. Higher values give more non-linearity. Default is 1.0.
amplitude (float, optional) – Signal amplitude. Default is 1.0.
- Returns:
chirp – NLFM chirp signal.
- Return type:
ndarray
Examples
>>> chirp = generate_nlfm_chirp(0.001, 1000, 5000, 44100, beta=2.0) >>> len(chirp) 44
- pytcl.mathematical_functions.signal_processing.matched_filter.ambiguity_function(signal, fs, max_delay=None, max_doppler=None, n_delay=256, n_doppler=256)[source]
Compute the ambiguity function of a signal.
The ambiguity function characterizes the resolution and ambiguity properties of a radar waveform in delay (range) and Doppler (velocity).
- Parameters:
signal (array_like) – Input signal (e.g., radar waveform).
fs (float) – Sampling frequency in Hz.
max_delay (float, optional) – Maximum delay in seconds. Default is signal duration.
max_doppler (float, optional) – Maximum Doppler shift in Hz. Default is fs/2.
n_delay (int, optional) – Number of delay bins. Default is 256.
n_doppler (int, optional) – Number of Doppler bins. Default is 256.
- Returns:
delays (ndarray) – Delay values in seconds.
dopplers (ndarray) – Doppler frequency values in Hz.
af (ndarray) – Ambiguity function (2D, complex).
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[complexfloating]]]
Examples
>>> chirp = generate_lfm_chirp(0.001, 1000, 5000, 44100) >>> delays, dopplers, af = ambiguity_function(chirp, 44100) >>> af.shape (256, 256)
- pytcl.mathematical_functions.signal_processing.matched_filter.cross_ambiguity(signal1, signal2, fs, max_delay=None, max_doppler=None, n_delay=256, n_doppler=256)[source]
Compute the cross-ambiguity function between two signals.
- Parameters:
signal1 (array_like) – First signal.
signal2 (array_like) – Second signal.
fs (float) – Sampling frequency in Hz.
max_delay (float, optional) – Maximum delay in seconds.
max_doppler (float, optional) – Maximum Doppler shift in Hz.
n_delay (int, optional) – Number of delay bins.
n_doppler (int, optional) – Number of Doppler bins.
- Returns:
delays (ndarray) – Delay values in seconds.
dopplers (ndarray) – Doppler frequency values in Hz.
caf (ndarray) – Cross-ambiguity function (2D, complex).
- Return type:
tuple[ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[floating]], ndarray[tuple[Any, …], dtype[complexfloating]]]
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import ( ... cross_ambiguity, generate_lfm_chirp ... ) >>> # Generate two LFM chirps for correlation analysis >>> fs = 10000 # 10 kHz sampling >>> signal1 = generate_lfm_chirp(0.001, 1000, 2000, fs) # 1 ms chirp >>> signal2 = generate_lfm_chirp(0.001, 1000, 2000, fs) # Identical chirp >>> # Compute cross-ambiguity function >>> delays, dopplers, caf = cross_ambiguity( ... signal1, signal2, fs, n_delay=64, n_doppler=64 ... ) >>> # Auto-correlation should have peak near zero delay/Doppler >>> caf.shape (64, 64) >>> np.max(np.abs(caf)) > 0.9 # High correlation for identical signals True
CFAR Detection
Constant False Alarm Rate (CFAR) detection algorithms.
Constant False Alarm Rate (CFAR) detection algorithms.
CFAR algorithms maintain a constant probability of false alarm by adaptively setting detection thresholds based on local noise estimates. These are essential for radar signal processing where the noise environment varies.
Functions
cfar_ca: Cell-Averaging CFAR
cfar_go: Greatest-Of CFAR
cfar_so: Smallest-Of CFAR
cfar_os: Order-Statistic CFAR
cfar_2d: Two-dimensional CFAR
threshold_factor: Compute CFAR threshold multiplier
detection_probability: Compute detection probability
References
Richards, M. A. (2014). Fundamentals of Radar Signal Processing (2nd ed.). McGraw-Hill.
Rohling, H. (1983). Radar CFAR thresholding in clutter and multiple target situations. IEEE Transactions on Aerospace and Electronic Systems, 19(4), 608-621.
- class pytcl.mathematical_functions.signal_processing.detection.CFARResult(detections, threshold, detection_indices, noise_estimate)[source]
Bases:
NamedTupleResult of 1D CFAR detection.
- detections
Boolean array indicating detections.
- Type:
ndarray
- threshold
Adaptive threshold values.
- Type:
ndarray
- detection_indices
Indices of detection points.
- Type:
ndarray
- noise_estimate
Estimated noise level at each cell.
- Type:
ndarray
- class pytcl.mathematical_functions.signal_processing.detection.CFARResult2D(detections, threshold, noise_estimate)[source]
Bases:
NamedTupleResult of 2D CFAR detection.
- detections
2D boolean array indicating detections.
- Type:
ndarray
- threshold
2D adaptive threshold values.
- Type:
ndarray
- noise_estimate
2D estimated noise level.
- Type:
ndarray
- pytcl.mathematical_functions.signal_processing.detection.threshold_factor(pfa, n_ref, method='ca', k=None)[source]
Compute the CFAR threshold multiplier for a given probability of false alarm.
- Parameters:
- Returns:
alpha – Threshold multiplier.
- Return type:
Examples
>>> alpha = threshold_factor(1e-6, 32, method='ca') >>> alpha > 1 True
Notes
For CA-CFAR with n_ref reference cells, the relationship between threshold factor alpha and Pfa is:
Pfa = (1 + alpha/n_ref)^(-n_ref)
- Solving for alpha:
alpha = n_ref * (Pfa^(-1/n_ref) - 1)
- pytcl.mathematical_functions.signal_processing.detection.detection_probability(snr, pfa, n_ref, method='ca', swerling_case=0)[source]
Compute detection probability for a given SNR and Pfa.
- Parameters:
snr (float) – Signal-to-noise ratio (linear, not dB).
pfa (float) – Probability of false alarm.
n_ref (int) – Number of reference cells.
method ({'ca'}, optional) – CFAR method. Default is ‘ca’.
swerling_case ({0, 1, 2, 3, 4}, optional) – Swerling target model. 0 is non-fluctuating (Marcum). Default is 0.
- Returns:
pd – Probability of detection.
- Return type:
Examples
>>> pd = detection_probability(snr=10, pfa=1e-6, n_ref=32) >>> 0 < pd < 1 True
Notes
- For a non-fluctuating target (Swerling 0/Marcum case) with CA-CFAR:
Pd = (1 + alpha/(n_ref*(1+snr)))^(-n_ref)
where alpha is the threshold factor for the given Pfa.
- pytcl.mathematical_functions.signal_processing.detection.cfar_ca(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Cell-Averaging CFAR detector.
CA-CFAR estimates the noise level by averaging the cells in the reference window (excluding guard cells around the cell under test).
- Parameters:
signal (array_like) – Input signal (typically power or magnitude).
guard_cells (int) – Number of guard cells on each side of the cell under test.
ref_cells (int) – Number of reference cells on each side.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
alpha (float, optional) – Threshold multiplier. If provided, overrides pfa calculation.
- Returns:
result – Named tuple with detections, threshold, indices, and noise estimate.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> # Noise with a few targets >>> signal = np.random.exponential(1.0, 1000) >>> signal[250] = 50 # Target 1 >>> signal[500] = 100 # Target 2 >>> signal[750] = 30 # Target 3 >>> result = cfar_ca(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> 250 in result.detection_indices True
Notes
The CA-CFAR is optimal for homogeneous noise (noise power constant across all cells). It suffers in heterogeneous environments and near closely-spaced targets.
- pytcl.mathematical_functions.signal_processing.detection.cfar_go(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Greatest-Of CFAR detector.
GO-CFAR takes the maximum of the leading and lagging reference window averages. This provides better performance at clutter edges but increased loss against distributed targets.
- Parameters:
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> signal = np.random.exponential(1.0, 500) >>> signal[250] = 50 >>> result = cfar_go(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> len(result.detection_indices) >= 1 True
Notes
GO-CFAR reduces false alarms at clutter edges (where noise level changes abruptly) compared to CA-CFAR, at the cost of slightly reduced detection probability in homogeneous noise.
- pytcl.mathematical_functions.signal_processing.detection.cfar_so(signal, guard_cells, ref_cells, pfa=1e-06, alpha=None)[source]
Smallest-Of CFAR detector.
SO-CFAR takes the minimum of the leading and lagging reference window averages. This provides better detection near clutter edges but increased false alarms in some scenarios.
- Parameters:
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import cfar_so >>> # Create test signal with closely spaced targets in clutter >>> np.random.seed(42) >>> signal = np.random.exponential(1.0, 500) >>> signal[200:205] = [30, 40, 35, 45, 38] # Target cluster >>> signal[350] = 50 # Isolated target >>> # Detect using SO-CFAR >>> result = cfar_so(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> # SO-CFAR good for clutter edge detection >>> len(result.detection_indices) >= 2 # Should find multiple targets True
Notes
SO-CFAR is complementary to GO-CFAR. It is more sensitive near clutter edges but may produce more false alarms when interfering targets are present in the reference window.
- pytcl.mathematical_functions.signal_processing.detection.cfar_os(signal, guard_cells, ref_cells, pfa=1e-06, k=None, alpha=None)[source]
Order-Statistic CFAR detector.
OS-CFAR uses an order statistic (k-th smallest value) of the reference cells instead of the mean. This makes it robust to interfering targets in the reference window.
- Parameters:
signal (array_like) – Input signal.
guard_cells (int) – Number of guard cells on each side.
ref_cells (int) – Number of reference cells on each side.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
k (int, optional) – Order statistic to use (1 = minimum, n_ref = maximum). Default is 0.75 * n_ref.
alpha (float, optional) – Threshold multiplier.
- Returns:
result – Named tuple with detection results.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> signal = np.random.exponential(1.0, 500) >>> signal[250] = 50 >>> signal[260] = 40 # Closely spaced target >>> result = cfar_os(signal, guard_cells=2, ref_cells=16, pfa=1e-4) >>> len(result.detection_indices) >= 2 True
Notes
OS-CFAR is robust to interfering targets in the reference window because the order statistic ignores outliers. The choice of k trades off between: - Low k: Robust to multiple interferers, but sensitive to noise - High k: Less robust to interferers, but better in homogeneous noise
- pytcl.mathematical_functions.signal_processing.detection.cfar_2d(image, guard_cells, ref_cells, pfa=1e-06, method='ca', alpha=None)[source]
Two-dimensional CFAR detector.
2D CFAR is used for range-Doppler maps or image detection where the reference window extends in both dimensions.
- Parameters:
image (array_like) – 2D input (e.g., range-Doppler map).
guard_cells (tuple) – (guard_rows, guard_cols) - guard cells in each direction.
ref_cells (tuple) – (ref_rows, ref_cols) - reference cells in each direction.
pfa (float, optional) – Probability of false alarm. Default is 1e-6.
method ({'ca', 'go', 'so'}, optional) – CFAR method. Default is ‘ca’.
alpha (float, optional) – Threshold multiplier.
- Returns:
result – Named tuple with 2D detections, threshold, and noise estimate.
- Return type:
Examples
>>> import numpy as np >>> np.random.seed(42) >>> image = np.random.exponential(1.0, (100, 100)) >>> image[50, 50] = 100 # Target >>> result = cfar_2d(image, guard_cells=(2, 2), ref_cells=(8, 8), pfa=1e-4) >>> result.detections[50, 50] True
Notes
The 2D reference window forms a rectangular annulus around the cell under test. The total number of reference cells is:
(2*guard_rows + 2*ref_rows + 1) * (2*guard_cols + 2*ref_cols + 1) - (2*guard_rows + 1) * (2*guard_cols + 1)
- pytcl.mathematical_functions.signal_processing.detection.cluster_detections(detections, min_separation=1)[source]
Cluster nearby detections and return peak indices.
- Parameters:
detections (array_like) – Boolean detection array or signal values at detection points.
min_separation (int, optional) – Minimum separation between distinct detections. Default is 1.
- Returns:
peak_indices – Indices of detection peaks after clustering.
- Return type:
ndarray
Examples
>>> import numpy as np >>> from pytcl.mathematical_functions.signal_processing import cluster_detections >>> # CFAR detection result with closely spaced detections >>> detections = np.zeros(100, dtype=bool) >>> detections[20:24] = True # Cluster 1 (4 adjacent detections) >>> detections[60] = True # Cluster 2 (single detection) >>> detections[62] = True # Close to cluster 2 >>> # Cluster with min_separation=1 (adjacent counts as same cluster) >>> peaks = cluster_detections(detections, min_separation=1) >>> len(peaks) # Should find 2 clusters 2 >>> peaks[0] # Center of first cluster (indices 20-23) 21
- pytcl.mathematical_functions.signal_processing.detection.snr_loss(n_ref, method='ca')[source]
Compute the SNR loss due to CFAR processing.
CFAR detectors have an inherent SNR loss compared to an ideal detector with known noise level.
- Parameters:
n_ref (int) – Number of reference cells.
method ({'ca', 'go', 'so', 'os'}, optional) – CFAR method. Default is ‘ca’.
- Returns:
loss – SNR loss in dB.
- Return type:
Examples
>>> loss = snr_loss(32, method='ca') >>> 0 < loss < 1 # Small loss for many reference cells True