Skip to content

Zero phase or causal

A filter run forward only (causal, as hardware does it live) delays and reshapes what it passes. Run forward and then backward (zero phase, mode="forward_backward", the default) the delays cancel: a spike stays where it was, with its shape. Causal filters are for streams that cannot wait for the future; zero phase for everything recorded. dspk’s zero-phase result equals scipy’s sosfiltfilt.

import numpy as np
from scipy import signal as sp
import dspk
from plating import signals
s = signals.synthetic(noise_uv=2.0) # quieter, so the spike's shape is easy to follow
BAND = (300.0, 6_000.0)
zero = dspk.ops.filter.iir(s.data, s.sample_rate, "bandpass", BAND)
causal = dspk.ops.filter.iir(s.data, s.sample_rate, "bandpass", BAND, mode="forward")
# The same zero-phase filter in scipy (same padding)
sos = sp.butter(5, BAND, "bandpass", fs=s.sample_rate, output="sos")
ref = sp.sosfiltfilt(sos, s.data.astype(np.float64), axis=1, padtype="odd", padlen=dspk.ops.filter.settling_samples(sos))
print(f"zero phase vs scipy sosfiltfilt: max difference {np.max(np.abs(zero - ref)) / np.max(np.abs(ref)):.1e} of the peak")

Zero phase or causal

The whole script is playground/02_filtering/02_zero_phase_vs_causal.py. Run it with uv run python playground/02_filtering/02_zero_phase_vs_causal.py.