Skip to content

Add a causal half-Gaussian option to the Gaussian smoothing filter - #229

Merged
cboulay merged 1 commit into
devfrom
feat/causal-half-gaussian
Aug 25, 2026
Merged

Add a causal half-Gaussian option to the Gaussian smoothing filter#229
cboulay merged 1 commit into
devfrom
feat/causal-half-gaussian

Conversation

@cboulay

@cboulay cboulay commented Aug 25, 2026

Copy link
Copy Markdown
Member

Why

FilterByDesignTransformer applies the Gaussian kernel causally via lfilter, so the acausal half of the symmetric kernel becomes pure group delay — (n_taps - 1) / 2, i.e. width * sigma samples. For a real-time BCI feature stream at 100 Hz, a 50 ms sigma costs 200 ms of lag, a large fraction of a closed-loop control budget.

What

Adds causal: bool = False to GaussianSmoothingSettings (and to gaussian_smoothing_filter_design). When True, the design is gaussian(2*K - 1, std=sigma)[K-1:] with K = int(width*sigma + 1), normalized to unit sum: the peak sits at lag 0 and the tail extends only into the past. Group delay becomes the half-Gaussian centroid, sigma * sqrt(2/pi)0.8 * sigma.

The default of False preserves current behavior exactly.

The two modes are not interchangeable at equal sigma — halving the kernel also halves the effective averaging window. They should be compared at matched white-noise variance reduction, sum(b**2), not at matched sigma. Measured at 100 Hz:

noise gain symmetric causal
0.141 σ = 20 ms → 80.0 ms σ = 38 ms → 27.3 ms
0.056 σ = 50 ms → 200.0 ms σ = 98 ms → 75.1 ms

sigma stays in seconds and is scaled by fs in design_wrapper, unchanged. The kernel_size override and the too-small-kernel warning both apply to the causal branch, where kernel_size counts causal taps and is checked against the one-sided recommended length width * sigma + 1.

The settings docstring documents the group delay of each mode and notes that the causal kernel's stopband rolls off less steeply for a given sigma.

Tests

11 new tests (63 pass in the file, 71 with test_filter.py):

  • unit sum for both modes
  • causal kernel has argmax == 0, is strictly decreasing, and equals the renormalized half of the symmetric kernel of the same sigma
  • measured DC group delay via scipy.signal.group_delay matches sigma * sqrt(2/pi) within a tap
  • symmetric group delay is exactly (n_taps - 1) / 2
  • matched-noise-gain lag comparison: the causal kernel hits the same sum(b**2) at less than half the delay
  • kernel_size override and undersized/identity warnings in causal mode
  • end-to-end through the transformer, confirming fs scaling still holds

Note for reviewers

Discrete truncation biases the causal group delay low relative to sigma*sqrt(2/pi) by a near-constant ~1/π tap (7.51 vs 7.82 at σ=9.8), because the n=0 tap gets full weight where the continuous centroid integral gives it half. It's in the favorable direction, but it's why the group-delay test uses abs=1.0 rather than something tighter.

FilterByDesignTransformer applies the kernel causally via lfilter, so the
acausal half of the symmetric Gaussian is pure group delay: (n_taps - 1) / 2,
or width * sigma samples. At 100 Hz a 50 ms sigma costs 200 ms of lag, which
is a large fraction of a closed-loop control budget.

`causal=True` keeps only the causal half of the same kernel -- peak at lag 0,
tail extending only into the past -- renormalized to unit sum. Its group delay
is the half-Gaussian centroid, sigma * sqrt(2 / pi) ~= 0.8 * sigma.

The two modes are not interchangeable at equal sigma; compare them at matched
white-noise variance reduction, sum(b ** 2). On that footing (100 Hz):

    noise gain    symmetric          causal
    0.141         sigma=20ms,  80ms  sigma=38ms, 27ms
    0.056         sigma=50ms, 200ms  sigma=98ms, 75ms

The default is False, preserving the existing symmetric kernel. kernel_size
still overrides the automatic length and still warns when undersized; in
causal mode it counts causal taps and is checked against the one-sided
recommended length, width * sigma + 1.
@cboulay
cboulay merged commit f8880b4 into dev Aug 25, 2026
14 checks passed
@cboulay
cboulay deleted the feat/causal-half-gaussian branch August 25, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant