From 87dfcdbef9c264cf7ff294cf4e04817cc636461b Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Tue, 17 Feb 2026 00:08:15 -0500 Subject: [PATCH 01/10] add pyfftw sdp and check for fftw/pyfftw --- stumpy/sdp.py | 189 ++++++++++++++++++++++++++++++++++++++++++++++ test.sh | 50 +++++++++--- tests/test_sdp.py | 25 ++++++ 3 files changed, 255 insertions(+), 9 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index bd1d8fea7..12ef280d5 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -6,6 +6,13 @@ from . import config +try: # pragma: no cover + import pyfftw + + FFTW_IS_AVAILABLE = True +except ImportError: # pragma: no cover + FFTW_IS_AVAILABLE = False + @njit(fastmath=config.STUMPY_FASTMATH_TRUE) def _njit_sliding_dot_product(Q, T): @@ -109,6 +116,188 @@ 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, keyed by + (next_fast_n, n_threads, planning_flag). + + irfft_objects : dict + Cache of FFTW inverse transform objects, keyed by + (next_fast_n, n_threads, planning_flag). + + Notes + ----- + The class maintains internal caches of FFTW objects to avoid redundant planning + operations when called multiple times with similar-sized inputs and parameters. + + 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 `__ + + `pyfftw documentation `__ + """ + + def __init__(self, max_n=2**20): + """ + 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 + 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. + + Returns + ------- + None + """ + # Preallocate arrays + self.real_arr = pyfftw.empty_aligned(max_n, dtype="float64") + self.complex_arr = pyfftw.empty_aligned(1 + (max_n // 2), dtype="complex128") + + # 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="float64") + self.complex_arr = pyfftw.empty_aligned( + 1 + (next_fast_n // 2), dtype="complex128" + ) + + 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) + if rfft_obj is None: + rfft_obj = pyfftw.FFTW( + input_array=real_arr, + output_array=complex_arr, + direction="FFTW_FORWARD", + flags=(planning_flag,), + threads=n_threads, + ) + self.rfft_objects[key] = rfft_obj + else: + rfft_obj.update_arrays(real_arr, complex_arr) + + irfft_obj = self.irfft_objects.get(key, None) + if irfft_obj is None: + 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.irfft_objects[key] = irfft_obj + else: + irfft_obj.update_arrays(complex_arr, real_arr) + + # RFFT(T) + real_arr[:n] = T + real_arr[n:] = 0.0 + rfft_obj.execute() # output is in complex_arr + complex_arr_T = complex_arr.copy() + + # RFFT(Q) + # Scale by 1/next_fast_n to account for + # FFTW's unnormalized inverse FFT via execute() + np.multiply(Q[::-1], 1.0 / next_fast_n, out=real_arr[:m]) + real_arr[m:] = 0.0 + rfft_obj.execute() # output is in complex_arr + + # RFFT(T) * RFFT(Q) + np.multiply(complex_arr, complex_arr_T, out=complex_arr) + + # IRFFT (input is in complex_arr) + irfft_obj.execute() # output is in real_arr + + return real_arr[m - 1 : n] + + +if FFTW_IS_AVAILABLE: + _pyfftw_sliding_dot_product = _PYFFTW_SLIDING_DOT_PRODUCT(max_n=2**20) + + def _sliding_dot_product(Q, T): """ Compute the sliding dot product between `Q` and `T` diff --git a/test.sh b/test.sh index 194288574..fc80e76c3 100755 --- a/test.sh +++ b/test.sh @@ -154,27 +154,58 @@ gen_ray_coveragerc() # Generate a .coveragerc_override file that excludes Ray functions and tests gen_coveragerc_boilerplate echo " def .*_ray_*" >> .coveragerc_override - echo " def ,*_ray\(*" >> .coveragerc_override + echo " def .*_ray\(*" >> .coveragerc_override echo " def ray_.*" >> .coveragerc_override echo " def test_.*_ray*" >> .coveragerc_override } -set_ray_coveragerc() +check_fftw_pyfftw() { - # If `ray` command is not found then generate a .coveragerc_override file - if ! command -v ray &> /dev/null + if ! command -v fftw-wisdom &> /dev/null \ + || ! python -c "import pyfftw" &> /dev/null; then - echo "Ray Not Installed" + echo "FFTW and/or pyFFTW Not Installed" + else + echo "FFTW and pyFFTW Installed" + fi +} + +gen_pyfftw_coveragerc() +{ + gen_coveragerc_boilerplate + echo " class .*PYFFTW*" >> .coveragerc_override + echo " def test_.*pyfftw*" >> .coveragerc_override +} + +set_coveragerc() +{ + fcoveragerc="" + + if ! command -v ray &> /dev/null; + then + echo "Ray not installed" gen_ray_coveragerc - fcoveragerc="--rcfile=.coveragerc_override" else - echo "Ray Installed" + echo "Ray installed" + fi + + if ! command -v fftw-wisdom &> /dev/null \ + || ! python -c "import pyfftw" &> /dev/null; + then + echo "FFTW and/or pyFFTW not Installed" + gen_pyfftw_coveragerc + else + echo "FFTW and pyFFTW Installed" + fi + + if [ -f .coveragerc_override ]; then + fcoveragerc="--rcfile=.coveragerc_override" fi } show_coverage_report() { - set_ray_coveragerc + set_coveragerc coverage report --show-missing --fail-under=100 --skip-covered --omit=fastmath.py,docstring.py,versions.py $fcoveragerc check_errs $? } @@ -361,6 +392,7 @@ check_print check_pkg_imports check_naive check_ray +check_fftw_pyfftw if [[ -z $NUMBA_DISABLE_JIT || $NUMBA_DISABLE_JIT -eq 0 ]]; then @@ -405,4 +437,4 @@ else test_coverage fi -clean_up +clean_up \ No newline at end of file diff --git a/tests/test_sdp.py b/tests/test_sdp.py index b8d98eeb9..6537fefda 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -74,6 +74,9 @@ def get_sdp_function_names(): if func_name.endswith("sliding_dot_product"): out.append(func_name) + if sdp.FFTW_IS_AVAILABLE: + out.append("_pyfftw_sliding_dot_product") + return out @@ -152,3 +155,25 @@ def test_sdp_power2(): raise e return + + +def test_pyfftw_sdp_max_n(): + if not sdp.FFTW_IS_AVAILABLE: # pragma: no cover + pytest.skip("Skipping Test pyFFTW Not Installed") + + # When `len(T)` larger than `max_n` in pyfftw_sdp, + # the internal preallocated arrays should be resized. + # This test checks that functionality. + max_n = 2**10 + sdp_func = sdp._PYFFTW_SLIDING_DOT_PRODUCT(max_n) + + # len(T) > max_n to trigger array resizing + T = np.random.rand(max_n + 1) + Q = np.random.rand(2**8) + + comp = sdp_func(Q, T) + ref = naive.rolling_window_dot_product(Q, T) + + np.testing.assert_allclose(comp, ref) + + return From ae9050705a5be4bed2e6caf1e53bd9525482ad50 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Tue, 17 Feb 2026 00:35:57 -0500 Subject: [PATCH 02/10] fixed coverage --- stumpy/sdp.py | 2 +- tests/test_sdp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 12ef280d5..6db96b22d 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -294,7 +294,7 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): return real_arr[m - 1 : n] -if FFTW_IS_AVAILABLE: +if FFTW_IS_AVAILABLE: # pragma: no cover _pyfftw_sliding_dot_product = _PYFFTW_SLIDING_DOT_PRODUCT(max_n=2**20) diff --git a/tests/test_sdp.py b/tests/test_sdp.py index 6537fefda..90abb3528 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -74,7 +74,7 @@ def get_sdp_function_names(): if func_name.endswith("sliding_dot_product"): out.append(func_name) - if sdp.FFTW_IS_AVAILABLE: + if sdp.FFTW_IS_AVAILABLE: # pragma: no cover out.append("_pyfftw_sliding_dot_product") return out From 6051ab424dd8575dc54c02f787bc01ebfb3d0b35 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Thu, 19 Feb 2026 22:47:31 -0500 Subject: [PATCH 03/10] addressed comments --- stumpy/sdp.py | 52 +++++++++++++++++++++++------------------------ tests/test_sdp.py | 4 ++-- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 6db96b22d..58c1af1f1 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -9,9 +9,9 @@ try: # pragma: no cover import pyfftw - FFTW_IS_AVAILABLE = True + PYFFTW_IS_AVAILABLE = True except ImportError: # pragma: no cover - FFTW_IS_AVAILABLE = False + PYFFTW_IS_AVAILABLE = False @njit(fastmath=config.STUMPY_FASTMATH_TRUE) @@ -121,7 +121,7 @@ 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 + 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 @@ -141,12 +141,12 @@ class _PYFFTW_SLIDING_DOT_PRODUCT: Preallocated complex-valued array for FFTW computations. rfft_objects : dict - Cache of FFTW forward transform objects, keyed by - (next_fast_n, n_threads, planning_flag). + 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, keyed by - (next_fast_n, n_threads, planning_flag). + Cache of FFTW inverse transform objects with + (next_fast_n, n_threads, planning_flag) as lookup keys. Notes ----- @@ -247,7 +247,9 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): key = (next_fast_n, n_threads, planning_flag) rfft_obj = self.rfft_objects.get(key, None) - if rfft_obj is None: + 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, @@ -255,12 +257,6 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): flags=(planning_flag,), threads=n_threads, ) - self.rfft_objects[key] = rfft_obj - else: - rfft_obj.update_arrays(real_arr, complex_arr) - - irfft_obj = self.irfft_objects.get(key, None) - if irfft_obj is None: irfft_obj = pyfftw.FFTW( input_array=complex_arr, output_array=real_arr, @@ -268,33 +264,35 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): flags=(planning_flag, "FFTW_DESTROY_INPUT"), threads=n_threads, ) + self.rfft_objects[key] = rfft_obj self.irfft_objects[key] = irfft_obj else: + rfft_obj.update_arrays(real_arr, complex_arr) irfft_obj.update_arrays(complex_arr, real_arr) - # RFFT(T) + # Compute RFFT of T real_arr[:n] = T real_arr[n:] = 0.0 - rfft_obj.execute() # output is in complex_arr - complex_arr_T = complex_arr.copy() + rfft_obj.execute() # output is stored in complex_arr - # RFFT(Q) - # Scale by 1/next_fast_n to account for - # FFTW's unnormalized inverse FFT via execute() + # need to make a copy since the array will be + # overwritten later during the RFFT(Q) step + rfft_of_T = complex_arr.copy() + + # Compute RFFT of Q (reversed and scaled by 1/next_fast_n) np.multiply(Q[::-1], 1.0 / next_fast_n, out=real_arr[:m]) real_arr[m:] = 0.0 - rfft_obj.execute() # output is in complex_arr - - # RFFT(T) * RFFT(Q) - np.multiply(complex_arr, complex_arr_T, out=complex_arr) + rfft_obj.execute() # output is stored in complex_arr + rfft_of_Q = complex_arr - # IRFFT (input is in complex_arr) - irfft_obj.execute() # output is in real_arr + # Compute IRFFT of the element-wise product of the RFFTs + np.multiply(rfft_of_Q, rfft_of_T, out=complex_arr) + irfft_obj.execute() # output is stored in real_arr return real_arr[m - 1 : n] -if FFTW_IS_AVAILABLE: # pragma: no cover +if PYFFTW_IS_AVAILABLE: # pragma: no cover _pyfftw_sliding_dot_product = _PYFFTW_SLIDING_DOT_PRODUCT(max_n=2**20) diff --git a/tests/test_sdp.py b/tests/test_sdp.py index 90abb3528..3ec26971f 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -74,7 +74,7 @@ def get_sdp_function_names(): if func_name.endswith("sliding_dot_product"): out.append(func_name) - if sdp.FFTW_IS_AVAILABLE: # pragma: no cover + if sdp.PYFFTW_IS_AVAILABLE: # pragma: no cover out.append("_pyfftw_sliding_dot_product") return out @@ -158,7 +158,7 @@ def test_sdp_power2(): def test_pyfftw_sdp_max_n(): - if not sdp.FFTW_IS_AVAILABLE: # pragma: no cover + if not sdp.PYFFTW_IS_AVAILABLE: # pragma: no cover pytest.skip("Skipping Test pyFFTW Not Installed") # When `len(T)` larger than `max_n` in pyfftw_sdp, From 0a3427e83c39a4f7dbc9b3036dfb82105297940a Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Thu, 19 Feb 2026 23:07:35 -0500 Subject: [PATCH 04/10] improved readability --- stumpy/sdp.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 58c1af1f1..11e0bb332 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -267,29 +267,34 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): 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 RFFT of T real_arr[:n] = T real_arr[n:] = 0.0 - rfft_obj.execute() # output is stored in complex_arr - - # need to make a copy since the array will be - # overwritten later during the RFFT(Q) step - rfft_of_T = complex_arr.copy() - - # Compute RFFT of Q (reversed and scaled by 1/next_fast_n) + rfft_obj.execute() + rfft_of_T = rfft_obj.output_array.copy() + # output array is stored in complex_arr, so make a copy + # to avoid losing it when it is overwritten when computing + # the RFFT of Q + + # Compute RFFT of Q (reversed, scaled, and zero-padded) + # Note: scaling is needed since the thin wrapper `execute` + # is called which does not perform normalization np.multiply(Q[::-1], 1.0 / next_fast_n, out=real_arr[:m]) real_arr[m:] = 0.0 - rfft_obj.execute() # output is stored in complex_arr - rfft_of_Q = complex_arr + rfft_obj.execute() + rfft_of_Q = rfft_obj.output_array - # Compute IRFFT of the element-wise product of the RFFTs - np.multiply(rfft_of_Q, rfft_of_T, out=complex_arr) - irfft_obj.execute() # output is stored in real_arr + # Convert back to time domain by taking the inverse RFFT + np.multiply(rfft_of_T, rfft_of_Q, out=irfft_obj.input_array) + irfft_obj.execute() - return real_arr[m - 1 : n] + return irfft_obj.output_array[m - 1 : n] if PYFFTW_IS_AVAILABLE: # pragma: no cover From 58d99e21f24168c79059f7528f256b8a2c403d19 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Thu, 19 Feb 2026 23:14:31 -0500 Subject: [PATCH 05/10] improved docstring --- stumpy/sdp.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 11e0bb332..91978810b 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -148,10 +148,19 @@ class _PYFFTW_SLIDING_DOT_PRODUCT: 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. + 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 -------- From 0e82fbfd63aca1d75f8e57bdbc2f184fc3330c77 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Fri, 20 Feb 2026 10:00:25 -0500 Subject: [PATCH 06/10] add more comments --- stumpy/sdp.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 91978810b..73bfe7385 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -282,28 +282,33 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): rfft_obj.update_arrays(real_arr, complex_arr) irfft_obj.update_arrays(complex_arr, real_arr) - # Compute RFFT of T - real_arr[:n] = T - real_arr[n:] = 0.0 + # Compute the circular convolution between T and Q[::-1] + # using FFT, and then take the relevant slice of the output + + # 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_of_T = rfft_obj.output_array.copy() - # output array is stored in complex_arr, so make a copy - # to avoid losing it when it is overwritten when computing - # the RFFT of Q + # Step 2 # Compute RFFT of Q (reversed, scaled, and zero-padded) - # Note: scaling is needed since the thin wrapper `execute` - # is called which does not perform normalization - np.multiply(Q[::-1], 1.0 / next_fast_n, out=real_arr[:m]) - real_arr[m:] = 0.0 + # 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_of_Q = rfft_obj.output_array + # Step 3 # Convert back to time domain by taking the inverse RFFT np.multiply(rfft_of_T, rfft_of_Q, out=irfft_obj.input_array) irfft_obj.execute() - return irfft_obj.output_array[m - 1 : n] + return irfft_obj.output_array[m - 1 : n] # valid portion if PYFFTW_IS_AVAILABLE: # pragma: no cover From db09a611a43ab3273f0ff668a94680cad7ad86ac Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Fri, 20 Feb 2026 10:01:00 -0500 Subject: [PATCH 07/10] minor change --- stumpy/sdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 73bfe7385..03ef94f03 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -282,7 +282,7 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): 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] + # Compute the (circular) convolution between T and Q[::-1] # using FFT, and then take the relevant slice of the output # Step 1 From 73d8229f7269b42630038abf41a2dc7347d7539a Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Fri, 20 Feb 2026 11:00:53 -0500 Subject: [PATCH 08/10] improved comment --- stumpy/sdp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 03ef94f03..0ac6a7dbc 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -282,8 +282,9 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): 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] - # using FFT, and then take the relevant slice of the output + # 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) From 61d83519c1571c8da08c75755ca0f3d19d177fa7 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Fri, 20 Feb 2026 11:01:47 -0500 Subject: [PATCH 09/10] fixed flake8 --- stumpy/sdp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 0ac6a7dbc..3723b6d53 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -282,7 +282,7 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): 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], + # Compute the (circular) convolution between T and Q[::-1], # each zero-padded to length `next_fast_n` by performing # the following three steps: From 96b68b531bb3e3b36440e82c6d9857a3166960b6 Mon Sep 17 00:00:00 2001 From: NimaSarajpoor Date: Fri, 20 Feb 2026 11:17:32 -0500 Subject: [PATCH 10/10] minor change --- stumpy/sdp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stumpy/sdp.py b/stumpy/sdp.py index 3723b6d53..a809bada9 100644 --- a/stumpy/sdp.py +++ b/stumpy/sdp.py @@ -293,7 +293,7 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): rfft_obj.input_array[:n] = T rfft_obj.input_array[n:] = 0.0 rfft_obj.execute() - rfft_of_T = rfft_obj.output_array.copy() + rfft_T = rfft_obj.output_array.copy() # Step 2 # Compute RFFT of Q (reversed, scaled, and zero-padded) @@ -302,11 +302,11 @@ def __call__(self, Q, T, n_threads=1, planning_flag="FFTW_ESTIMATE"): 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_of_Q = rfft_obj.output_array + rfft_Q = rfft_obj.output_array # Step 3 # Convert back to time domain by taking the inverse RFFT - np.multiply(rfft_of_T, rfft_of_Q, out=irfft_obj.input_array) + np.multiply(rfft_T, rfft_Q, out=irfft_obj.input_array) irfft_obj.execute() return irfft_obj.output_array[m - 1 : n] # valid portion