-
Notifications
You must be signed in to change notification settings - Fork 349
Add pyfftw sdp #1132
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
NimaSarajpoor
wants to merge
18
commits into
stumpy-dev:main
Choose a base branch
from
NimaSarajpoor:add_pyfftw_sdp
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.
+342
−10
Open
Add pyfftw sdp #1132
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
87dfcdb
add pyfftw sdp and check for fftw/pyfftw
NimaSarajpoor ae90507
fixed coverage
NimaSarajpoor 6051ab4
addressed comments
NimaSarajpoor 0a3427e
improved readability
NimaSarajpoor 58d99e2
improved docstring
NimaSarajpoor 0e82fbf
add more comments
NimaSarajpoor db09a61
minor change
NimaSarajpoor 73d8229
improved comment
NimaSarajpoor 61d8351
fixed flake8
NimaSarajpoor 96b68b5
minor change
NimaSarajpoor 2ebbac1
empty commit
NimaSarajpoor ec3744d
revise condition for checking pyffftw
NimaSarajpoor 22e5a55
add param to change dtype and check multi-thresding
NimaSarajpoor 7e75ade
fixed coverage
NimaSarajpoor 68d767c
addressed comments
NimaSarajpoor d6fda46
remove instance of class from stumpy module
NimaSarajpoor 51a21eb
addressed comments
NimaSarajpoor de9cade
pass default value when creating an instance
NimaSarajpoor 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
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,13 @@ | |||||
|
|
||||||
| from . import config | ||||||
|
|
||||||
| try: # pragma: no cover | ||||||
| import pyfftw | ||||||
|
|
||||||
| PYFFTW_IS_AVAILABLE = True | ||||||
| except ImportError: # pragma: no cover | ||||||
| PYFFTW_IS_AVAILABLE = False | ||||||
|
|
||||||
|
|
||||||
| @njit(fastmath=config.STUMPY_FASTMATH_TRUE) | ||||||
| def _njit_sliding_dot_product(Q, T): | ||||||
|
|
@@ -109,6 +116,223 @@ def _pocketfft_sliding_dot_product(Q, T): | |||||
| return c2r(False, np.multiply(fft_2d[0], fft_2d[1]), n=next_fast_n)[m - 1 : n] | ||||||
|
|
||||||
|
|
||||||
| class _PYFFTW_SLIDING_DOT_PRODUCT: | ||||||
| """ | ||||||
| A class to compute the sliding dot product using FFTW via pyfftw. | ||||||
|
|
||||||
| This class uses FFTW (via pyfftw) to efficiently compute the sliding dot product | ||||||
| between a query sequence, Q, and a time series, T. It preallocates arrays and caches | ||||||
| FFTW objects to optimize repeated computations with similar-sized inputs. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| max_n : int, default=2**20 | ||||||
| Maximum length to preallocate arrays for. This will be the size of the | ||||||
| real-valued array. A complex-valued array of size `1 + (max_n // 2)` | ||||||
| will also be preallocated. If inputs exceed this size, arrays will be | ||||||
| reallocated to accommodate larger sizes. | ||||||
|
|
||||||
| Attributes | ||||||
| ---------- | ||||||
| real_arr : pyfftw.empty_aligned | ||||||
| Preallocated real-valued array for FFTW computations. | ||||||
|
|
||||||
| complex_arr : pyfftw.empty_aligned | ||||||
| Preallocated complex-valued array for FFTW computations. | ||||||
|
|
||||||
| rfft_objects : dict | ||||||
| Cache of FFTW forward transform objects with | ||||||
| (next_fast_n, n_threads, planning_flag) as lookup keys. | ||||||
|
|
||||||
| irfft_objects : dict | ||||||
| Cache of FFTW inverse transform objects with | ||||||
| (next_fast_n, n_threads, planning_flag) as lookup keys. | ||||||
|
|
||||||
| Methods | ||||||
| ------- | ||||||
| __call__(Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE") | ||||||
| Compute the sliding dot product between `Q` and `T` using FFTW | ||||||
| via pyfftw, and cache FFTW objects if not already cached. | ||||||
|
|
||||||
| Notes | ||||||
| ----- | ||||||
| The class maintains internal caches of FFTW objects to avoid redundant planning | ||||||
| operations when called multiple times with similar-sized inputs and parameters. | ||||||
|
NimaSarajpoor marked this conversation as resolved.
|
||||||
| When `planning_flag=="FFTW_ESTIMATE"`, there will be no planning operation. | ||||||
| However, caching FFTW objects is still beneficial as the overhead of creating | ||||||
| those objects can be avoided in subsequent calls. | ||||||
|
|
||||||
| Examples | ||||||
| -------- | ||||||
| >>> sdp_obj = _PYFFTW_SLIDING_DOT_PRODUCT(max_n=1000) | ||||||
| >>> Q = np.array([1, 2, 3]) | ||||||
| >>> T = np.array([4, 5, 6, 7, 8]) | ||||||
| >>> result = sdp_obj(Q, T) | ||||||
|
|
||||||
| References | ||||||
| ---------- | ||||||
| `FFTW documentation <http://www.fftw.org/>`__ | ||||||
|
|
||||||
| `pyfftw documentation <https://pyfftw.readthedocs.io/>`__ | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, max_n=2**20, real_dtype="float64"): | ||||||
| """ | ||||||
| Initialize the `_PYFFTW_SLIDING_DOT_PRODUCT` object, which can be called | ||||||
| to compute the sliding dot product using FFTW via pyfftw. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| max_n : int, default=2**20 | ||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Maximum length to preallocate arrays for. This will be the size of the | ||||||
| real-valued array. A complex-valued array of size `1 + (max_n // 2)` | ||||||
| will also be preallocated. | ||||||
|
|
||||||
| real_dtype : str, default="float64" | ||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| The real data type to use for the preallocated arrays. Must be either | ||||||
| "float64" or "longdouble". The complex data type will be set to | ||||||
| "complex128" or "clongdouble", respectively. | ||||||
|
|
||||||
| Returns | ||||||
| ------- | ||||||
| None | ||||||
| """ | ||||||
| REAL_TO_COMPLEX_MAP = { | ||||||
| "float64": "complex128", | ||||||
| "longdouble": "clongdouble", | ||||||
| } | ||||||
| if real_dtype not in ["float64", "longdouble"]: # pragma: no cover | ||||||
| raise ValueError( | ||||||
| f"Invalid real_dtype: {real_dtype}. Must be 'float64' or 'longdouble'." | ||||||
| ) | ||||||
| complex_dtype = REAL_TO_COMPLEX_MAP[real_dtype] | ||||||
|
|
||||||
| # Preallocate arrays | ||||||
| self.real_arr = pyfftw.empty_aligned(max_n, dtype=real_dtype) | ||||||
| self.complex_arr = pyfftw.empty_aligned(1 + (max_n // 2), dtype=complex_dtype) | ||||||
|
|
||||||
| # Store FFTW objects, keyed by (next_fast_n, n_threads, planning_flag) | ||||||
| self.rfft_objects = {} | ||||||
| self.irfft_objects = {} | ||||||
|
|
||||||
| def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): | ||||||
| """ | ||||||
| Compute the sliding dot product between `Q` and `T` using FFTW via pyfftw, | ||||||
| and cache FFTW objects if not already cached. | ||||||
|
|
||||||
| Parameters | ||||||
| ---------- | ||||||
| Q : numpy.ndarray | ||||||
| Query array or subsequence. | ||||||
|
|
||||||
| T : numpy.ndarray | ||||||
| Time series or sequence. | ||||||
|
|
||||||
| n_threads : int, default=1 | ||||||
| Number of threads to use for FFTW computations. | ||||||
|
|
||||||
| planning_flag : str, default="FFTW_ESTIMATE" | ||||||
| The planning flag that will be used in FFTW for planning. | ||||||
| See pyfftw documentation for details. Current options, ordered | ||||||
| ascendingly by the level of aggressiveness in planning, are: | ||||||
| "FFTW_ESTIMATE", "FFTW_MEASURE", "FFTW_PATIENT", and "FFTW_EXHAUSTIVE". | ||||||
| The more aggressive the planning, the longer the planning time, but | ||||||
| the faster the execution time. | ||||||
|
|
||||||
| Returns | ||||||
| ------- | ||||||
| out : numpy.ndarray | ||||||
| Sliding dot product between `Q` and `T`. | ||||||
|
|
||||||
| Notes | ||||||
| ----- | ||||||
| The planning_flag is defaulted to "FFTW_ESTIMATE" to be aligned with | ||||||
| MATLAB's FFTW usage (as of version R2025b) | ||||||
| See: https://www.mathworks.com/help/matlab/ref/fftw.html | ||||||
|
|
||||||
| This implementation is inspired by the answer on StackOverflow: | ||||||
| https://stackoverflow.com/a/30615425/2955541 | ||||||
| """ | ||||||
| m = Q.shape[0] | ||||||
| n = T.shape[0] | ||||||
| next_fast_n = pyfftw.next_fast_len(n) | ||||||
|
|
||||||
| # Update preallocated arrays if needed | ||||||
| if next_fast_n > len(self.real_arr): | ||||||
| self.real_arr = pyfftw.empty_aligned(next_fast_n, dtype=self.real_arr.dtype) | ||||||
| self.complex_arr = pyfftw.empty_aligned( | ||||||
| 1 + (next_fast_n // 2), dtype=self.complex_arr.dtype | ||||||
| ) | ||||||
|
|
||||||
| real_arr = self.real_arr[:next_fast_n] | ||||||
| complex_arr = self.complex_arr[: 1 + (next_fast_n // 2)] | ||||||
|
|
||||||
| # Get or create FFTW objects | ||||||
| key = (next_fast_n, n_threads, planning_flag) | ||||||
|
|
||||||
| rfft_obj = self.rfft_objects.get(key, None) | ||||||
|
NimaSarajpoor marked this conversation as resolved.
|
||||||
| irfft_obj = self.irfft_objects.get(key, None) | ||||||
|
|
||||||
| if rfft_obj is None or irfft_obj is None: | ||||||
| rfft_obj = pyfftw.FFTW( | ||||||
| input_array=real_arr, | ||||||
| output_array=complex_arr, | ||||||
| direction="FFTW_FORWARD", | ||||||
| flags=(planning_flag,), | ||||||
| threads=n_threads, | ||||||
| ) | ||||||
| irfft_obj = pyfftw.FFTW( | ||||||
| input_array=complex_arr, | ||||||
| output_array=real_arr, | ||||||
| direction="FFTW_BACKWARD", | ||||||
| flags=(planning_flag, "FFTW_DESTROY_INPUT"), | ||||||
| threads=n_threads, | ||||||
| ) | ||||||
| self.rfft_objects[key] = rfft_obj | ||||||
| self.irfft_objects[key] = irfft_obj | ||||||
| else: | ||||||
| # Update the input and output arrays of the cached FFTW objects | ||||||
| # in case their original input and output arrays were reallocated | ||||||
| # in a previous call | ||||||
| rfft_obj.update_arrays(real_arr, complex_arr) | ||||||
| irfft_obj.update_arrays(complex_arr, real_arr) | ||||||
|
|
||||||
| # Compute the (circular) convolution between T and Q[::-1], | ||||||
| # each zero-padded to length `next_fast_n` by performing | ||||||
| # the following three steps: | ||||||
|
|
||||||
| # Step 1 | ||||||
| # Compute RFFT of T (zero-padded) | ||||||
| # Must make a copy of output to avoid losing it when the array is | ||||||
| # overwritten when computing the RFFT of Q in the next step | ||||||
| rfft_obj.input_array[:n] = T | ||||||
| rfft_obj.input_array[n:] = 0.0 | ||||||
| rfft_obj.execute() | ||||||
| rfft_T = rfft_obj.output_array.copy() | ||||||
|
|
||||||
| # Step 2 | ||||||
| # Compute RFFT of Q (reversed, scaled, and zero-padded) | ||||||
| # Scaling is required because the thin wrapper `execute` | ||||||
| # that will be called below does not perform normalization | ||||||
| np.multiply(Q[::-1], 1.0 / next_fast_n, out=rfft_obj.input_array[:m]) | ||||||
| rfft_obj.input_array[m:] = 0.0 | ||||||
| rfft_obj.execute() | ||||||
| rfft_Q = rfft_obj.output_array | ||||||
|
|
||||||
| # Step 3 | ||||||
| # Convert back to time domain by taking the inverse RFFT | ||||||
| np.multiply(rfft_T, rfft_Q, out=irfft_obj.input_array) | ||||||
| irfft_obj.execute() | ||||||
|
|
||||||
| return irfft_obj.output_array[m - 1 : n] # valid portion | ||||||
|
|
||||||
|
|
||||||
| if PYFFTW_IS_AVAILABLE: # pragma: no cover | ||||||
| _pyfftw_sliding_dot_product = _PYFFTW_SLIDING_DOT_PRODUCT( | ||||||
| max_n=2**20, real_dtype="float64" | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| def _sliding_dot_product(Q, T): | ||||||
| """ | ||||||
| Compute the sliding dot product between `Q` and `T` | ||||||
|
|
||||||
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
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.
Uh oh!
There was an error while loading. Please reload this page.
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 add a comment here that says: "The name of any callable object that computes the sliding dot product should end with
sliding_dot_product"