Skip to content

[ENH] Speed up DrCIF part 1: Catch22 exact within tolerance changes#3618

Open
TonyBagnall wants to merge 8 commits into
mainfrom
ajb/drcif
Open

[ENH] Speed up DrCIF part 1: Catch22 exact within tolerance changes#3618
TonyBagnall wants to merge 8 commits into
mainfrom
ajb/drcif

Conversation

@TonyBagnall

@TonyBagnall TonyBagnall commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

part of #3617 and #3616

this adds speed ups that do not change the output at all. This is done with claude code under my guidance. This PR is improvements to catch22, since it dominates the run time for DrCIF. Aiming for 2x speed up. All the changes are bitwise identical except the last, which could potentially give differences <1e-13 difference, but on tests doesnt.

image

catch 22 features

note all changes verified identical to the previous. If reviewer requires can provide more info.

Validation: full-transform output is bit-for-bit identical to a reference saved before the change across 12 configs (univariate/multivariate, several lengths); the predict-time skip-mask path matches; the catch22 and interval-forest test suites pass; the DrCIF doctest predictions are unchanged.

feature share
DN_OutlierInclude_p (idx 13) ~21%
DN_OutlierInclude_n (idx 14) ~21%
welch/FFT (SP_Summaries, 15/20) ~9%
SC_FluctAnal rs_range/dfa (18/19) ~10%
SB_TransitionMatrix (8) ~6%
autocorrelation kernel (shared) ~5%
everything else ~26%

The two outlier-timing features alone are ~43% of catch22 compute: These are all on outlier_include

1. _outlier_include

Speed up catch22 _outlier_include without changing output

Replace the per-threshold rescan (O(num_thresholds*n) with per-iteration allocations) with a single-pass approach: bucket each position by its
highest qualifying threshold, then sweep thresholds while maintaining
count/min/max incrementally and reading the median from a Fenwick tree.
Bit-for-bit identical to the previous implementation; ~8.7x faster.

More detail available if required, but this is very clever, would have taken some deep investigation to find this.

2. welch-feature FFT

Batch catch22 welch-feature FFT across cases without changing output

The two SP_Summaries welch features need np.fft.fft of the mean-centred
series; _transform_case computed one small FFT per case, so numpy's fixed
per-call overhead dominated. Precompute the FFT for the whole batch in a
single np.fft.fft(..., axis=1) call and pass it into _transform_case.

Bit-for-bit identical: a batched FFT row equals the per-case 1D FFT, and
the subtracted mean still uses the same numba mean() (np.mean differs by
ULPs under fastmath). Falls back to per-case for unequal-length input and
honours the predict-time attribute-skip mask.

at this point benchmark arrowhead 133s -> 84s.

3. _sb_coarsegrain — remove redundant sorts

_sb_coarsegrain computes num_groups + 1 quantile thresholds by calling _quantile, and each _quantile call sorted the input array from scratch — so the same array was sorted 4 times per call. It now sorts once and reads every threshold from the sorted copy via a new _quantile_sorted helper (_quantile keeps its old signature by sorting then delegating). Results are bit-for-bit identical. This speeds the two features that use coarse-graining: entropy_pairs (SB_MotifThree) by ~21% and transition_matrix (SB_TransitionMatrix), reducing the full 22-feature transform from ~55 ms to ~50 ms (211×128).

4. Feature 12 whiten_timescale — compile the wrapper

_FC_LocalSimple_mean1_tauresrat was a plain-Python @staticmethod that made three separate numba calls (_local_simple_mean, _compute_autocorrelations, _ac_first_zero), marshalling the intermediate arrays back to Python between each. It now carries @njit like its sibling feature functions, keeping the whole computation inside numba. Output is bit-for-bit identical (verified over 6.4k fuzz cases). This is a minor consistency/cleanup change — its
runtime is dominated by the autocorrelation FFT, so the measurable speedup is negligible.

5 — bespoke least squares in _fluct_prop (rs_range + dfa)

This was my spot not claude, based on forecasting work, so I'm taking credit although we may ditch these features in part 2. Replaced the per-window _linear_regression (which re-summed the fixed grid d=[1..tau] every window) with closed-form grid moments computed once per tau, and for DFA computed the fluctuation as SSR-from-sums (Σy² − c1·Σxy − c2·Σy) instead of materialising and detrending the window.

  • Correctness: bit-identical — 0.0 diff across 4,200 fuzz cases (including ties and near-perfect-linear inputs where SSR≈0 and the negative-clamp matters), plus the full-transform harness. The closed-form moments are exact integers, and the SSR-from-sums rounds identically here — so this one turned out exact, not just within tolerance.
  • Speed: ~1.6× on rs_range + dfa (10.3→6.6 ms @128, 31.8→19.4 @512, 59.8→37.3 @1024). Those two are ~30% of Catch22, so ~1.1–1.15× on the full transform.

6: twiddle precompute — the FFT twiddle table in _compute_autocorrelations is rebuilt on every call but depends only on the (constant-within-a-transform) series length.

Cant claim this, all claude. Basically features 2, 3, 8, 10 and 12 all need the autocorrelation, whose FFT twiddle factors depend only on the (common) series length. Build them once here and reuse across every case instead of rebuilding per autocorrelation call.

this is more benefit for full feature transform, but worth doing.

6 — twiddle precompute (shared autocorrelation)

Refactored _compute_autocorrelations into reusable pieces (_ac_nfft, _ac_twiddles, _autocorrelations_with_tw) and precompute the FFT twiddle table once per transform (via a new _ac_twiddle_cache, threaded
through like the welch cache), reusing it across all cases for the shared ac used by features 2/3/8/10/12. The public _compute_autocorrelations(X) stays a thin wrapper, so anything else (incl. feature 12's own
autocorrelation) is unchanged.

  • Correctness: bit-identical — 0.0 diff on the full-transform harness and multivariate; the twiddles are deterministic np.exp, so this is exact (a "common function" reuse, not a numerical change).
  • Speed: ~1.12–1.17× on the acf-cluster features in isolation (the twiddle loop was ~17% of each autocorrelation, now computed once instead of per case).

Looking at fastmath. When True it causes problems with infinities. We found on #3474 that we can probably trade to contract with no loss of runtime and safer infinities. First test with catch22

image image

so 14% fastmath stays here at least, given our need for speed.

@aeon-actions-bot aeon-actions-bot Bot added enhancement New feature, improvement request or other non-bug code enhancement transformations Transformations package labels Jul 8, 2026
@aeon-actions-bot

Copy link
Copy Markdown
Contributor

Thank you for contributing to aeon

I have added the following labels to this PR based on the title: [ enhancement ].
I have added the following labels to this PR based on the changes made: [ transformations ]. Feel free to change these if they do not properly represent the PR.

The Checks tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our pre-commit code quality check fails, please run pre-commit locally and push the fixes to your PR branch.

Don't hesitate to ask questions on the aeon Discord channel if you have any.

PR CI actions

These checkboxes will add labels to enable or disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

  • Run pre-commit checks for all files
  • Run mypy typecheck tests
  • Run all pytest tests and configurations
  • Run all notebook example tests
  • Run numba-disabled codecov tests
  • Disable numba cache loading
  • Regenerate expected results for testing
  • Push an empty commit to re-run CI checks

@TonyBagnall TonyBagnall added classification Classification package regression Regression package transformations Transformations package and removed enhancement New feature, improvement request or other non-bug code enhancement transformations Transformations package labels Jul 8, 2026
@TonyBagnall TonyBagnall changed the title [ENH] Speed up DrCIF part 1 [ENH] Speed up DrCIF part 1: Catch22 Jul 8, 2026
@TonyBagnall TonyBagnall marked this pull request as ready for review July 8, 2026 12:41
@TonyBagnall TonyBagnall changed the title [ENH] Speed up DrCIF part 1: Catch22 [ENH] Speed up DrCIF part 1: Catch22 bitwise exact Jul 8, 2026
_SB_MotifThree_quantile_hh (entropy_pairs): replace the r1/r2 per-symbol
position-list machinery and terminal-position trim with a direct 3x3
adjacent-transition count. The trim is just excluding the last position as
a transition source, which range(n-1) handles, so this is a straight
simplification that drops ~7 temporary length-n arrays. ~1.5x faster in
isolation.

_sb_coarsegrain: collapse the num_groups labelling passes into a single
pass with an early break, since the group ranges are disjoint.

Output is bit-for-bit identical: max abs diff 0.0 vs a reference set and a
5760-case fuzz of _SB_MotifThree_quantile_hh against the original r1/r2
implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TonyBagnall and others added 4 commits July 8, 2026 20:41
_fluct_prop: bespoke least squares over the fixed grid d=[1..tau] - the
grid moments are closed-form and computed once per tau, and the DFA
fluctuation is a residual-sum-of-squares from sums instead of detrending
each window. ~1.6x on rs_range/dfa.

_compute_autocorrelations: factor out the FFT twiddle table (depends only
on the series length) and precompute it once per transform via
_ac_twiddle_cache, reused for the shared autocorrelation features
(2, 3, 8, 10, 12) instead of rebuilding it per call. ~1.15x on the acf
cluster.

Output is bit-for-bit identical to the previous implementation (verified
against the original over fixed-seed panels incl. multivariate and edge
cases, plus 4200 direct _fluct_prop fuzz cases); catch22 tests pass.
Together ~1.2x on the full 22-feature transform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_FC_LocalSimple_mean1_tauresrat computes the autocorrelation of the
differenced series; pass it the transform's precomputed twiddle table
(added for the other autocorrelation features) so it reuses them instead
of rebuilding, falling back to computing its own when the differenced
series lands on a different FFT size (length 2**k + 1). Bit-for-bit
identical; ~1.1x on whiten_timescale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TonyBagnall TonyBagnall changed the title [ENH] Speed up DrCIF part 1: Catch22 bitwise exact [ENH] Speed up DrCIF part 1: Catch22 exact within tolerance changes Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

classification Classification package regression Regression package transformations Transformations package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant