diff --git a/ALGORITHMS.md b/ALGORITHMS.md index 14db222..d487503 100644 --- a/ALGORITHMS.md +++ b/ALGORITHMS.md @@ -46,7 +46,6 @@ Grouped by family (subdirectory under `lib/src/onehz/`). File paths are relative |---|---|---| | `correctRr` | `foundations/rr_correction.dart` | Lipponen & Tarvainen 2019 RR artifact correction (dRR/mRR/sRR beat classification, Kubios-style) | | `Baselines` (Winsorized-EWMA) | `foundations/ewma_baselines.dart` | Winsorized exponentially-weighted moving baseline — the rolling personal reference most other metrics compare against | -| PPG signal-quality index | `foundations/ppg_sqi.dart` | Skewness-based SQI | | inverse-variance fusion | `foundations/fusion.dart` | Standard inverse-variance weighting for combining multiple noisy estimates of the same quantity | ### `clinical/` — Tier-1 cardiac/autonomic metrics @@ -104,7 +103,6 @@ Grouped by family (subdirectory under `lib/src/onehz/`). File paths are relative ### `workout/` | Function | File | Method | Citation | |---|---|---|---| -| `detectWorkouts` | `workout/workout_detect.dart` | explicit workout detection + zones | — | | `autoDetectWorkouts` | `workout/auto_detect.dart` | automatic workout detection | — | | `hrRecovery` | `workout/hr_recovery.dart` | HRR — HR drop N seconds post-peak | Cole/Lauer 1999-style HRR | | `Calories.dailyEnergy` / `estimateBoutCalories` | `workout/calories.dart` | Keytel HR→kcal regression + Harris-Benedict/Mifflin BMR | Keytel et al. 2005 | diff --git a/README.md b/README.md index afd6d50..f9be7a6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Eight families, each its own subdirectory with its own sub-barrel, built on two foundation layers: - **`foundations/`** — Lipponen-Tarvainen RR artifact correction, Winsorized-EWMA rolling - baselines, inverse-variance fusion, a PPG signal-quality index. + baselines, inverse-variance fusion. - **`clinical/`** (Tier-1) — HRV time/frequency domain (RMSSD/SDNN/pNNx, Lomb-Scargle LF/HF), PRSA (deceleration/acceleration capacity), nocturnal RHR/dip, an illness-risk CUSUM state machine, Plews ln-RMSSD readiness, Baevsky stress index, Banister/Edwards @@ -86,8 +86,8 @@ foundation layers: - **`motion/`** — ENMO/MAD activity metrics, a hybrid live/1 Hz step estimator (AN-2554 100 Hz pedometer preferred, a gated-and-bout-length-checked 1 Hz fallback for whatever the live stream missed), energy-expenditure fusion. -- **`workout/`** — workout detection (both explicit and automatic), heart-rate-reserve - zones, Keytel/Harris-Benedict calorie estimation. +- **`workout/`** — automatic workout detection (bout suggestion, never explicit/ + retroactive), heart-rate-reserve zones, Keytel/Harris-Benedict calorie estimation. - **`wellness/`** — the canonical composite readiness score, multivariate (Mahalanobis) anomaly detection, CUSUM changepoint detection, temperature-based illness flagging. - **`human/`** — sleep regularity index, social jetlag/chronotype, single-night event @@ -110,7 +110,8 @@ not a feature, no matter how tempting the plausible-looking headline is. dart test # run from the repo root — some fixtures resolve paths relative to it ``` -290 tests, nothing mocked — pure functions, fixture in, assertion out. +624 passed / 6 skipped (630 total), nothing mocked — pure functions, fixture in, +assertion out. ## If you want to add a metric diff --git a/lib/src/onehz/workout/observed_max_hr.dart b/lib/src/onehz/workout/observed_max_hr.dart index 79a08e8..7e208a6 100644 --- a/lib/src/onehz/workout/observed_max_hr.dart +++ b/lib/src/onehz/workout/observed_max_hr.dart @@ -143,10 +143,16 @@ Metric sessionHrCeiling( final holdMs = holdSeconds * 1000.0; final gapMs = maxGapSeconds * 1000.0; + // A real held effort's corroborating motion is not necessarily even across + // the hold (e.g. a couple of seconds of arm swing at each end of a quiet + // middle), so a start that fails the motion gate on the MINIMAL qualifying + // window still gets to extend further before giving up — capped, so one + // quiet start can't turn this into an O(n²) scan of a whole day. + final maxSpanMs = holdMs * 4; HrCeiling? best; - // ponytail: O(n · holdSeconds) — one session at 1 Hz, so ~15 passes over a - // few thousand samples. A monotonic-deque sliding minimum if it ever runs - // over a whole day. + // ponytail: O(n · holdSeconds) — one session at 1 Hz, so a bounded number of + // passes over a few thousand samples. A monotonic-deque sliding minimum if + // it ever runs over a whole day. for (var i = 0; i < rows.length; i++) { var lo = rows[i].hr; var motionSum = 0.0; @@ -158,19 +164,22 @@ Metric sessionHrCeiling( count++; final span = rows[j].ts - rows[i].ts; if (span < holdMs) continue; - // The window qualifies on duration. `lo` is the bpm sustained across all - // of it; extending further can only lower it, so this is the best this - // start can do and we stop. + if (span > maxSpanMs) break; // gave this start its fair shot + // The window qualifies on duration. `lo` is the bpm sustained across + // all of it. Only stop once the motion actually corroborates — that's + // the earliest point extending further can only lower `lo` for no gain. final motion = motionSum / count; - if (motion >= gate && (best == null || lo > best.bpm)) { - best = HrCeiling( - bpm: lo, - tsMs: rows[i].ts, - heldSeconds: (span / 1000).round(), - motionG: motion, - ); + if (motion >= gate) { + if (best == null || lo > best.bpm) { + best = HrCeiling( + bpm: lo, + tsMs: rows[i].ts, + heldSeconds: (span / 1000).round(), + motionG: motion, + ); + } + break; } - break; } }