diff --git a/.jules/bolt.md b/.jules/bolt.md index 38d4b732..d68f9565 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -41,3 +41,7 @@ ## 2025-02-15 - Replace Array.from(map.values()).map with a for...of loop **Learning:** Using `Array.from(map.values()).map(...)` creates an unnecessary intermediate array which wastes memory allocation and garbage collection time, particularly for frequently re-rendered components handling large collections. **Action:** Use a `for...of` loop over `map.values()` to iterate and push mapped elements directly into the final array for O(1) memory and avoiding intermediate array allocations. + +## 2026-07-08 - Vectorize SSM novelty extraction +**Learning:** Extracting checkerboard kernel responses one diagonal window at a time repeats Python slicing and summation overhead for every SSM frame. +**Action:** Sum each checkerboard offset across the full valid diagonal with `np.diagonal(...)`, and keep a loop-reference parity test so boundary scoring stays numerically stable. diff --git a/services/analysis-engine/src/bandscope_analysis/sections/segmenter.py b/services/analysis-engine/src/bandscope_analysis/sections/segmenter.py index 84817769..4b7e1470 100644 --- a/services/analysis-engine/src/bandscope_analysis/sections/segmenter.py +++ b/services/analysis-engine/src/bandscope_analysis/sections/segmenter.py @@ -95,7 +95,7 @@ def _checkerboard_novelty( The checkerboard kernel highlights transitions where the local structure changes (i.e., moving from one repeated section to a new one). - Iterates over valid diagonal patches while keeping the SSM frame count bounded. + Vectorizes diagonal patch extraction while keeping the SSM frame count bounded. """ n = ssm.shape[0] half = kernel_size // 2 @@ -109,11 +109,16 @@ def _checkerboard_novelty( kernel[:half, :half] = -1.0 kernel[half:, half:] = -1.0 - # Extract all valid diagonal patches and compute dot products. - valid_range = range(half, n - half) - for i in valid_range: - patch = ssm[i - half : i + half, i - half : i + half] - novelty[i] = np.sum(patch * kernel) + # Sum each checkerboard offset across all valid diagonal windows at once. + valid = novelty[half : n - half] + for di in range(-half, half): + for dj in range(-half, half): + value = kernel[di + half, dj + half] + diagonal = np.diagonal(ssm[half + di : n - half + di, half + dj : n - half + dj]) + if value > 0: + valid += diagonal + else: + valid -= diagonal # Normalize by peak absolute magnitude, preserving sign. max_val = np.max(np.abs(novelty)) diff --git a/services/analysis-engine/tests/test_segmenter.py b/services/analysis-engine/tests/test_segmenter.py index e20b5b33..eb79ab6f 100644 --- a/services/analysis-engine/tests/test_segmenter.py +++ b/services/analysis-engine/tests/test_segmenter.py @@ -103,6 +103,33 @@ def test_checkerboard_novelty_short_matrix_returns_zeros() -> None: assert np.array_equal(novelty, np.zeros(2, dtype=np.float64)) +def test_checkerboard_novelty_matches_loop_reference() -> None: + """Ensure diagonal vectorization preserves checkerboard novelty values.""" + rng = np.random.default_rng(42) + ssm = rng.random((48, 48), dtype=np.float64) + ssm = (ssm + ssm.T) / 2.0 + kernel_size = 8 + half = kernel_size // 2 + expected = np.zeros(ssm.shape[0], dtype=np.float64) + + kernel = np.ones((kernel_size, kernel_size), dtype=np.float64) + kernel[:half, :half] = -1.0 + kernel[half:, half:] = -1.0 + for i in range(half, ssm.shape[0] - half): + patch = ssm[i - half : i + half, i - half : i + half] + expected[i] = np.sum(patch * kernel) + + max_value = np.max(np.abs(expected)) + expected = expected / max_value + + np.testing.assert_allclose( + _checkerboard_novelty(ssm, kernel_size=kernel_size), + expected, + rtol=1e-12, + atol=1e-12, + ) + + def test_detect_boundaries_short_novelty_returns_start_only() -> None: """Ensure too-short novelty curves fail closed to a single start boundary.""" boundaries = detect_boundaries(