mne-python: Potential problem with causal filters

Description of the problem

While filtering a rather long recording, I came across (to me) unexpected behavior in causal filters: they do not remove the DC offset for me. I compared the behavior to the zero-phase filter and there the results are as expected. (Baseline correction is not an option for my use case.)

I discussed this briefly with @larsoner already, who suggested to test on a synthetic data example:

Steps to reproduce

import mne
import numpy as np

data = np.zeros((100, 10000))
data[:] = np.linspace(-1, 1, 100)[:, np.newaxis]

info = mne.create_info(100, 1000, ch_types='mag')

# causal filter
evoked = mne.EvokedArray(data, info, tmin=0.0)
evoked.filter(0.5, None, phase='minimum')
evoked.plot();

# non-causal filter
evoked = mne.EvokedArray(data, info, tmin=0.0)
evoked.filter(0.5, None)
evoked.plot();

Expected results

The zero-phase non-causal filter behaves as expected: image

Actual results

The causal minimum phase filter does not behave as epxected: image

This is in line with what I observe on actual MEG data.

Additional information

I am in a fresh conda env and on main with scipy version 1.11.4. @larsoner predicts this to be a problem with scipy.signal.minimum_phase and that is indeed the difference in our filter.py code between causal and non-causal filters. I can look into this more - but could use some guidance on how to decide how/where a fix would be necessary (if this is indeed in need of a fix).

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Comments: 15 (15 by maintainers)

Most upvoted comments

Fixed by https://github.com/scipy/scipy/pull/19706, will backport to mne.fixes once it lands

Can you post the filter design specs for both causal and non-causal filters (including amplitude and phase responses and/or impulse responses)? Maybe there’s something wrong with our design (e.g. filter order too small to achieve the desired properties etc.)? It might also be that our input (e.g. the linear-phase filter) does not fulfill the requirements (e.g. the docs mention an odd number of taps)?

If the only difference is scipy.signal.minimum_phase, it might also be worth trying to design the filter with only SciPy (it could be a bug in this function). It might also be worth trying to replicate the example in the SciPy docs, and see how it behaves when you add more and more MNE stuff (starting with your artifical toy data maybe).