Skip to content

FilterBankLayer: support causal filter-bank filtering for source-faithful model configs #1081

Description

@bruAristimunha

Problem

FilterBankLayer can now run IIR filtering stably in float64 (thanks for the fix in #1067), but its IIR path still applies only zero-phase forward/backward filtering via filtfilt.

That leaves no way to reproduce source implementations that use causal/forward-only filter-bank preprocessing (scipy.signal.lfilter). In downstream reproducibility work, this forces us to keep Braindecode's default FIR zero-phase bank as the best available approximation for some models, even when the released source uses causal IIR/FIR filtering.

Current Braindecode behavior

In the current dev install I checked (braindecode 1.6.1dev0), FilterBankLayer._apply_iir is:

filtered = filtfilt(
    x.double(),
    a_coeffs=a_coeffs.double().to(x.device),
    b_coeffs=b_coeffs.double().to(x.device),
    clamp=False,
)

So even if a user passes filter_parameters={"method": "iir", ...}, the filtering is always zero-phase. There is a phase argument on FilterBankLayer, but it does not provide a causal IIR lfilter mode.

Why this matters for model fidelity

Several filter-bank model sources use forward-only causal filtering before the network sees the data.

FBMSNet

The released FBMSNet source uses 9 non-overlapping 4-Hz bands from 4-40 Hz, Chebyshev type-II IIR coefficients, and applies them with scipy.signal.lfilter:

self.f_trans = 2
self.f_pass = np.arange(4,40,4)
self.f_width = 4
self.gpass = 3
self.gstop = 30
order, wn = cheb2ord(wp, ws, self.gpass, self.gstop)
b, a = signal.cheby2(order, self.gstop, ws, btype='bandpass')
eeg_data_filtered = np.asarray([signal.lfilter(b,a,eeg_data[j,:,:]) for j in range(n_trials)])

Braindecode's FBMSNet already exposes filter_parameters, but there is no causal option to express the source filter bank. In our reproduction, trying a stable zero-phase Cheby2 IIR bank regressed substantially relative to the default FIR bank, which is consistent with the phase/filter implementation not matching the source. We therefore keep the default FIR zero-phase bank as an approximation, but it is not source-faithful.

Current Braindecode-side model details relevant to this issue:

model: FBMSNet
filter bank: 9 bands, 4-40 Hz
n_filters_spat: 36
temporal_layer: LogVarLayer
stride_factor: 4
dilatability: 8
kernels_weights: [15, 31, 63, 125]
cnn_max_norm: 2
linear_max_norm: 0.5

FBCNet

The FBCNet/centralRepo source filter-bank helper also defaults to causal filtering:

def __init__(self, filtBank, fs, filtAllowance=2, axis=1, filtType='filter'):
    ...

# Cheby2 design via cheb2ord/cheby2
if filtType == 'filtfilt':
    dataOut = signal.filtfilt(b, a, data, axis=axis)
else:
    dataOut = signal.lfilter(b, a, data, axis=axis)

Braindecode's FBCNet also uses FilterBankLayer(..., **filter_parameters), so it has the same expressivity gap.

FBLightConvNet

The released LightConvNet preprocessing builds the same 9-band 4-40 Hz filter bank and calls a helper whose default is also filtType='filter', falling back to signal.lfilter unless filtfilt is explicitly requested.

IFNet

The released IFNet preprocessing uses a two-band filter bank and causal Butterworth filtering:

for bank in config.DATA.FILTER_BANK:  # [(4, 16), (16, 40)]
    parameter = signal.butter(N=5, Wn=bank, btype='bandpass', fs=config.DATA.FS)
    EEG_filtered = signal.lfilter(parameter[0], parameter[1], EEG)

Braindecode's IFNet also delegates filtering to FilterBankLayer, so source-faithful IFNet configs need the same causal filtering support.

Requested enhancement

Add a causal filtering mode to FilterBankLayer that can reproduce source implementations using scipy.signal.lfilter semantics, for example one of:

  • support phase="forward" or phase="causal" for method="iir" and apply a one-pass IIR recursion;
  • add an explicit filter_mode={"zero_phase", "causal"} / causal=True option;
  • expose a separate causal filter-bank module if that is cleaner for Torch/torchaudio constraints.

For source fidelity, it would also help if this worked for both IIR and FIR coefficient paths, because the upstream source helpers often expose both filter and filtfilt modes.

Suggested test shape

A minimal regression test could compare FilterBankLayer(..., method="iir", phase="causal") against scipy.signal.lfilter(b, a, x, axis=-1) on a fixed small tensor and fixed coefficients, while preserving the existing zero-phase behavior by default.

Related

This is separate from #1067. #1067 fixed numerical stability for the current zero-phase IIR path. This issue is about expressivity: source-faithful causal/forward-only filter banks cannot currently be represented.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions