Skip to content
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

ENH: Improve FIR path of signal.decimate #5975

Merged
merged 2 commits into from
Apr 8, 2016
Merged

Conversation

e-q
Copy link
Contributor

@e-q e-q commented Mar 16, 2016

This PR aims to relieve the phase shift due to the group delay of an FIR downsampling filter in signal.decimate through the use of resample_poly.

This requires modifying resample_poly to accept arbitrary FIR filter coefficients, as decimate itself advertises that capability. A test for resample_poly has been added for this capability. Hopefully this isn't too much of a hack. (Thoughts, @Eric89GXL ?)

@codecov-io
Copy link

@@            master   #5975   diff @@
======================================
  Files          238     238       
  Stmts        43803   43814    +11
  Branches      8211    8214     +3
  Methods          0       0       
======================================
+ Hit          34230   34244    +14
+ Partial       2603    2602     -1
+ Missed        6970    6968     -2

Review entire Coverage Diff as of 31f7fbd

Powered by Codecov. Updated on successful CI builds.

@larsoner
Copy link
Member

This requires modifying resample_poly to accept arbitrary FIR filter coefficients

It's good to add that capability. I didn't do it originally to keep the PR simple, and to offload the work of getting it right on the next developer. Looks like you fell into my trap :)

@@ -1896,6 +1896,9 @@ def resample_poly(x, up, down, axis=0, window=('kaiser', 5.0)):
Desired window to use to design the low-pass filter. See
`scipy.signal.get_window` for a list of windows and required
parameters.
num : array_like, optional
FIR filter coefficients of the low-pass filter to be used. Overrides
the `window` argument if not None.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just roll this into window. See e.g. how resample allows multiple types for window:

http://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.signal.resample.html

@larsoner
Copy link
Member

So we're swapping in (currently) resample_poly for filtfilt and (soon) upfirdn for lfilter to save time. It would be nice in the tests to have the outputs computed using the naive filtfilt and lfilter algorithms, and compare outputs of decimate (which will use resample_poly and upfirdn under the hood). They should hopefully match well using assert_allclose. It helps ensure we are really getting equivalent operations. WDYT?

@larsoner
Copy link
Member

BTW can you add a comment about the speed gains? We doing 1 / down operations by using upfirdn compared to lfilter. Also, something neat under the hood is that instead of doing a two-pass operation for filtfilt, resample_poly does a single filter operation by taking advantage of associativity (x(t) * h(t)) * h(-t) = (x(t) * (h(t) * h(-t))), i.e. convolving the filter with it's time-reversed version, then dealing with the zero padding properly.

Speaking of which, this is one way that you might need to change the code -- I think that filtfilt would apply the same filter twice -- once forward and once backward -- whereas resample_poly will only apply it once (and it will assume you've constructed your filter in this symmetrical way). So maybe in this function, the window function/array needs to be convolved with it's time-reversed version before being passed to resample_poly? You should see it in the tests if you implement what I mention above.

@larsoner larsoner added enhancement A new feature or improvement scipy.signal needs-work Items that are pending response from the author labels Mar 22, 2016
@e-q
Copy link
Contributor Author

e-q commented Mar 24, 2016

Thanks for the feedback @Eric89GXL! I've incorporated much of your feedback.

The speed gains of upfirdn are quite good for larger downsampling factors, which is great. Where were you thinking of the speedups being mentioned, besides the commit message?

As for the upfirdn vs. lfilter results, I'm opening a separate PR that adds it to the upfirdn tests (which seems a little more natural to me). I haven't yet tried resample_poly vs. filtfilt yet. I think this should work as long as the filtfilt is explicitly given the zero-padding argument...

@larsoner
Copy link
Member

Where were you thinking of the speedups being mentioned, besides the commit message?

A comment in the code saying why upfirdn is preferred to lfilter (polyphase resampling avoids unnecessary calculations) would be a good place

I haven't yet tried resample_poly vs. filtfilt yet. I think this should work as long as the filtfilt is explicitly given the zero-padding argument...

I don't expect the zero-padding to really matter. resample_poly must zero pad and remove zeros at the end because it only does a single pass, in one direction, and computes only specific subsamples -- this introduces a phase shift, so the data are not centered after the operation. filtfilt on the other hand does two passes, one forward and one backward, and this is a zero-phase operation. So hopefully you don't have to do anything too special, mostly:

  1. Pass h as the filter to filtfilt
  2. Extend resample_poly to allow for arbitrary filters
  3. Pass convolve(h, h[::-1]) as the filter to resample_poly

@e-q e-q changed the title WIP, ENH: Improve FIR path of signal.decimate ENH: Improve FIR path of signal.decimate Mar 26, 2016
@e-q
Copy link
Contributor Author

e-q commented Mar 26, 2016

Ok, testing vs filtfilt is included now. You were exactly right about passing the convolved filter, thanks.

I was unclear before, the zero-padding I was referring to was in regards to the input signal, not the filter. filtfilt does an odd signal extension by default, but can do constant padding. Since, according to the docstring, resample_poly assumes zeros outside of the input samples, I set the first and last samples to zero and used filtfilt's constant padding mode. Anyways, no big deal, the test passes on my machine.

@larsoner
Copy link
Member

larsoner commented Mar 26, 2016 via email

@e-q
Copy link
Contributor Author

e-q commented Mar 27, 2016

Decimating x=np.random.randn(10**7) with resample_poly, using a 31 tap FIR filter, is actually faster than filtfilt by more than a factor of down on my machine.

q    filtfilt    resample_poly   speedup
----------------------------------------
2    591ms       211.0ms          2.8
5    577ms        98.7ms          5.8
13   604ms        44.4ms         13.6

This comes from the additional time cost of slicing the output of filtfilt. Without the slicing, the speedup for q=13 is 12.97, for instance.

@larsoner
Copy link
Member

Excellent. Is this good to go from your end, then?

@e-q
Copy link
Contributor Author

e-q commented Mar 27, 2016

yep!

f_c = 1. / max_rate # cutoff of FIR filter (rel. to Nyquist)
half_len = 10 * max_rate # reasonable cutoff for our sinc-like function
h = firwin(2 * half_len + 1, f_c, window=window)
n_out = int(np.ceil(x.shape[axis] * up / down))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to effectively be floor, why the change to ceil?

I assume it's for the extra sample in case it doesn't divide evenly, In that case it would actually be better to use integer arithmetic if possible so we stay exact, like:

n_out = x.shape['axis'] * up
n_out = n_out // down + bool(n_out % down)

@larsoner
Copy link
Member

Other than my minor comments, LGTM.

@e-q
Copy link
Contributor Author

e-q commented Mar 27, 2016

Good points all around, addressed and squashed. Thanks!

The short answer about the change of n_out for resample_poly was to make it match the number of outputs from decimate, which in turn is given by how many outputs you get from slicing x[::q].

Incidentally, you'll see in the zero_phase=False, ftype='fir' path that the output from upfirdn is truncated to that same amount. It seems that upfirdn tacks a few more output points on than I would expect from the decimation point of view. Why is this?

@larsoner
Copy link
Member

IIRC upfirdn lets the filter ring out to the end, like np.convolve(..., mode='full')

@e-q
Copy link
Contributor Author

e-q commented Apr 5, 2016

Is this good to go?

step, so it should be designed to operate on a signal at a sampling
frequency higher than the original by a factor of `up//gcd(up, down)`.
This function's output will be centered with respect to this array, so it
is best to pass a symmetric filter with an odd number of samples to if, as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"number of samples to if" -> "number of samples if"

@larsoner
Copy link
Member

larsoner commented Apr 5, 2016

Other than my one new gripe, +1 for merge. After the rewording, let's wait a day or two to see if anyone else wants to comment then merge

e-q added 2 commits April 5, 2016 13:41
This PR aims to relieve the phase shift due to the group delay of an FIR
downsampling filter in signal.decimate through the use of `resample_poly`.

This requires modifying `resample_poly` to accept arbitrary FIR filter
coefficients, as `decimate` itself advertises that capability. A test for
`resample_poly` has been added for this capability.

Additionally, the "traditional" FIR path has been sped up via the use of
`upfirdn` which only calculates every q'th output.
Since `resample_poly` is used instead of `filtfilt` in `decimate` for FIR
decimation for speed reasons, this adds a test to ensure equivalent output of
these two methods.
@e-q
Copy link
Contributor Author

e-q commented Apr 5, 2016 via email

@larsoner larsoner merged commit b65314b into scipy:master Apr 8, 2016
@larsoner
Copy link
Member

larsoner commented Apr 8, 2016

Thanks @e-q

@ev-br ev-br added this to the 0.18.0 milestone Apr 8, 2016
@@ -3,12 +3,29 @@
import numpy as np

try:
from scipy.signal import lfilter, firwin
from scipy.signal import lfilter, firwin, decimate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually best to put these in a separate import statement, so that not having decimate does not prevent running eg the firwin benchmarks

@e-q e-q deleted the firdecimate branch April 10, 2016 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A new feature or improvement needs-work Items that are pending response from the author scipy.signal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants