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: NamedTuple

Filter 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

b: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

a: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

sos: ndarray[tuple[Any, ...], dtype[floating]] | None

Alias for field number 2

class pytcl.mathematical_functions.signal_processing.FrequencyResponse(frequencies, magnitude, phase)[source]

Bases: NamedTuple

Frequency 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

frequencies: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

magnitude: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

phase: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

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:

FilterCoefficients

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:

FilterCoefficients

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:
  • order (int) – Filter order.

  • attenuation (float) – Minimum stopband attenuation in dB.

  • cutoff (float or tuple) – Stopband edge frequency in Hz.

  • 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:

FilterCoefficients

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.

  • cutoff (float or tuple) – Cutoff frequency in Hz.

  • 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:

FilterCoefficients

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.

  • cutoff (float or tuple) – Cutoff frequency in Hz.

  • 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:

FilterCoefficients

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:

FrequencyResponse

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:

int

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: NamedTuple

Result of matched filter operation.

output

Matched filter output.

Type:

ndarray

peak_index

Index of the peak (detection point).

Type:

int

peak_value

Value at the peak.

Type:

float

snr_gain

Processing gain in dB.

Type:

float

output: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

peak_index: int

Alias for field number 1

peak_value: float

Alias for field number 2

snr_gain: float

Alias for field number 3

class pytcl.mathematical_functions.signal_processing.PulseCompressionResult(output, peak_index, compression_ratio, peak_sidelobe_ratio)[source]

Bases: NamedTuple

Result of pulse compression.

output

Compressed pulse output.

Type:

ndarray

peak_index

Index of the compressed pulse peak.

Type:

int

compression_ratio

Ratio of input pulse length to compressed pulse width.

Type:

float

peak_sidelobe_ratio

Peak-to-sidelobe ratio in dB.

Type:

float

output: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

peak_index: int

Alias for field number 1

compression_ratio: float

Alias for field number 2

peak_sidelobe_ratio: float

Alias for field number 3

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:

MatchedFilterResult

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:
  • signal (array_like) – Input signal.

  • template (array_like) – Template signal to match.

  • fs (float, optional) – Sampling frequency (used for output scaling). Default is 1.0.

  • normalize (bool, optional) – If True, normalize by template energy. Default is True.

Returns:

result – Named tuple with filter output, peak location, and SNR gain.

Return type:

MatchedFilterResult

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:

PulseCompressionResult

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:
  • 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.

  • amplitude (float, optional) – Signal amplitude. Default is 1.0.

  • phase (float, optional) – Initial phase in radians. Default is 0.0.

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: NamedTuple

Result 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

detections: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 0

threshold: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

detection_indices: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 2

noise_estimate: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 3

class pytcl.mathematical_functions.signal_processing.CFARResult2D(detections, threshold, noise_estimate)[source]

Bases: NamedTuple

Result 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

detections: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 0

threshold: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

noise_estimate: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

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:

CFARResult

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:
  • 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.

  • alpha (float, optional) – Threshold multiplier.

Returns:

result – Named tuple with detection results.

Return type:

CFARResult

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:
  • 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.

  • alpha (float, optional) – Threshold multiplier.

Returns:

result – Named tuple with detection results.

Return type:

CFARResult

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:

CFARResult

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:

CFARResult2D

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:
  • pfa (float) – Desired probability of false alarm (0 < pfa < 1).

  • n_ref (int) – Number of reference cells.

  • method ({'ca', 'go', 'so', 'os'}, optional) – CFAR method. Default is ‘ca’.

  • k (int, optional) – Order statistic index for OS-CFAR (1 <= k <= n_ref).

Returns:

alpha – Threshold multiplier.

Return type:

float

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:

float

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:

float

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: NamedTuple

Filter 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

b: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

a: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

sos: ndarray[tuple[Any, ...], dtype[floating]] | None

Alias for field number 2

class pytcl.mathematical_functions.signal_processing.filters.FrequencyResponse(frequencies, magnitude, phase)[source]

Bases: NamedTuple

Frequency 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

frequencies: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

magnitude: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

phase: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

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:

FilterCoefficients

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:

FilterCoefficients

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:
  • order (int) – Filter order.

  • attenuation (float) – Minimum stopband attenuation in dB.

  • cutoff (float or tuple) – Stopband edge frequency in Hz.

  • 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:

FilterCoefficients

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.

  • cutoff (float or tuple) – Cutoff frequency in Hz.

  • 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:

FilterCoefficients

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.

  • cutoff (float or tuple) – Cutoff frequency in Hz.

  • 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:

FilterCoefficients

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:

FrequencyResponse

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:

int

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

class pytcl.mathematical_functions.signal_processing.matched_filter.MatchedFilterResult(output, peak_index, peak_value, snr_gain)[source]

Bases: NamedTuple

Result of matched filter operation.

output

Matched filter output.

Type:

ndarray

peak_index

Index of the peak (detection point).

Type:

int

peak_value

Value at the peak.

Type:

float

snr_gain

Processing gain in dB.

Type:

float

output: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

peak_index: int

Alias for field number 1

peak_value: float

Alias for field number 2

snr_gain: float

Alias for field number 3

class pytcl.mathematical_functions.signal_processing.matched_filter.PulseCompressionResult(output, peak_index, compression_ratio, peak_sidelobe_ratio)[source]

Bases: NamedTuple

Result of pulse compression.

output

Compressed pulse output.

Type:

ndarray

peak_index

Index of the compressed pulse peak.

Type:

int

compression_ratio

Ratio of input pulse length to compressed pulse width.

Type:

float

peak_sidelobe_ratio

Peak-to-sidelobe ratio in dB.

Type:

float

output: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 0

peak_index: int

Alias for field number 1

compression_ratio: float

Alias for field number 2

peak_sidelobe_ratio: float

Alias for field number 3

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:

MatchedFilterResult

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:
  • signal (array_like) – Input signal.

  • template (array_like) – Template signal to match.

  • fs (float, optional) – Sampling frequency (used for output scaling). Default is 1.0.

  • normalize (bool, optional) – If True, normalize by template energy. Default is True.

Returns:

result – Named tuple with filter output, peak location, and SNR gain.

Return type:

MatchedFilterResult

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:

PulseCompressionResult

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:
  • 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.

  • amplitude (float, optional) – Signal amplitude. Default is 1.0.

  • phase (float, optional) – Initial phase in radians. Default is 0.0.

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

class pytcl.mathematical_functions.signal_processing.detection.CFARResult(detections, threshold, detection_indices, noise_estimate)[source]

Bases: NamedTuple

Result 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

detections: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 0

threshold: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

detection_indices: ndarray[tuple[Any, ...], dtype[int64]]

Alias for field number 2

noise_estimate: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 3

class pytcl.mathematical_functions.signal_processing.detection.CFARResult2D(detections, threshold, noise_estimate)[source]

Bases: NamedTuple

Result 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

detections: ndarray[tuple[Any, ...], dtype[bool]]

Alias for field number 0

threshold: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

noise_estimate: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

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:
  • pfa (float) – Desired probability of false alarm (0 < pfa < 1).

  • n_ref (int) – Number of reference cells.

  • method ({'ca', 'go', 'so', 'os'}, optional) – CFAR method. Default is ‘ca’.

  • k (int, optional) – Order statistic index for OS-CFAR (1 <= k <= n_ref).

Returns:

alpha – Threshold multiplier.

Return type:

float

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:

float

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:

CFARResult

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:
  • 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.

  • alpha (float, optional) – Threshold multiplier.

Returns:

result – Named tuple with detection results.

Return type:

CFARResult

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:
  • 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.

  • alpha (float, optional) – Threshold multiplier.

Returns:

result – Named tuple with detection results.

Return type:

CFARResult

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:

CFARResult

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:

CFARResult2D

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:

float

Examples

>>> loss = snr_loss(32, method='ca')
>>> 0 < loss < 1  # Small loss for many reference cells
True