Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
224e9c9
strain: price every strain on the user's own quiet-waking level
DropTabl Oct 5, 2026
9b780d7
copy(hrv): describe the HRV headline and hrv_cv as computed
DropTabl Oct 2, 2026
646573e
fix(hrv): wire the RR over-count gate and the headline's diagnostics
DropTabl Oct 5, 2026
548a5ff
fix(hrv): stored rmssd is one estimator, the session mean, or absent
DropTabl Oct 5, 2026
837a67e
fix(hrv): days derived before the single estimator keep their HRV as …
DropTabl Oct 5, 2026
94c4394
test(resp): a sensor gap does not cost the night its respiratory rate
DropTabl Oct 2, 2026
942f806
copy(resp): describe the respiratory-rate estimator the app ships
DropTabl Oct 2, 2026
d78f636
strain: seed quiet levels on the live derive's ceiling
DropTabl Oct 8, 2026
2b1f11d
l10n: translate the nightly RMSSD labels in Investigate
DropTabl Oct 8, 2026
3a94371
strain: the quiet-level seed picks measured days first and moves the …
DropTabl Oct 8, 2026
2b74e6b
copy(hrv): write 20% without a space, matching the rest of the method…
abdulsaheel Oct 8, 2026
ccfa296
copy(hrv): describe the nightly RMSSD as the mean of 5-min sleep windows
abdulsaheel Oct 8, 2026
4bd62b9
docs(hrv): keep LocalRepository's doc on the class; reflow rmssdHisto…
abdulsaheel Oct 8, 2026
38e0378
strain: hash the newest quiet level once a later day sits on it
abdulsaheel Oct 8, 2026
0d8daf5
two-device golden: strain's provenance label for the personal quiet l…
abdulsaheel Oct 8, 2026
6ef00f8
Merge branch 'pr-552' into integrate/hrv-strain-v110
abdulsaheel Oct 9, 2026
9b3ba34
Merge branch 'pr-551' into integrate/hrv-strain-v110
abdulsaheel Oct 9, 2026
8b28d77
Merge branch 'pr-553' into integrate/hrv-strain-v110
abdulsaheel Oct 9, 2026
7fad06c
repin analytics to main @ b7d5819 (#78 #88-#91), algo 110
abdulsaheel Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
427 changes: 399 additions & 28 deletions lib/compute/derivation_engine.dart

Large diffs are not rendered by default.

46 changes: 29 additions & 17 deletions lib/compute/manual_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,33 +251,39 @@ List<double> zoneMinutesFor(
/// ceiling is a property of the strap that measured the window as well as the
/// athlete's age (`estimatedMaxHr`), and this scorer is device-agnostic. Null
/// — no age, or an uncalibrated/unstamped strap — abstains.
///
/// [quietHrr] is THIS user's quiet-waking level for the day the window sits in
/// (`personalQuietLevelBefore`) — the level that day's own strain was priced
/// on, so a bout and its day are on one baseline. Null abstains: the 0.20
/// population reference billed being awake as load for some users and erased
/// real workouts for others. [quietSettled] false only lowers confidence
/// inside analytics; the value is the same.
double? strainFromPerMinuteHr(
List<double> perMinuteHr, {
required Profile profile,
required double? restingHr,
required double? hrMax,
required double? quietHrr,
bool quietSettled = true,
}) {
final sex = profile.sex?.toLowerCase();
if (perMinuteHr.isEmpty || hrMax == null || restingHr == null || sex == null) {
if (perMinuteHr.isEmpty ||
hrMax == null ||
restingHr == null ||
sex == null ||
quietHrr == null) {
return null;
}
final trimp = ana.banisterTrimp(
// Banister TRIMP above the quiet level over the window's own minutes, with
// exercise minutes never offset by quiet ones — the same analytics call the
// day headline makes, so "workout strain" and "daily strain" are one method.
final score = ana.strainScoreFromSeries(
perMinuteHr,
restingHr: restingHr,
maxHr: hrMax,
quietHrr: quietHrr,
sex: workoutSex(sex) == 'female' ? ana.Sex.female : ana.Sex.male,
);
if (!trimp.present || trimp.value == null) return null;
// The window's own length is the baseline window: strain is the load earned
// ABOVE quiet waking, and the same sex constant has to price the baseline as
// priced the TRIMP or the subtraction is off by the male/female coefficient.
final score = ana.strainScoreMetric(
trimp.value,
wakeMinutes: perMinuteHr.length.toDouble(),
// Reference level, not this user's — see onehz_pipeline's
// `strainMetric` for why, and edge#226 for the fix.
quietHrr: ana.quietWakingHrr,
female: workoutSex(sex) == 'female',
quietSettled: quietSettled,
);
return score.present ? score.value : null;
}
Expand All @@ -289,8 +295,8 @@ double? strainFromPerMinuteHr(
/// [restingHr] is the nightly/user RHR; [profile] supplies age/weight/sex.
///
/// Uses the SAME published methods as the day-level derivation so a manual
/// session and the day it sits in are on one scale: `ana.banisterTrimp` ->
/// `ana.strainScoreMetric` for strain, `ana.Calories.estimateBoutCalories`
/// session and the day it sits in are on one scale: `ana.strainScoreFromSeries`
/// against [quietHrr] (the session day's level) for strain, `ana.Calories.estimateBoutCalories`
/// (Keytel 2005) for kcal. A missing anchor makes the dependent metric null.
///
/// [hrMax] is THE ceiling for this window — `estimatedMaxHr(age, family)`,
Expand All @@ -308,6 +314,8 @@ ManualSessionStats computeManualSessionStats({
required double? hrMax,
double? restingHr,
ana.HeartRateZoneSet? zoneSet,
double? quietHrr,
bool quietSettled = true,
}) {
if (hrTs.isEmpty || hrTs.length != hrBpm.length) {
return const ManualSessionStats();
Expand Down Expand Up @@ -344,7 +352,11 @@ ManualSessionStats computeManualSessionStats({
final sex = profile.sex?.toLowerCase();

final strain = strainFromPerMinuteHr(perMin,
profile: profile, restingHr: restingHr, hrMax: hrMax);
profile: profile,
restingHr: restingHr,
hrMax: hrMax,
quietHrr: quietHrr,
quietSettled: quietSettled);

double? calories;
if (profile.hasCalorieAnchors &&
Expand Down
Loading
Loading