Skip to content

Vectorized fast paths: trailing stack, moving-reference stretching, filter-once ensembles - #33

Closed
mdenolle wants to merge 2 commits into
feat/run-pipeline-return-ccfrom
perf/vectorized-fast-paths
Closed

Vectorized fast paths: trailing stack, moving-reference stretching, filter-once ensembles#33
mdenolle wants to merge 2 commits into
feat/run-pipeline-return-ccfrom
perf/vectorized-fast-paths

Conversation

@mdenolle

@mdenolle mdenolle commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Implements the three performance optimizations verified bit-exact by prototypes during the 2026-08-02 audit (max dv/v difference ~1e-15 vs the current code on the 1095-day volcano synthetic).

Stacked on #32 (feat/run-pipeline-return-cc), because the moving-reference fast path must preserve the collect_cc behavior added there. When #32 merges, this PR retargets to master automatically.

Changes

  1. _trailing_stack via cumulative sums (synthetic_demo.py). Difference of float64 cumsums: O(ndays x nlag) independent of stack length, replacing the O(ndays x k x nlag) per-day loop. Measured 2.2x at k=45 (roughly at parity below k~15, where the loop was already cheap).

  2. Vectorized moving-reference stretching (measure_stretching_trailing, dispatched from deviations._moving_reference for "stretching (TS)"). The stretched sample positions t/(1+eps) are data-independent, so the linear-interpolation gather indices/weights are computed once per epsilon and applied to every day's band-passed trailing reference at once; trailing references are built by cumsum and the band-pass runs once over the whole matrix (2 FFTs total instead of 2 per day). Measured 4.7x on the 3-year volcano synthetic (12.2 -> 2.6 s), max |dv/v| difference 1.4e-15. Other estimators keep the generic per-day loop; collect_cc from Add return_cc option to run_pipeline #32 is preserved (regression-tested).

  3. Filter-once ensembles: run_pipeline(..., prefiltered=True). Band-passing is linear, so it commutes exactly with linear stacking; callers evaluating several stack/reference variants at the same band can band-pass the raw CCF matrix once and the estimators skip their internal band-pass. Only valid at an identical band and only for estimators whose band usage is that one linear filter (stretching, WCC, DTW, MWCS); the wavelet estimators raise ValueError. Measured 1.7x on a 5-member same-band ensemble on top of the other fast paths.

Combined effect on a 5-member ensemble: ~4x per pair-band.

Tests

  • TestFastPathRegressions in tests/test_deviations.py: each fast path vs the replaced per-day loop (replicated inline), and prefiltered=True vs the internal band-pass for fixed / moving / inversion references and MWCS — all asserted at rtol=0, atol=1e-12 (observed ~1e-15), plus the ValueError for wavelet estimators.
  • Full tests/test_deviations.py, tests/test_golden.py, plus test_synthetic_demo, test_use_cases, test_uq_bayes, test_frugalmind_export, test_private_golden, test_bench: 93 passed (Python 3.13 env; the repo pixi env has a pre-3.10 interpreter that cannot import the package).
  • pre-commit: ruff, ruff-format, black, hygiene hooks pass. The mypy hook fails identically on unmodified files (the pinned mypy 1.7.0 cannot parse current numpy stubs under python_version = "3.10"); a current mypy run on the changed files shows no new errors vs the branch baseline.

CHANGELOG updated under Unreleased.

🤖 Generated with Claude Code

…-once ensembles

Three performance fast paths, each verified to reproduce the per-day loop it
replaces to ~1e-15 in dv/v (regression tests at rtol=0, atol=1e-12):

- _trailing_stack: difference of float64 cumulative sums, O(ndays*nlag)
  independent of the stack length instead of O(ndays*k*nlag) (~2.3x at k=45).
- measure_stretching_trailing: vectorized stretching against a trailing
  reference. The stretched sample positions t/(1+eps) are data-independent,
  so the linear-interpolation gather indices/weights are computed once per
  epsilon and applied to all days at once; trailing references come from a
  cumulative sum and the band-pass runs once over the whole matrix.
  deviations._moving_reference dispatches to it for "stretching (TS)"
  (measured 4.7x on the 3-year volcano synthetic, 12.2 -> 2.6 s), keeping
  the generic per-day loop for other estimators. collect_cc behavior from
  the return_cc branch is preserved.
- run_pipeline(..., prefiltered=True): callers evaluating several
  stack/reference variants at the same band can band-pass the raw CCF matrix
  once; the estimators skip their internal band-pass. Exact because the
  band-pass is linear and commutes with linear stacking; restricted to the
  estimators whose band usage is that one linear filter (stretching, WCC,
  DTW, MWCS), ValueError otherwise.

Combined, a 5-member same-band ensemble drops ~4x per pair-band.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 05:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds several vectorized “fast paths” to speed up dv/v processing while preserving bit-level agreement (to floating-point rounding) with the existing per-day loop implementations. It also introduces an optional prefiltered=True mode so ensembles at the same band can apply the band-pass once and skip redundant internal filtering.

Changes:

  • Reworked _trailing_stack to use float64 cumulative sums for an O(ndays × nlag) trailing mean.
  • Added a vectorized moving-reference stretching implementation (measure_stretching_trailing) and dispatched to it from deviations._moving_reference for "stretching (TS)".
  • Added run_pipeline(..., prefiltered=True) plus estimator-level support to skip internal band-pass where valid, and updated tests/changelog accordingly.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/test_deviations.py Adds regression tests for the new fast paths and prefiltered=True behavior.
src/codameter/synthetic_demo.py Implements vectorized trailing stretching and adds prefiltered plumbing to supported estimators.
src/codameter/deviations.py Routes moving-reference stretching to the vectorized path; adds prefiltered support + validation in run_pipeline.
CHANGELOG.md Documents the new fast paths and the prefiltered option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_deviations.py Outdated
)
np.testing.assert_allclose(dvv_b, dvv_a, rtol=0, atol=1e-12)
np.testing.assert_array_equal(val_b, val_a)
np.testing.assert_allclose(cc_b, cc_a, rtol=0, atol=1e-12)
Copilot flagged assert_allclose(cc_b, cc_a, ...) as failing on the
all-NaN cc arrays (inversion/mwcs cases, where no CC is collected)
because equal_nan defaults to False. That's true of plain np.allclose,
but numpy.testing.assert_allclose already defaults equal_nan=True --
verified all 4 parametrized cases (fixed/moving/inversion/mwcs) were
already passing. Not a real bug, but making the default explicit (with
a comment on why) so the next reader doesn't hit the same false alarm.
@mdenolle

mdenolle commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #34 — this PR's base branch (feat/run-pipeline-return-cc) was deleted when #32 squash-merged, before GitHub's auto-retarget-to-master could kick in, so it got closed with no way to reopen (base ref gone). Same branch, same commits (plus the Copilot-review fixes), re-opened directly against master as #34.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants