-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Preprocessing] add causal backward filtering to correct hardware induced phase shift #2942
[Preprocessing] add causal backward filtering to correct hardware induced phase shift #2942
Conversation
PR related to issue #2943. |
causal: True or False, default: False | ||
if True, backward causal filtering is performed to correct hardware induced phase shift |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe it would be better to have causal_mode
:
filtfilt
forward
backward
?
Filter form of the filter coefficients: | ||
- second-order sections ("sos") | ||
- numerator/denominator : ("ba") | ||
coef : array or None, default: None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert these changes. There should be a space before the :
Hey @JuanPimientoCaicedo I think the git history went funny, this can happen if you are unlucky and a big formatting change is merged after you branched off from SpikeInterface main. One suggestion, as no one else as contributed to this branch, is rebasing off an updated main branch and then force pushing. To do this you would:
There are other ways around this problem you may want to try and do not involve force pushing / rebasing, but this is the one that comes first to my mind. Please let me know if you want to try this approach and have any questions! |
bba53c3
to
8bbccab
Compare
I will sync the fork and make the changes again, there were just a few lines, thanks @JoeZiminski. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, I find the semantics of this a bit confusing.
Do I understand correctly that you goal is to rever the phase shift introduced by harward causal filters? If that's correct I have some suggestions that I think can improve the semantics and the maintenance.
You are trying to rever the effects of a causal filter but the process that is used for that is not causal, hence, I don't think CausalFilterRecording
is a good name or causal
is a good keyword to route this behavior in the corresponding recording segment. I would call it by what you are doing which is BackwardsFilter
and then explain in the docs that if you run this with the same parameters of the Forward Filter of the hardward you can get rid of the phase shift.
I have some more comments but I have to go. I will add more later.
elif self.filter_mode == "ba": | ||
b, a = self.coeff | ||
filtered_traces = scipy.signal.filtfilt(b, a, traces_chunk, axis=0) | ||
if causal: | ||
filtered_traces = np.flip(scipy.signal.lfilter(b, a, np.flip(traces_chunk), axis=0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the same comment for line 154 is unecessarily fliping the channels as well. I think you should only flip the time axis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, yes. You are right about the channels flipping as well. that is missing: the correct line should look like this, right?:
filtered_traces = np.flip(scipy.signal.lfilter(b, a, np.flip(traces_chunk, axis = 0), axis=0))
Yes, BackwardsFilter sounds better and it is more self explanatory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I am wondering if just using the reverse notation traces[::-1, :]
is better. I think that using this instead of flip creates a view instead of a new memory allocation.
The scipy filter functions are really memory hungry. I will test this.
For me, this should be generalised to a causal filter implementation rather than its implementation tailored to a specific goal. Using a causal filter to reverse the phase shift from the NP hardware would be just a specific use-case of the more generally implemented causal filter e.g BTW @JuanPimientoCaicedo apologies for the delay reply to the message in the thread, I think in general what you wrote is correct but note Jennifer comment on the Neuropixels slack thread, the actual filter implementation in the headstage may be more complex than first order RC filters and may not be well modelled by first order butterworth. ATM I am looking a little more into these switched capacitor filters, the transfer function of some typical implementations can be found here (slide 37) so was interested in looking at how different their phase response is from the standard RC filter and responding properly ASAP. But like Jennifer said I think the safest way is to get the exact implementation details from imec, maybe we can meet with her and anyone else interested to help get a response from imec. |
Hi, @JoeZiminski. No worries! You are right, I had the idea from a previous slack comment that they were single pole RC filters. It looks like instead of assuming some configuration, it is important to use the specific parameters as @oliche pointed out. Looking forward to having that information at hand. Jennifer said that she was going to ask the imec engineers almost 2 weeks ago, however, she has not posted anything new since then... I wonder if they replied back. |
@JoeZiminski I am thinking
This makes me reconsider that backwards might be a bad name but I am afraid of getting too much into details that will bog down the contribution of this PR. Right now we have two kind of preprocessing filters in the API:
Note that I feel that at the implementation, testing and interface of this would be easier if we think on this contribution as a purpose specific filter: That is, we are adding I follow the heuristic that in most cases it is better to create an example and then generalize after but maybe I am wrong here. I really think that this should be the call of @JuanPimientoCaicedo on what he wants to do because both scopes seem very different. He can either want to create a specific purpose filter with interface tailored for this case (the internals we can adjust later) or maybe he wants to create general machinery for 1) causal filters, 2) applying filters with reverse time, than then the user can use for this purpose of something else. Note that the documentation and testing of both cases would be very different with the latter being harder I feel. |
In general I would agree with you that first making a single example is better and then generalise later to avoid premature generalisation. However in this case I think it is worth making it general for the following reasons:
|
I like the generalized causal filter implementation with the Now, @h-mayorquin points out something important, It might be better to develop that function outside of the current Jennifer pointed out she does not have the filter details yet. I am wondering if it is possible to obtain the filter responses in any other way, in order to try to advance on the specific details of this implementation. |
@JoeZiminski @JuanPimientoCaicedo this makes sense to me. I just did not want to burden Juan with something more general. Creating a new CausalFilter class based on
How do you think the details of the implementation will affect the PR here? If we are going for the more general version, the API should expose generic parameters that define the filter like this: And then, once you have those details you can create a filter, pass the parameters as in the above with |
Sorry for the delay, I am going to work on this later this week. Yeah, it makes sense. So, to be clear, I am planning to create a new class called CausalFilter that works independently of the FilterRecording class. The main differences there are the implementation of |
That's what I am proposing to avoid having way too many branches on the segment of the current filter. |
I like the idea of not overloading True though as you mentioned though once we find the information about the headstage filters we might need to expose more parameters anyway. Maybe the easiest way will be to take directly |
Yeah, we come at this from very different places. We have had this disagreement before. Outside of code by PhD students or data scientists in my previous job I never seen a place that really needs more DRY. Most coders love abstractions and the concept of generalizing. On the other hand, I have seen too many early generalizations, conditional structures with number_of_branches > 5, wrong abstractions, gigantic one-liners, etc. I would be curios what do you think of the common criticisms to DRY as a principle but let's have this discussion outside of this thread. My concrete argument here: |
Anyway, I think that the call should be of @JuanPimientoCaicedo . This is just what I believe is best based on the reasons above but I think both implementations work. |
Yes I think it is a philosophical difference in approaches, it will be good to discuss more some time 😄 For me the complex structures that can occur when trying to apply DRY are a problem but my preferred solution is to better modularise them but not at the expense of DRY. One area that I found DRY key was making it easy to add new features to codebases without introducing bugs. Anyways, a discussion for another day! I agree @JuanPimientoCaicedo please go ahead in whatever way feels natural. I was thinking it would require complete rewriting of |
Hi, guys. I just started working in the new class and noticed that the get_traces() method is from the FilterRecordingSegment class. And now I am wondering which could be the best way to add this feature without affecting the current functionality. should I create two subclasses like this?:
Is there a better way to do it? |
Yes, if you create a new class you will need to create both a recording and a recording segment. I don't think you need to inherit from the other classess directly. You can inherit from the abstract class themselves. |
ea0241c
to
4539550
Compare
1. solved a typo in filter class. 2. added a new causal_filter class and function.
for more information, see https://pre-commit.ci
We are reading this implementation and discuss with lots of delays. We could propose to modify the FilterRecording instead but we could have a function 'causal_filter` that use this class and explicitly expose the direction parameter. |
That's what @JoeZiminski was proposing, @samuelgarcia . I don't like it because the filtfilt family functions are forward-backward only and I think you will end up with a more complicated functions with more if branches. The final proposal though was that @JuanPimientoCaicedo implements it in the way that he finds it easier and we can refactor aftewards. |
Hi, @samuelgarcia. I can create another PR with that implementation and that way you can compare both approaches. You can choose the one you prefer. How does that sound? |
Closing in favor of #3133 |
Hi, guys. I worked in a new class to include a causal backward filtering to the Spikeinterface preprocessing pipeline.
I did this to be able to correct for the hardware induced phase shift in the lf and ap band during neuropixel recordings. This is the first time I do this, so I don't know if this is the correct way of proposing these changes.
Please let me know what you think!