Skip to content

Performance: 15x cheaper night recalculation with streams and monoidic accumulators - #99

Open
mikenrafter wants to merge 2 commits into
OpenStrap:mainfrom
mikenrafter:pr/analytics-incremental
Open

mikenrafter wants to merge 2 commits into
OpenStrap:mainfrom
mikenrafter:pr/analytics-incremental

Conversation

@mikenrafter

@mikenrafter mikenrafter commented Oct 23, 2026 •

Copy link
Copy Markdown

Edge recomputes the whole night on every sync, so each 15-minute sync costs more than the one before. On a desktop AOT build, one night of syncs spends about 28.8 s on RR correction, 22.1 s on hrvFreq, 15.4 s on RSA, 8.7 s on CVHR and 34.5 s on staging, mostly redoing data it has already seen. The day repeats the same pattern.

This PR makes those calculations cheaper to run again. Exact speedups make one pass cheaper, and streams and accumulators let a sync pay only for its own new chunk. Across a night of 15-minute syncs the calculations drop from 109.5 s to 7.2 s (15.3x); across a day, from 148.3 s to 0.27 s once Edge uses the incremental paths. Numbers and method are under "Benchmarks".

What a user sees. Existing metric outputs change. Edge must bump kAlgoVersion when it takes this version, so devices recalculate stored results. The full list with sizes is in "Output changes" below.

What changes (on top of main 458ba36):

  • Exact speedups. Batch RR correction, CVHR, sliding medians, and cardio epoch features give the same output as main, only faster.
  • Exact streams. RR correction, hrvFreq, RSA, and CVHR can take a night in chunks and give the same answer as one batch call over the same data (hrvFreq: see "Known differences").
  • Aggregator framework. A small interface (empty, addSlice, combine, finish) with merge-law tests, JSON checkpoints, and reusable parts. The public batch functions now run their aggregators for hrvTime, banisterTrimp, ctlAtlTsb, cosinor, HeartRateZones.timeInZone, and circadianNonparametric, as do SRI and hypnogram metrics. PRSA, phase-rectified signal averaging, and readiness lnRMSSD already matched their aggregators bit for bit. Readiness shares its suffix helper. Chunked results use the tolerances below.
  • Incremental minute and energy APIs. IncrementalHrvTime, IncrementalLombScargle, IncrementalMinuteMetrics, CalculationCache, Calories.minuteEnergy, Calories.hourlyRollup, hrDipFromDayTotals.
  • The group registry. A declared list of calculation groups, each with its files, dependencies, and a version counter. A test checks it against the real import graph.
  • The causal stage API. CausalStager.observe: a past-only version of the cardio stager, for decisions that can't wait for the night to end.
  • The RR artifact-run cap and shared epoch rows. correctRr(maxArtifactRun: 32), RrSandwich (chunked corrected NN into any aggregator), and EpochRows / EpochRowsMemo. The default stager now builds its epoch rows with EpochRows, anchored at the detected sleep onset, so its labels equal the pre-change labels.
  • Smaller additions. stageMinuteRanges, irregularBeatScreenDetailed, and non-finite-time guards (see below).

Accumulator (monoid) design

Each accumulator is a state plus three operations. addSlice(state, rows, start, end) folds a run of rows into the state. combine(left, right) merges two adjacent chunks, left first. finish(state) turns the state into the result without changing it. Order matters (no commutativity), so a state can carry the few rows at its edges that a neighbouring chunk needs. A calculation that looks at neighbours (successive differences, lag pairs) wraps its inner step in Lookback(k), which hands the seam rows to the inner step exactly once under any chunking. Per-day or per-bin work uses GroupBy. Persistable states round-trip through versioned JSON checkpoints. A bad or old checkpoint is refused and the caller recomputes from raw data.

I used an interface smaller than a textbook monoid because a monoid asks for things that cost speed or honesty here: one immutable value combined per element (180,000 µs against 141 µs for a hand loop, over a day of samples), structural equality of states, and one context-free operation. Several calculations need context known before data arrives (session bounds, a bin origin). docs/AGGREGATOR_DESIGN.md has the argument.

checkLaws (lib/src/aggregate/laws.dart) tests every converted calculation against these laws, all compared after finish:

  1. left and right identity
  2. split: any two adjacent chunks combined equal one fold
  3. associativity on three adjacent chunks
  4. one-row chunks combined one at a time
  5. one state fed random consecutive slices
  6. finish twice gives the same result and the state keeps working
  7. checkpoint round trip (persistable ones)

The harness is itself tested against deliberately broken aggregators, so a pass means something. expectConversion also checks parity with the old function on whole, chunked, right-nested, sliced, and singleton evaluation.

The streams (RR corrector, hrvFreq, RSA, CVHR, EpochRows) are fold-and-snapshot objects rather than Aggregators. Their chunk-invariance is tested in welch_stream_laws_test.dart, rr_irregular_laws_test.dart, and incremental_laws_test.dart.

I adopted the toleranced aggregators in the public batch functions in this PR. A batch call runs the whole input as one slice. Tests require that result to match the one-slice aggregator bit for bit.


Benchmarks

Total speedup by phase. Real night and real day, with the synthetic night and synthetic day in brackets. The whole-night, whole-day and batch day-of-syncs rows were timed as one call per pass. The other rows add up the medians of the calculations benchmarked per sync. For the night: RR correction, HRV time domain, hrvFreq, RSA, CVHR and staging (cardioStager). For the day, the recalculation Edge repeats through the day: RR correction with the irregular-rhythm screen, HR dip, TRIMP, strain score and curve, daily energy and time in zones. Main's column is main's batch functions. This PR's incremental rows use the streams, accumulators and the staging row cache, which Edge reaches once it adopts them (the Edge PRs).

Phase Main This PR Speedup
A whole night in one pass (import, recalculation, or a full night synced at once; all seven batch calculations) 8,334.5 ms 6,374.6 ms 1.3x (1.3x)
A night of 15-minute syncs, all syncs added together 109,493 ms 7,162 ms 15.3x (14.2x)
One sync at the end of the night 6,420.9 ms 182.2 ms 35.2x (25.1x)
A whole day in one pass (all daily calls) 2,955.5 ms 94.4 ms 31.3x (35.0x)
A day of 15-minute syncs, batch functions only (96 syncs) 148,283 ms 4,190 ms 35.4x (36.3x)
A day of 15-minute syncs, incremental paths 148,283 ms 267 ms about 555x (about 1,000x)
One sync at the end of the day, incremental paths 2,933.4 ms 2.75 ms about 1,070x (about 2,270x)

The per-calculation tables follow.

Method. I compared upstream main 458ba36 with the PR branch on the same idle machine: Linux 7.0.13 aarch64, 8 cores, 7 GB RAM, Dart 3.11.4. I compiled ahead of time, AOT, with dart compile exe. I ran one benchmark process at a time, after warmup, and report the median of 5 or 15 runs. The run count varies by case. Each case waited for a 1-minute load below 1.5 with no test processes. Input was one night of my own 1 Hz heart rate, accelerometer and RR data. I reran the PR branch on the final code and reused main's measurements from the first clean run. The real night is the main table below; the synthetic night differs most on HRV time domain. Raw numbers, spreads and the synthetic results are available on request. hrvFreq differs from main only in the last digits, from the Lomb-Scargle origin change.

The daily cases use one full local day of my data (2026-10-06, America/Denver: 85,908 one-Hz rows, 43,289 RR beats) synced every 15 minutes from midnight (96 syncs). They cover the calls in Edge's per-day derive whose input grows through the day. The day takes the night's stored timestamps as a fixed sleep window rather than Edge's sleep detection, and uses fixed benchmark profile constants (75 kg, 175 cm, age 35, resting HR 60, max HR 183.5), not my profile. That real day was quiet: strain and active energy are zero, so those paths are exercised only by the synthetic day, which has a 17:00-18:00 exercise hour. Cross-day calculations (CTL/ATL/TSB, cosinor, circadian indices, SRI) need several days of data and are not in these cases. Every daily result agrees with main except TRIMP's last digits (real day 95.96351535268741 against 95.96351535268737; synthetic day 294.5430326586097 against 294.5430326586095), the adopted-aggregator tolerance above.

Wall clock, real night (ms, median).

Main This PR
RR correction, one batch 1,615.0 33.20 48.6x faster
CVHR, one batch 506.6 22.70 22.3x faster
HRV time domain, one batch 3.52 1.41 2.5x faster
hrvFreq, one batch 1,400.8 1,434.6 2 % slower
RSA, one batch 873.8 909.1 4 % slower
cardioStager, one batch 1,930.2 1,950.0 1 % slower
AdvancedSleepStager.stageWindow, one batch 1,969.9 1,983.9 1 % slower
All of the above, one pass 8,334.5 6,374.6 1.3x faster
RR correction, a night of 15-min syncs 28,779.1 55.49 (stream) about 519x
hrvFreq, a night of syncs 22,108.3 1,639.4 (stream) 13.5x
RSA, a night of syncs 15,357.6 1,058.8 (stream) 14.5x
CVHR, a night of syncs 8,702.3 532.2 (stream) 16.4x
HRV time domain, a night of syncs 60.32 7.28 (stream) 8.3x
Staging, a night of syncs 34,485.5 3,869.0 (row cache) 8.9x
Staging, one more chunk after a full night 1,940.8 100.5 (row cache) 19.3x

On the synthetic night, batch RR correction is 49.4x faster and CVHR is 20.5x faster. HRV time domain takes 19.05 ms against main's 17.79 ms, 7 % slower, because that night needs the jitter evidence. Its 5.89 ms IQR also shows more timing variation than the real night's 0.004 ms IQR. Across a night of syncs, incremental HRV time domain takes 295.4 ms against main's 264.7 ms, 12 % slower. The other synthetic batch cases are within 5 %, and the all-in-one pass is 1.3x faster.

Wall clock, real day (ms, median).

Main This PR
RR correction, one batch 2,888.3 60.01 48.1x faster
Irregular-rhythm screen, one batch (prepared NN) 6.89 10.79 1.6x slower
RR correction + irregular screen, one batch 2,919.7 73.80 39.6x faster
HR dip, one batch 3.92 3.91 same
Banister TRIMP, one batch 0.005 0.016 3.2x slower
Strain score / strain curve, one batch 0.018 / 0.015 0.018 / 0.012 about the same
Daily energy, one batch 0.001 0.004 4x slower
Time in HR zones, one batch 0.064 0.101 1.6x slower
All daily calls, one pass 2,955.5 94.42 31.3x faster
RR correction, a day of syncs 149,202.3 3,244.5 (batch); 125.7 (stream) 46.0x; about 1,190x
RR correction + irregular screen, a day of syncs 145,404.0 3,900.1 (batch); 134.0 (stream) 37.3x; about 1,085x
Irregular screen, a day of syncs 407.1 632.0 (batch); 5.38 (IrregularScreenState) 1.6x slower; 76x
HR dip, a day of syncs 257.9 131.6 (hrDipFromDayTotals) 2.0x
TRIMP + daily energy, a day of syncs 0.234 0.224 (IncrementalMinuteMetrics) same
Time in HR zones, a day of syncs 2.49 0.066 (aggregator kept across syncs) 38x
All daily calls, a day of syncs 148,283.3 4,190.4 (batch) 35.4x
All daily calls, one sync at the end of the day 2,933.4 85.08 (batch); 2.75 (incremental) 34.5x; about 1,070x

RR correction dominates the day: it is about 98 % of main's whole-day pass (2,888 of 2,956 ms). The small daily calls are slower in batch on this branch, because TRIMP, zones and daily energy now run their aggregators or minute bills. Each stays under 0.11 ms per call, and they are cheaper again once kept across syncs. The irregular screen batch is 1.6x slower (10.8 ms against 6.9 ms) because of the added diagnostics and timestamp-gap handling; its incremental state costs 0.05 ms per sync.

On the synthetic day (an exercise hour, so strain and active energy are non-zero), batch RR correction is 44.9x faster (7,977 against 178 ms), the whole-day pass 35.0x and a day of syncs 36.3x with batch functions (364.2 s against 10.0 s). The small calls show the same pattern as the real day: the irregular screen batch is 1.6x slower, TRIMP, energy and zones are slower by microseconds. Strain, strain curve and daily energy agree with main exactly.

Memory (RSS, MB; range across the eight whole-night batch cases, real night). RSS is a per-process high-water mark, not a per-calculation allocation, so read it as a sanity check.

Main This PR
Process after loading the night (real / synthetic) 57 / 51 40 / 26
Peak 66 to 85 73 to 82
Steady 56 to 77 62 to 75
Peak, streaming and row-cache paths (ours only) 65 to 94
Day: process after loading the day 103 104
Day: peak, whole-day batch cases 101 to 248 115 to 253
Day: steady, whole-day batch cases 103 to 159 118 to 176
Day: peak, incremental paths (ours only) 118 to 227

The peak is about the same. The batch RR correction reads 73 against 77 MB and the all-in-one pass 85 against 82 MB. On the day, the all-in-one pass peaks at 221 against 234 MB.

Database size. Analytics has no database, so there is nothing to measure here. The size comparison (main against this work, same export, same derived days) is in the Edge PRs.


Tests

Most of this PR is tests. Exact paths are checked against frozen copies of main's code (test/onehz/support/, including the pre-change stager). Converted calculations have parity and merge-law tests. Adoption tests also require the public batch result to match the one-slice aggregator bit for bit. Streams are checked at every chunking the law harness generates.

Local checks. dart analyze reports no issues on both commits.

  • Full suite on this branch: dart test on Linux (aarch64): 2,618 passed, 7 skipped, 0 failed.

The seven skips are six plausibility checks that skip without whoop_hist.jsonl, upstream's own capture kept beside the repo, and one DREAMT grid comparison because the corpus is absent.

I have run this on my own device: Pixel 9a on Android/GrapheneOS. This package has no device code. Edge consuming it is tested in the Edge PRs.

Test notes.

  • The only existing test file changed is test/onehz/advanced_stager_test.dart, with added lines only. No test is deleted or weakened.
  • The staging agreement guard is 0.97 and the frozen-night agreement is 1.0. The still-night efficiency guard (> 0.85) passes.
  • Registry test: re-derives each group's file list from the import graph and compares it with the declaration.
  • Not covered: the 18 days of real data cited in the CHANGELOG for the RR cap are not in the repo, so reviewers can't rerun that claim. The July 2026 overnight capture that the incremental tests read is checked in, and missing files fail those tests rather than skipping them.

Suggested review order

  1. Exact speedups and their frozen references: util.dart (slidingMedians), rr_correction.dart, hrv_freq.dart, cvhr_apnea.dart, cardio_stager.dart, and phase0_parity_test.dart.
  2. The Lomb-Scargle origin change (util.dart, lomb_scargle_test.dart). Check its output changes against "Output changes" below.
  3. The aggregator core and its law harness (lib/src/aggregate/), then the building blocks.
  4. The exact streams: hrvFreq, RSA, CVHR, then the RR corrector.
  5. The converted modules, smallest first: TRIMP, CTL/ATL/TSB, SRI, hypnogram metrics, then time in zone, circadian indices, cosinor, PRSA, readiness, and HRV time with its jitter evidence. Check the public batch adoption tests too.
  6. The RR cap, RrSandwich, EpochRows, and how advanced_stager.dart uses them. docs/COMBINED_BUMP_STATUS.md explains the design and the onset-anchored grid.
  7. Incremental minute and energy APIs, the group registry, the causal stager, stageMinuteRanges, and the irregular diagnostics.

Per-calculation details and accuracy

Per calculation. Chunk times are on the real night, or the real day where marked, 15-minute chunks, median per chunk, with the maximum in brackets. Raw numbers are available on request. "Main's batch, last sync" is main's batch function over the full-night prefix, which is what main pays at that sync. Blank means not benchmarked here; those are one pass over the chunk's rows by construction, but I have not measured them.

Calculation Accumulator? Exact or toleranced Streaming cost per chunk Main's batch, last sync
RR correction (RrCorrector) Stream, checkpointed (v2) Exact: equals the capped batch, including one-beat folds and JSON restarts 1.74 ms (1.96); day: 1.31 ms (1.97) 1,686 ms; day: 2,952 ms
RrSandwich (corrected NN into any aggregator) combine re-corrects only the seam Exact against correctRr(maxArtifactRun: K) not benchmarked
hrvFreq (HrvFreqStream) Stream Exact against our batch (bit-identical); see "Known differences" for main 66.3 ms (88.9) 1,402 ms
RSA (RsaRespStream) Stream, checkpointed (v2) Exact 32.5 ms (51.8) 877 ms
CVHR (CvhrStream) Stream Exact 17.2 ms (19.8) 511 ms
Sleep staging (EpochRows + CardioRowCache) Stream; checkpoint v4 Exact: labels equal the frozen pre-change stager 67.8 ms (82.9) rows only; 112.8 ms (166.2) with the cache 1,941 ms
HRV time domain, incremental (IncrementalHrvTime) Stream Toleranced, 1e-8 relative (this repo's test helper) 0.224 ms (0.369) 3.5 ms
HRV time domain (hrvTimeAggregator) Aggregator, persistable Toleranced, 1e-9; the jitter gate is rebuilt from mergeable evidence 30,000 beats in 1.2 ms (24 slices + combine) against 5.0 ms for hrvTime (AGGREGATOR_DESIGN.md, AOT)
Hypnogram metrics Aggregator, in place Exact
Phillips SRI Aggregator, in place Exact
Readiness lnRMSSD Aggregator (LastRows); shared suffix helper, bit-identical Exact
Banister TRIMP Aggregator Toleranced, 1e-12 day: under 0.001 ms day: 0.005 ms
Cosinor Aggregator Toleranced, 1e-9 (R² 1e-7)
Time in zone Aggregator, exact gap histograms Toleranced, 1e-12 day: 0.001 ms (0.002) day: 0.058 ms
CTL/ATL/TSB Aggregator (EWMA as an affine map) Toleranced, 1e-9
PRSA DC/AC Aggregator; batch already bit-identical to one slice Chunk merges toleranced, 1e-9
Circadian IS/IV/M10/L5/RA Aggregator Toleranced, 1e-12 on integer data, 1e-9 on real data
Irregular-rhythm screen (IrregularScreenState) Stream, checkpoint v3 Verdict identical; running moments differ from batch near 1e-13 day: 0.053 ms (0.121) day: 9.47 ms
HR dip (hrDipFromDayTotals) Running day totals (count, sum) Exact on the benchmark day day: 1.30 ms (3.20) day: 3.92 ms
Daily energy (Calories.minuteEnergy + hourlyRollup) Per-minute bills, hourly totals Daily total matches dailyEnergy within 1e-8 on the benchmark days day: 0.017 ms (0.049), including the hourly rollup day: 0.001 ms
IncrementalLombScargle, minute TRIMP/energy Incremental state Toleranced, 1e-8 (30,000 appends stay within 1e-9) day, minute TRIMP + energy: 0.002 ms (0.013) day: 0.006 ms (TRIMP + energy)

Folding a whole night in chunks costs more in total than one batch call. For hrvFreq it is 1,639 ms against 1,401 ms, and for RSA 1,059 ms against 874 ms. Each sync then pays only its own chunk. For a one-shot caller, use the batch function.

Output changes

Change Which outputs How much
Lomb-Scargle measures time from the first sample hrvFreq, cardiacCoherence, RSA and respiration peak rate, the cardio stager's LF/HF About 1e-14 relative on the checked night's hrvFreq bands. The old error reached about 5e-6 relative per frequency. Threshold decisions can change.
Adopted HRV aggregator hrvTime Last digits; tested tolerance 1e-9 absolute plus 1e-9 relative.
Adopted training-load aggregators banisterTrimp, ctlAtlTsb TRIMP: 1e-12 relative. CTL/ATL/TSB: 1e-9 absolute and relative.
Adopted zone aggregator HeartRateZones.timeInZone Zone seconds: 1e-9 absolute / 1e-12 relative.
Adopted circadian aggregator circadianNonparametric 1e-12 relative on integer fixtures, 1e-9 on real fixtures. Near-ties can select different L5/M10 start epochs.
Adopted cosinor aggregator cosinor Mesor, the fitted mean, and amplitude: 1e-9 relative. Near-zero amplitude has a floor of 1e-12 * max(1, abs(mesor)); near-zero mesor has a 1e-12 data-scale floor. Circular phase error: 1e-9 * max(1, abs(phaseRad)) radians or 1e-9 * periodHours hours. Phase is not compared at rounding-noise amplitudes. R², adjusted R² and confidence: 1e-7 absolute.
Near-singular cosinor fit cosinor Now abstains with a null value when phase coverage is insufficient. The relative determinant check replaces the old absolute pivot check.
RR artifact-run cap of 32 beats correctRr and downstream metrics Longer runs become gaps. Runs of 32 or fewer keep the previous output exactly.
Non-finite beat time hrvFreq, RSA, the hrvTime family, CVHR Abstain with a note. Before, hrvFreq and CVHR could loop forever. Finite input is unchanged by these guards.
Timestamp gaps in the irregular-rhythm stream IrregularScreenState Pairs across a gap no longer count as successive beats. Statistics, window votes and verdicts can change. Old checkpoints are refused.

These tolerances describe checked parity, not universal bounds. Sleep labels are unchanged by the epoch-row refactor. The separately listed RR and spectral changes can affect downstream staging features. SRI, hypnogram metrics, PRSA and readiness retain their outputs. Calories.dailyEnergy golden values are pinned with ==.

Registry counters found.core, hrv.freq, night.rhythm and activity.load go to 2. found.rr_correction and sleep.stage are already 2. Dependent groups update through their declared dependencies.

Every new or changed method cites its paper next to the code and has an ALGORITHMS.md row. These include Task Force 1996 for HRV, Morton, Fitz-Clarke & Banister 1990 for TRIMP, Halberg & Nelson 1979 for cosinor, and Bauer et al. 2006 for PRSA. Internal rules are marked ESTIMATE with low confidence: the RR artifact-run cap, RMSSD jitter rejection and exemptions, fitted staging weights and calibration, the causal staging adaptation, stage-minute margins and bounds, irregular-rhythm thresholds and sustained-window voting, time-in-zone cadence/interval/tail credit, and HR-dip bands and minimum sample counts. Their component citations do not validate those internal rules.

Known differences

  • hrvFreq, last digits. On the benchmark night, main and this branch differ in the last few digits of LF and HF (about 1e-14 relative). LF/HF agreed on one night and differed in the last digit on the other (real: 0.9406551454559028 against 0.9406551454559098). The cause is the Lomb-Scargle origin change above: the sums are formed from t - t.first rather than raw seconds, so the floating-point rounding differs. The adopted aggregators can also change last digits within the tolerances above.
  • RR correction latency. The capped corrector keeps 4H + 2K + 4 beats provisional (248 at the defaults), above the roughly 124 I was aiming for.
  • RR sandwich reach. The reach (4H + 3K + 4 = 280) is the corrector's proven settle horizon plus one K. It is not a proof for every adversarial artifact pattern, and the tests don't fail until the reach is cut to about 2H. I have not benchmarked RrSandwich.
  • Cold one-shot corrector. Folding a whole day in one call and taking a snapshot took 210 ms against 175 ms for the batch, JIT (tool/bench_night_nn.dart). The benefit is on repeated passes.

Still batch, and why

  • Float medians and quantiles. slidingMedians is exact (it matches the old per-position median), but it is still a pass over the window array on every call. IntHistogram gives exact, mergeable order statistics, but only for whole-number series (heart rate). It has no caller yet, and docs/INCREMENTAL_USAGE.md lists which batch callers could take it. Epoch HR is a mean, not a whole number, so those gates can't use it without changing output.
  • nocturnalRhr. It takes the first percentile of the night's valid HR and a lowest-window mean on every call. It could use IntHistogram; I did not make that change.
  • Viterbi. It exists only in the StagingMethod.v2 HMM, which is not the default and is kept for regression coverage. The default cardio stager doesn't use it.
  • ENMO with the automatic gravity reference. The reference is the median of the whole record and can change earlier minutes. Passing an explicit measured reference uses the incremental path.
  • Sleep staging, in a single pass. Production calls (AdvancedSleepStager, cardioStager) build their epoch rows per call. EpochRowsMemo exists and is tested, but nothing in this package calls it, and I have not wired it into a production caller. A caller gets cheaper repeat staging by passing a CardioRowCache. In a single pass staging costs what it did on main (1,950 ms against 1,930 ms for cardioStager on the real night). The row cache then makes a repeat staging cost 93 ms warm, and one more chunk after a full night 101 ms.

Change distribution breakdown

I kept two commits on main 458ba36:

  1. c813cf7 contains the code and tests.
  2. c32007f adds citations, CHANGELOG.md, documentation and benchmark tools.

The final contents depend on each other across themes. Finer commits would not build on their own. The review order above guides the large code commit.

180 files, +53,764 / -1,395.

Files Added Deleted
lib/ 59 14,507 1,377
test/ 90 31,588 0
Docs, tools and build files 31 7,669 18

Most added lines are tests. Frozen copies of main's code check the exact paths. The converted modules and streams have merge-law, parity, checkpoint and non-finite-input coverage. flake.nix pins a Flutter 3.41.6 / Dart 3.11.4 development shell.


Known issues kept as they are on main

This PR does not fix these:

  • StagingMethod.v1 and v2 are not recommended for production and keep their old behavior, kept for regression coverage.
  • Cardio staging still scores kappa 0.13 against PSG on the held-out DREAMT split (docs/CAUSAL_STAGER.md). The causal stager is not more accurate than it.
  • readinessLnRmssd's public function does not call readinessLnRmssdAggregator. It shares the suffix function with it, as on main.

Not covered on the test host. Real devices. This package is pure Dart with no runtime dependencies.


Questions for reviewers

1. Are you comfortable taking the output changes together with the speedups in one PR? I kept them together because the correction, aggregators, staging rows and registry depend on each other. Edge needs one kAlgoVersion bump for this version.
2. Should EpochRowsMemo and IntHistogram stay in this PR with no caller? Both are tested and meant for Edge to adopt. The alternative is to land them with the change that uses them.

… and the sleep-staging epoch rows

What this does:
- Exact speedups for sliding medians, RR correction, hrvFreq, CVHR and the cardio
  stager, each checked against a frozen copy of the old code.
- An aggregator interface (empty, addSlice, combine, finish) with JSON checkpoints
  and property tests for the merge laws. HRV time, TRIMP, CTL/ATL/TSB, cosinor,
  circadian, time in zone, SRI, hypnogram metrics, PRSA and readiness lnRMSSD are
  converted; their public batch functions now return the aggregator's result.
- Exact streams for the RR corrector, hrvFreq, RSA and CVHR, so a sync folds only
  its new chunk.
- EpochRows accumulate sleep-staging features; staging anchors its epochs at the
  session start, with labels equal to the previous stager.
- Incremental minute/energy APIs, a group registry with per-group counters, the
  causal stager, stageMinuteRanges and irregular-rhythm diagnostics.

Output changes:
- Lomb-Scargle measures times from the first sample: hrvFreq LF/HF move in the
  last digits (relative ~1e-14).
- The adopted aggregators move results in the last digits only, within each one's
  tested tolerance (1e-12 to 1e-9 relative). A near-singular cosinor design now
  abstains (null value).
- RR correction caps artifact runs at 32 beats; a longer run becomes a gap. Runs of
  32 or fewer give the previous output exactly.
- Sleep labels do not change.
Edge's kAlgoVersion must be bumped when it takes this version.
…lculations

Citations next to each new or changed method and a row per method in ALGORITHMS.md;
internal rules are marked ESTIMATE. The changelog for this series, and the benchmark
and validation tools under tool/.

Output change: none.

@sourcery-ai sourcery-ai Bot 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.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 20000 lines

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 178 files, which is 78 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 9301bdd4-3621-4b9e-aefe-a4f2dd1f30cc
📥 Commits

Reviewing files that changed from the base of the PR and between 458ba36 and c32007f.

⛔ Files ignored due to path filters: 2
  • flake.lock is excluded by !**/*.lock
  • test/onehz/fixtures/real_night_2026_07_hypnogram_v1.csv is excluded by !**/*.csv
📒 Files selected for processing: 178
  • ALGORITHMS.md
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • docs/AGGREGATOR_DESIGN.md
  • docs/CAUSAL_STAGER.md
  • docs/COMBINED_BUMP_STATUS.md
  • docs/INCREMENTAL_INTEGRATION_PLAN.md
  • docs/INCREMENTAL_MATH.md
  • docs/INCREMENTAL_USAGE.md
  • flake.nix
  • lib/aggregate.dart
  • lib/onehz.dart
  • lib/src/aggregate/aggregator.dart
  • lib/src/aggregate/checkpoint.dart
  • lib/src/aggregate/json_support.dart
  • lib/src/aggregate/laws.dart
  • lib/src/aggregate/primitives.dart
  • lib/src/onehz/clinical/cosinor.dart
  • lib/src/onehz/clinical/cosinor_aggregator.dart
  • lib/src/onehz/clinical/hrv_freq.dart
  • lib/src/onehz/clinical/hrv_freq_stream.dart
  • lib/src/onehz/clinical/hrv_jitter_evidence.dart
  • lib/src/onehz/clinical/hrv_time.dart
  • lib/src/onehz/clinical/hrv_time_aggregator.dart
  • lib/src/onehz/clinical/irregular_diagnostics.dart
  • lib/src/onehz/clinical/irregular_rhythm.dart
  • lib/src/onehz/clinical/irregular_rhythm_state.dart
  • lib/src/onehz/clinical/irregular_window.dart
  • lib/src/onehz/clinical/load_trimp.dart
  • lib/src/onehz/clinical/load_trimp_aggregator.dart
  • lib/src/onehz/clinical/nocturnal.dart
  • lib/src/onehz/clinical/prsa_aggregator.dart
  • lib/src/onehz/clinical/readiness_lnrmssd.dart
  • lib/src/onehz/clinical/readiness_lnrmssd_aggregator.dart
  • lib/src/onehz/clinical/rmssd_gate.dart
  • lib/src/onehz/foundations/rr_correction.dart
  • lib/src/onehz/foundations/rr_correction_kernel.dart
  • lib/src/onehz/foundations/rr_correction_stream.dart
  • lib/src/onehz/foundations/rr_sandwich.dart
  • lib/src/onehz/incremental.dart
  • lib/src/onehz/incremental_core.dart
  • lib/src/onehz/incremental_enmo.dart
  • lib/src/onehz/incremental_hrv.dart
  • lib/src/onehz/incremental_minutes.dart
  • lib/src/onehz/incremental_state.dart
  • lib/src/onehz/registry/group_registry.dart
  • lib/src/onehz/respiration/cvhr_apnea.dart
  • lib/src/onehz/respiration/cvhr_stream.dart
  • lib/src/onehz/respiration/resp_rate.dart
  • lib/src/onehz/respiration/respiration.dart
  • lib/src/onehz/sleep/advanced_stager.dart
  • lib/src/onehz/sleep/cardio_row_cache.dart
  • lib/src/onehz/sleep/cardio_stager.dart
  • lib/src/onehz/sleep/causal_stager.dart
  • lib/src/onehz/sleep/circadian_np.dart
  • lib/src/onehz/sleep/circadian_np_aggregator.dart
  • lib/src/onehz/sleep/circadian_window.dart
  • lib/src/onehz/sleep/epoch_rows.dart
  • lib/src/onehz/sleep/hypnogram.dart
  • lib/src/onehz/sleep/hypnogram_aggregator.dart
  • lib/src/onehz/sleep/segment.dart
  • lib/src/onehz/sleep/sleep.dart
  • lib/src/onehz/sleep/sri.dart
  • lib/src/onehz/sleep/sri_aggregator.dart
  • lib/src/onehz/sleep/stage_ranges.dart
  • lib/src/onehz/util.dart
  • lib/src/onehz/workout/calories.dart
  • lib/src/onehz/workout/hr_zones.dart
  • lib/src/onehz/workout/time_in_zone_aggregator.dart
  • test/aggregate/adopted_aggregators_test.dart
  • test/aggregate/aggregator_core_test.dart
  • test/aggregate/circadian_np_aggregator_test.dart
  • test/aggregate/conversion_check_test.dart
  • test/aggregate/cosinor_aggregator_test.dart
  • test/aggregate/ctl_atl_aggregator_test.dart
  • test/aggregate/ewma_prefix_test.dart
  • test/aggregate/group_by_test.dart
  • test/aggregate/hrv_time_aggregator_test.dart
  • test/aggregate/hypnogram_aggregator_test.dart
  • test/aggregate/last_rows_test.dart
  • test/aggregate/laws_harness_test.dart
  • test/aggregate/lookback_test.dart
  • test/aggregate/persistence_test.dart
  • test/aggregate/primitives_test.dart
  • test/aggregate/prsa_aggregator_test.dart
  • test/aggregate/readiness_lnrmssd_aggregator_test.dart
  • test/aggregate/rr_sandwich_test.dart
  • test/aggregate/sri_aggregator_test.dart
  • test/aggregate/sum_of_test.dart
  • test/aggregate/time_in_zone_aggregator_test.dart
  • test/aggregate/trimp_aggregator_test.dart
  • test/algo_version_test.dart
  • test/incremental/day_curves_test.dart
  • test/incremental/hrv_boundary_test.dart
  • test/incremental/hrv_resp_nn_test.dart
  • test/incremental/hrv_windows_test.dart
  • test/incremental/rr_stream_test.dart
  • test/incremental/support.dart
  • test/incremental/thin_input_test.dart
  • test/onehz/advanced_stager_test.dart
  • test/onehz/cardio_epoch_features_test.dart
  • test/onehz/cardio_row_cache_test.dart
  • test/onehz/causal_stager_real_night_test.dart
  • test/onehz/causal_stager_test.dart
  • test/onehz/cvhr_nan_test.dart
  • test/onehz/cvhr_stream_test.dart
  • test/onehz/epoch_rows_anchor_test.dart
  • test/onehz/epoch_rows_test.dart
  • test/onehz/incremental/calculation_cache_test.dart
  • test/onehz/incremental/checkpoint_ownership_test.dart
  • test/onehz/incremental/drift_test.dart
  • test/onehz/incremental/enmo_series_test.dart
  • test/onehz/incremental/hrv_time_test.dart
  • test/onehz/incremental/int_histogram_test.dart
  • test/onehz/incremental/irregular_diagnostics_stream_test.dart
  • test/onehz/incremental/irregular_screen_state_test.dart
  • test/onehz/incremental/lomb_scargle_test.dart
  • test/onehz/incremental/minute_metrics_test.dart
  • test/onehz/incremental/minute_summary_test.dart
  • test/onehz/incremental/rr_corrector_test.dart
  • test/onehz/incremental/running_moments_test.dart
  • test/onehz/irregular_diagnostics_empty_test.dart
  • test/onehz/irregular_diagnostics_test.dart
  • test/onehz/lomb_scargle_precision_test.dart
  • test/onehz/minute_energy_test.dart
  • test/onehz/nonfinite_beat_times_test.dart
  • test/onehz/phase0_parity_test.dart
  • test/onehz/rr_artifact_cap_test.dart
  • test/onehz/rr_correction_oracle_test.dart
  • test/onehz/rr_correction_perf_test.dart
  • test/onehz/sliding_median_test.dart
  • test/onehz/stage_minute_ranges_test.dart
  • test/onehz/support/causal_night.dart
  • test/onehz/support/correct_rr_reference.dart
  • test/onehz/support/energy_day.dart
  • test/onehz/support/epoch_input.dart
  • test/onehz/support/incremental_compare.dart
  • test/onehz/support/incremental_fixtures.dart
  • test/onehz/support/pre_bump_stager.dart
  • test/onehz/support/rr_chunking.dart
  • test/onehz/support/rr_compare.dart
  • test/onehz/support/rr_runs.dart
  • test/onehz/support/rr_stream_driver.dart
  • test/onehz/support/rr_synth.dart
  • test/onehz/wallclock_agreement_test.dart
  • test/onehz/welch_stream_test.dart
  • test/properties/incremental_laws_test.dart
  • test/properties/rr_irregular_laws_test.dart
  • test/properties/stage_minute_ranges_laws_test.dart
  • test/properties/welch_stream_laws_test.dart
  • test/registry_closure_test.dart
  • test/support/aggregator_laws.dart
  • test/support/checkpoint_support.dart
  • test/support/fold_law_support.dart
  • test/support/hrv_beats.dart
  • test/support/law_registry.dart
  • test/support/property.dart
  • test/support/property_test.dart
  • tool/bench_aggregate.dart
  • tool/bench_night_bins.dart
  • tool/bench_night_nn.dart
  • tool/bench_phase0.dart
  • tool/causal_stager_validate.dart
  • tool/combined_bump_probe.dart
  • tool/incremental/bench_all.dart
  • tool/incremental/bench_rr.dart
  • tool/incremental/driver.dart
  • tool/incremental/edge_oracles.dart
  • tool/incremental/err_probe.dart
  • tool/incremental/hrv_incr.dart
  • tool/incremental/lag_stats.dart
  • tool/incremental/oracle_util.dart
  • tool/incremental/resp_incr.dart
  • tool/incremental/rr_stream.dart
  • tool/incremental/size_probe.dart
  • tool/incremental/synth.dart
  • tool/incremental_benchmark.dart

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mikenrafter mikenrafter changed the title Analytics: exact speedups, mergeable (accumulator) calculations, an RR artifact-run cap, and staging on shared epoch rows Performance: Stream/snapshot and monoidic accumulator calculations (major speedup) Oct 23, 2026
@mikenrafter

Copy link
Copy Markdown
Author

It seems I left nix tooling in this branch. Let me know if you'd rather it be removed. (or just do it yourself on the branch)

mikenrafter added a commit to mikenrafter/edge that referenced this pull request Oct 24, 2026
…ion 116

The pin moves 83fc308 -> 6a2a9fc on the same fork branch: the code in
OpenStrap/analytics#99. Outputs move, so kAlgoVersion goes 115 -> 116:
the RR artifact-run cap of 32 beats (longer runs become gaps), the toleranced
batch aggregators (hrvTime, TRIMP, CTL/ATL/TSB, cosinor, time in zone,
circadian; last digits), the near-singular cosinor abstention, non-finite
guards and irregular-screen gap handling. Staging labels are unchanged by the
shared epoch rows. Checkpoint validation accepts the capped RR corrector.

WHOOP freeze goldens regenerated with owner approval: only the 115 -> 116
stamps and TRIMP/SDNN last digits move (max 2.7e-15 relative); evidence in
edge.research/perf/whoop-golden-116-diff.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikenrafter mikenrafter changed the title Performance: Stream/snapshot and monoidic accumulator calculations (major speedup) Performance: 15x cheaper night recalculation with streams and monoidic accumulators Oct 24, 2026
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.

1 participant