From f839de22775d0d0926bce789d37cecc47cfcb328 Mon Sep 17 00:00:00 2001 From: Cagatay Kenter Date: Fri, 10 Jul 2026 11:48:43 +0300 Subject: [PATCH] Fixed #1151 Forward query_idx from aamp_match to core.mass_absolute core._find_matches seeds candidate_idx = query_idx and then breaks when D[candidate_idx] > atol + max_distance, so it relies on D[query_idx] == 0. core.mass_absolute establishes that, but aamp_match never passed query_idx to it, unlike motifs.match which passes it to core.mass. query_idx was added to core.mass and core.mass_absolute in #860; #856 wired it into core.mass from motifs.match the next day, but aamp_motifs.py was not updated. core.mass_absolute's query_idx branches have been unreachable since, which is why they carry `# pragma: no cover`. They are now covered, so the pragmas are removed. For a correct self-join the fix is a no-op: cdist returns exactly 0.0 for the identical row, so D[query_idx] was already the minimum. What it restores is the "self-match is returned first" guarantee and the "Subsequences `Q` and `T[query_idx:query_idx+m]` are different" warning when query_idx does not describe Q -- neither of which could happen on the non-normalized path. --- stumpy/aamp_motifs.py | 4 +++- stumpy/core.py | 4 ++-- tests/test_aamp_motifs.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/stumpy/aamp_motifs.py b/stumpy/aamp_motifs.py index f49c11c90..6d009d128 100644 --- a/stumpy/aamp_motifs.py +++ b/stumpy/aamp_motifs.py @@ -398,7 +398,9 @@ def aamp_match( D = np.empty((d, n - m + 1)) for i in range(d): - D[i, :] = core.mass_absolute(Q[i], T[i], T_subseq_isfinite[i], p=p) + D[i, :] = core.mass_absolute( + Q[i], T[i], T_subseq_isfinite[i], p=p, query_idx=query_idx + ) D = np.mean(D, axis=0) return core._find_matches( diff --git a/stumpy/core.py b/stumpy/core.py index f5d6ebcf3..09fd80d8f 100644 --- a/stumpy/core.py +++ b/stumpy/core.py @@ -1372,7 +1372,7 @@ def mass_absolute(Q, T, T_subseq_isfinite=None, p=2.0, query_idx=None): check_window_size(m, max_size=Q.shape[0]) - if query_idx is not None: # pragma: no cover + if query_idx is not None: query_idx = int(query_idx) T_isfinite_idx = np.isfinite(T[query_idx : query_idx + m]) if not np.all(Q_isfinite == T_isfinite_idx) or not np.allclose( @@ -1407,7 +1407,7 @@ def mass_absolute(Q, T, T_subseq_isfinite=None, p=2.0, query_idx=None): if T_subseq_isfinite is None: T, T_subseq_isfinite = preprocess_non_normalized(T, m) distance_profile[:] = _mass_absolute(Q, T, p) - if query_idx is not None: # pragma: no cover + if query_idx is not None: distance_profile[query_idx] = 0.0 distance_profile[~T_subseq_isfinite] = np.inf diff --git a/tests/test_aamp_motifs.py b/tests/test_aamp_motifs.py index 17e2488f0..3f7f7dde6 100644 --- a/tests/test_aamp_motifs.py +++ b/tests/test_aamp_motifs.py @@ -239,3 +239,22 @@ def test_aamp_match_T_subseq_isfinite(Q, T): ) npt.assert_almost_equal(left, right) + + +def test_aamp_match_query_idx(): + T = np.random.uniform(-1000, 1000, [64]) + m = 8 + query_idx = 20 + Q = T[query_idx : query_idx + m] + + # `mass_absolute` zeroes the self-match distance when told where `Q` lives. + D = core.mass_absolute(Q, T, query_idx=query_idx) + npt.assert_almost_equal(D[query_idx], 0.0) + + # A `Q` that is not the subsequence at `query_idx` must still return the + # self-match first, and must warn, exactly as `stumpy.match` does. + with pytest.warns(UserWarning): + out = aamp_match(Q + 0.5, T, query_idx=query_idx, max_distance=1.0) + + assert out[0, 1] == query_idx + npt.assert_almost_equal(out[0, 0], 0.0)