[ENH] Speed up DrCIF part 1: Catch22 exact within tolerance changes#3618
[ENH] Speed up DrCIF part 1: Catch22 exact within tolerance changes#3618TonyBagnall wants to merge 8 commits into
Conversation
Thank you for contributing to
|
_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>
_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>
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.
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.
DN_OutlierInclude_p(idx 13)DN_OutlierInclude_n(idx 14)SP_Summaries, 15/20)SC_FluctAnalrs_range/dfa (18/19)SB_TransitionMatrix(8)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.
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.
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
so 14% fastmath stays here at least, given our need for speed.