Repository navigation
Add method="fir" to highpass_filter and bandpass_filter
#4856
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
Open
galenlynch
wants to merge
7
commits into
SpikeInterface:main
Choose a base branch
from
galenlynch:feat/bandpass-fir-method
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9242627
feat: add fir_filter step and FIR bandpass and highpass designs
galenlynch f795fbf
test: import scipy inside the fir_filter tests that use it
galenlynch 4728196
feat: add method="fir" to bandpass_filter and highpass_filter
galenlynch 578ff12
docs: drop the measured slowdown from the rfft length docstring
galenlynch 9f17a09
Merge branch 'feat/fir-filter' into feat/bandpass-fir-method
galenlynch f14a973
Merge branch 'main' into feat/bandpass-fir-method
alejoe91 758ed86
Merge branch 'main' into feat/bandpass-fir-method
alejoe91 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| """Filter design, separate from filter application, so kernels can be inspected and composed before use.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
|
|
||
|
|
||
| def _stopband_db(taps, fs, edges): | ||
| """Smallest attenuation, in dB, of `taps` over the frequency intervals `edges`, sampled 16 times per ripple.""" | ||
| import scipy.signal | ||
|
|
||
| worst = 0.0 | ||
| for low, high in edges: | ||
| freqs = np.linspace(low, high, max(64, int(16 * (high - low) / fs * taps.size))) | ||
| _, response = scipy.signal.freqz(taps, worN=freqs, fs=fs) | ||
| worst = max(worst, np.abs(response).max()) | ||
| return -20 * np.log10(max(worst, 1e-300)) | ||
|
|
||
|
|
||
| def design_fir_bandpass(freq_min, freq_max, fs, transition_low_hz=175.0, transition_high_hz=2000.0, stopband_db=45.0): | ||
| """ | ||
| Design a linear-phase bandpass FIR kernel by the Kaiser window method. | ||
|
|
||
| The response is -6 dB at `freq_min` and `freq_max`, and at least `stopband_db` down below | ||
| ``freq_min - transition_low_hz / 2`` and above ``freq_max + transition_high_hz / 2``. Passband ripple is at | ||
| most about ``2 * 10 ** (-stopband_db / 20)`` in amplitude. The kernel convolves a highpass and a lowpass, | ||
| each windowed to its own transition: one window would give both edges the narrower width, and a sharp | ||
| upper edge rings for longer. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| freq_min, freq_max : float | ||
| Lower and upper -6 dB cutoffs, in Hz. | ||
| fs : float | ||
| Sampling frequency, in Hz. | ||
| transition_low_hz, transition_high_hz : float, default: 175.0, 2000.0 | ||
| Width of the transition band centred on each cutoff, in Hz. | ||
| stopband_db : float, default: 45.0 | ||
| Least attenuation in both stopbands, in dB. | ||
|
|
||
| Returns | ||
| ------- | ||
| taps : np.ndarray | ||
| Symmetric, odd-length float64 kernel, for ``fir_filter``. | ||
| """ | ||
| import scipy.signal | ||
|
|
||
| stop_low = freq_min - transition_low_hz / 2 | ||
| stop_high = freq_max + transition_high_hz / 2 | ||
| if transition_low_hz <= 0 or transition_high_hz <= 0 or stopband_db <= 0: | ||
| raise ValueError("transition_low_hz, transition_high_hz and stopband_db must be positive") | ||
| if not 0 < stop_low < freq_min < freq_max < stop_high < fs / 2: | ||
| raise ValueError( | ||
| f"The transition bands, [{stop_low}, {freq_min + transition_low_hz / 2}] and " | ||
| f"[{freq_max - transition_high_hz / 2}, {stop_high}] Hz, must lie in (0, {fs / 2}) Hz in that order" | ||
| ) | ||
|
|
||
| def edge(cutoff, width, pass_zero): | ||
| num_taps, beta = scipy.signal.kaiserord(stopband_db, width / (fs / 2)) | ||
| return num_taps | 1, beta, cutoff, pass_zero | ||
|
|
||
| edges = [edge(freq_min, transition_low_hz, False), edge(freq_max, transition_high_hz, True)] | ||
| stopbands = [(0.0, stop_low), (stop_high, fs / 2)] | ||
| while True: | ||
| highpass, lowpass = ( | ||
| scipy.signal.firwin(n, cutoff, window=("kaiser", beta), pass_zero=pass_zero, fs=fs) | ||
| for n, beta, cutoff, pass_zero in edges | ||
| ) | ||
| taps = np.convolve(highpass, lowpass) | ||
| # The two kernels' ripples can add, so the window's nominal attenuation is checked and the failing edge | ||
| # lengthened. | ||
| misses = [_stopband_db(taps, fs, [band]) < stopband_db for band in stopbands] | ||
| if not any(misses): | ||
| return taps | ||
| edges = [(n + 2 * miss, beta, cutoff, pz) for (n, beta, cutoff, pz), miss in zip(edges, misses)] | ||
|
|
||
|
|
||
| def design_fir_highpass(freq_min, fs, transition_low_hz=175.0, stopband_db=45.0): | ||
| """ | ||
| Design a linear-phase highpass FIR kernel by the Kaiser window method. | ||
|
|
||
| The response is -6 dB at `freq_min` and at least `stopband_db` down below ``freq_min - transition_low_hz / 2``, | ||
| with passband ripple of at most about ``10 ** (-stopband_db / 20)`` in amplitude. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| freq_min : float | ||
| The -6 dB cutoff, in Hz. | ||
| fs : float | ||
| Sampling frequency, in Hz. | ||
| transition_low_hz : float, default: 175.0 | ||
| Width of the transition band centred on the cutoff, in Hz. | ||
| stopband_db : float, default: 45.0 | ||
| Least attenuation in the stopband, in dB. | ||
|
|
||
| Returns | ||
| ------- | ||
| taps : np.ndarray | ||
| Symmetric, odd-length float64 kernel, for ``fir_filter``. | ||
| """ | ||
| import scipy.signal | ||
|
|
||
| stop = freq_min - transition_low_hz / 2 | ||
| if transition_low_hz <= 0 or stopband_db <= 0: | ||
| raise ValueError("transition_low_hz and stopband_db must be positive") | ||
| if not 0 < stop < freq_min + transition_low_hz / 2 < fs / 2: | ||
| raise ValueError( | ||
| f"The transition band, [{stop}, {freq_min + transition_low_hz / 2}] Hz, must lie in (0, {fs / 2}) Hz" | ||
| ) | ||
| num_taps, beta = scipy.signal.kaiserord(stopband_db, transition_low_hz / (fs / 2)) | ||
| num_taps |= 1 # a highpass needs a type I kernel | ||
| while True: | ||
| taps = scipy.signal.firwin(num_taps, freq_min, window=("kaiser", beta), pass_zero=False, fs=fs) | ||
| if _stopband_db(taps, fs, [(0.0, stop)]) >= stopband_db: | ||
| return taps | ||
| num_taps += 2 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Instead of this custom
_init_fir, maybe it would be better to extend theFilterRecordingwith themethodand accepttapsas an optional argument? Then all the logic of instantiating different segment typees could go there. It is weird that theBandpassFilterRecordingisFilterRecording, but ifmethod=="fir"it's not aFilterRecordinganymore.@galenlynch what do you think?