Closed britta-wstnr closed 8 months ago
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).
Thanks @cbrnr - I finally had time to get back to this and this is what I found:
(still using MNE for plotting, also please note that I did not use double the filter length for the minimum phase filter as MNE does -- to be very pedantic, I actually used double for the zero phase, as these were the numbers I had on hand.)
(This is using MNE-Python for filter design again, just like 1.)
this does weirdly enough not fully fix the output for me - I don't quite understand why yet.
I commented out the following line of scipy.signal.minimum_phase
: https://github.com/scipy/scipy/blob/main/scipy/signal/_fir_filter_design.py#L1281
From the docstring of the function I gather that @larsoner followed the recipe in this link for implementation: http://dspguru.com/dsp/howtos/how-to-design-minimum-phase-fir-filters
However, the addition of this scaling seems to be questionable, as discussed here: https://www.dsprelated.com/showthread/comp.dsp/20912-1.php
I further compared with the filter implemented in FieldTrip - which does not have any scaling added in this step either.
Thanks @britta-wstnr for investigating. I agree that the implementation might be incorrect, the best way to make sure is to directly consult e.g. Oppenheim/Schafer.
Regarding the plots, I'm not really sure if the amplitude plot explains the totally weird filter result. It does attenuate below 4 Hz, but much less than the zero-phase filter. This, however, could be due to filtering twice in the latter.
In addition, the impulse response is not visible - what is happening here with the minimum phase filter? Also, why is the impulse shortly after second 0.8 for the zero-phase filter? Should the impulse not be exactly at zero (hence zero-phase)?
PS: It looks like Fieldtrip uses the "hilbert" method of converting to minimum phase (and you are referring to "homomorphic"), did you try this with SciPy?
https://github.com/fieldtrip/fieldtrip/blob/master/preproc/private/minphaserceps.m
Hi @cbrnr, thanks for checking this out. I did try the Hilbert implementation early on with not much success, but have not compared since. I think the FieldTrip implementation is the homomorphic filter approach, too?
I think the FieldTrip implementation is the homomorphic filter approach, too?
Yes, you are right, but it is missing the log (which as you and others have noted might be the correct thing to do)!
Still, it would be interesting to know if the Hilbert method works.
I don't think the FieldTrip implementation is missing a log - it is only missing the 0.5 (see line 57, https://github.com/fieldtrip/fieldtrip/blob/master/preproc/private/minphaserceps.m#L57). In fact, if I delete the 0.5 from the scipy version, the output is the same as from the FieldTrip function.
I re-ran the Hilbert one (same parameters as above, just method='hilbert'
instead of the default homomorphic
:
EDIT: I wanted to add, I unfortunately do not have access to the referenced book by Oppenheim. I looked around a bit for publications (papers) on this, but couldn't find much that had the basic math. If anyone could point me to accessible literature that would be great!
I don't think the FieldTrip implementation is missing a log - it is only missing the 0.5
Of course, apparently my brain can't handle MATLAB code anymore 😄.
I have the book somewhere, I can take a look (next week).
Can you post the filtered signal for the three variants (i.e. original SciPy homomorphic, modified/corrected SciPy homomorphic, SciPy hilbert)?
apparently my brain can't handle MATLAB code anymore 😄.
Hehehe, wouldn't blame you for that @cbrnr
Here you go:
Maybe it'd be better to actually work with an example that has slow frequencies instead of DC offset - not so much to see here ...
I can't interpret these outputs. Why do the minimum phase filters just show horizontal lines? Initially, I thought that this was the problem, but they all look the same!
Yes, I think because there is a problem with all of them: they don't remove the DC offset. (The example was made to show exactly this behavior - which I think is unexpected/wrong.)
I had hoped the 0.5 constant would fix this - but while it makes the filter characteristics look better / closer to the zero phase filter, it does apparently not fix the offset issue.
I can't explain why there is such a large difference, but apparently the zero-phase filter seems to have much better stop band attenuation, even though this is not visible in the amplitude responses. Did you try doubling the filter order? Alternatively, if you have a high offset, would removing the offset before filtering be an option?
Okay I think it has to do with the fact that scipy.signal.minimum_phase
halves the filter order, which is in the SciPy docs, but should be optional. It makes the attenuation at DC two orders of magnitude worse. I'm working on a PR to SciPy for this, which we can fairly easily backport/vendor until our min version reaches the next SciPy release.
Fixed by https://github.com/scipy/scipy/pull/19706, will backport to mne.fixes
once it lands
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
Expected results
The zero-phase non-causal filter behaves as expected:
Actual results
The causal minimum phase filter does not behave as epxected:
This is in line with what I observe on actual MEG data.
Additional information
I am in a fresh conda env and on
main
withscipy
version1.11.4
. @larsoner predicts this to be a problem withscipy.signal.minimum_phase
and that is indeed the difference in ourfilter.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).