Skip to content

dsp-props

Property-based testing for digital signal processing code.

DSP code is usually tested by comparing its output with a reference (MATLAB, SciPy) on a few signals, with a hand-picked tolerance. dsp-props instead checks relations that must always hold for any correct implementation: linearity, the shift and convolution theorems, Parseval's identity, Hermitian symmetry, invertibility, causality, time invariance, consistency with a declared transfer function, periodic time invariance of a resampler, and invariance of a streaming processor to the way its input is cut into blocks.

The relations are checked on automatically generated signals from Hypothesis, chosen to trip up DSP code: impulses, constants, off-grid sinusoids, close tones, near-Nyquist tones, extreme amplitudes (1e-100 to 1e100), lengths 1, 2, prime or 2^k ± 1 (about 15 % of the lengths drawn), and empty and one-sample blocks. Tolerances scale with the operation instead of a fixed atol: c · ε · log N for an FFT, with an engineering constant chosen to avoid false alarms, a derived bound for direct-form I recursive filters, and dp.calibrate when you need a tight tolerance. Every failure is reported with a small counterexample.

Status

Alpha. Version 0.1.2 is on PyPI. DOI: 10.5281/zenodo.23437840.

Install

python -m pip install "dsp-props[scipy]"

The scipy extra is needed for IIR filters and transfer-function checks (dp.lti_filter(transfer=...), IIRTolerance). Without it, dsp-props depends only on NumPy and Hypothesis. Python 3.10 or later.

For development:

git clone https://github.com/clement-basdevant/dsp-props
cd dsp-props
python -m pip install -e ".[dev]"
python -m pytest

30-second example

Declare what an operator is with one decorator, then call dp.verify:

import numpy as np
import dsp_props as dp

@dp.transform(inverse=np.fft.ifft, parseval=True, real_input_hermitian=True)
def my_fft(x):
    return np.fft.fft(x)

print(dp.verify(my_fft))
dsp-props report for my_fft
  ok   linearity
  ok   circular shift
  ok   parseval
  ok   hermitian symmetry
  ok   inversibility
  ok   convolution theorem

On failure, verify raises PropertyViolation, which carries for each violated relation the counterexample shrunk by Hypothesis. A transform that is off by one bin fails on a two-sample input:

@dp.transform(inverse=np.fft.ifft)
def buggy_fft(x):
    return np.roll(np.fft.fft(x), 1)

print(dp.verify(buggy_fft, raise_on_failure=False))
dsp-props report for buggy_fft
  ok   linearity
  FAIL circular shift
       circular shift (s=1): L2 error 4.000e+00 exceeds tolerance 8.882e-15 (N=2, ref norm 2.000e+00)
       minimal input: ([1, 1], 1)
  FAIL inversibility
       inversibility: L2 error 2.000e+00 exceeds tolerance 6.280e-15 (N=2, ref norm 1.414e+00)
       minimal input: [1, 1]
  FAIL convolution theorem
       convolution theorem: L2 error 1.000e+00 exceeds tolerance 3.140e-15 (N=2, ref norm 7.071e-01)
       minimal input: ([1, 1], [1, 1])

Inputs are random, so the exact counterexample can differ from one run to the next; pass seed=... or derandomize=True to verify for reproducible runs.

What can be declared

Declaration For Relations checked
dp.transform(kind="dft") complex DFT linearity, circular shift; Parseval, Hermitian symmetry, inversion and convolution theorem when declared
dp.transform(kind="rdft") real-input DFT (N // 2 + 1 bins) output shape, linearity, circular shift, real DC and Nyquist bins; Parseval, inversion and convolution theorem when declared
dp.transform(kind="generic") any linear transform (DCT, wavelets...) linearity; Parseval and inversion when declared
dp.lti_filter() LTI filter from zero state linearity, time invariance, causality; transfer function and gain bound when declared
dp.resampler(L, M) rational resampler linearity, output length, periodic time invariance, DC gain, alias rejection
dp.streaming(), dp.stateful() block processors block invariance (empty and one-sample blocks included)

See the relations catalogue for the exact statement of each relation, and the tutorial for an end-to-end example on a filter. C code can be tested through ctypes with dsp_props.adapters.

Where to go next

License

BSD-3-Clause, like NumPy and SciPy.