Methodology

Every number Koncur shows you is computed from open, peer-reviewed research. No proprietary scoring, no opaque scaling. Here's the maths.

Updated 2 May 2026 · Load model v1

On this page
  1. Readiness
  2. Sleep score
  3. HRV
  4. RHR drift
  5. Strain debt
  6. TRIMP
  7. ACWR
  8. Mechanical kJ
  9. Polarization
  10. Strain budget
  11. PRs (Brzycki)
  12. Age percentiles
  13. Caveats & confidence
  14. Concurrent training
  15. Strength periodisation
  16. Aerobic polarised distribution

Readiness composite

A 0–100 daily score that blends four independent signals. Decision aid: low → ease back, moderate → train as planned, high → push.

readiness = round(
  0.30 · sleep_score +
  0.30 · hrv_score +
  0.20 · rhr_score +
  0.20 · strain_debt_score
)
band = readiness ≥ 75 → high
     | readiness ≥ 55 → moderate
     | else           → low

Each component is itself a 0–100 score derived below. If a component is missing (no sleep tracker, no HR strap), its weight redistributes proportionally across the components you do have — no zero-padding.

Why these weights? Sleep and HRV carry the most predictive power for next-day performance. RHR is a slower-moving signal — useful as a drift detector, less so day-to-day. Strain debt closes the loop between how hard you've trained and how recovered you are.

Sleep score component

Four sub-scores, weighted into one number.

duration    = clamp((minutes_slept / target_minutes) × 100, 0, 100)
efficiency  = (sleep_total / in_bed) × 100
consistency = 100 − (wake_time_stddev_28d / 60 × 100)
restfulness = 100 − (wake_count × 4 + awake_minutes / 2)

sleep_score = round(
  0.40 · duration +
  0.25 · efficiency +
  0.20 · consistency +
  0.15 · restfulness
)

Default sleep target is 450 minutes (7.5h) per Watson et al. 2015; configurable per-user.

Edge case: under 4 hours of sleep caps the score at 50 regardless of efficiency / consistency. Watch under-detection (in-bed > 7h but classified-asleep < 5h) is flagged separately and the sleep weight is removed from the composite.

HRV component

Morning RMSSD heart-rate variability vs your personal 28-day rolling baseline. Higher than baseline = recovered; sharp drop = stress / illness / hard recent load.

delta_pct = (today_hrv − baseline_hrv) / baseline_hrv × 100
hrv_score = clamp(70 + delta_pct × 1.5, 0, 100)

Score curve:

Compares to your baseline only, not a population norm. A "low" HRV for one athlete is normal for another. Below 14 days of data we use a population seed baseline (by age + sex) and flag confidence: low. HRV measured more than 2h after wake is discarded — not morning HRV.

Resting HR drift component + indicator

Your resting heart rate vs the 28-day median, with an interquartile-range band. Three days above the upper quartile reliably predicts illness, alcohol intake, or accumulated training stress.

baseline = median(last_28_days_rhr)
range    = [percentile(rhr, 25), percentile(rhr, 75)]

delta = today_rhr − baseline
rhr_score = clamp(70 − delta × 5, 0, 100)   // each bpm above baseline = −5

drift_band =
  consecutive_days_above_range_high == 0 → 'normal'
  consecutive_days_above < 3              → 'drifting'
  consecutive_days_above ≥ 3              → 'elevated'
Three-day rule. The IQR-baseline methodology follows Hopkins 2000's reliability framework — we treat your personal RHR distribution, not a population mean, as the reference. Internal validation against our athlete cohort shows ~70% of three-day above-IQR drifts precede a self-reported illness flag within 24–48 hours. Koncur labels this state elevated and Coach receives it as a heads-up trigger.

Apple Health takes ~6h of overnight HR data before computing today's RHR. If you check the app before that, the baseline reads against yesterday's value or null. Open the app after 9 am for the freshest read.

Strain debt component

How much of your training-stress runway you've already spent. Penalises ramping up too fast and going too long without rest.

acute       = sum(trimp_last_7d)
chronic     = avg(weekly_trimp_over_28d)
strain_load = acute / max(chronic, 1)
days_since_rest = days since the last 0-session day

debt_penalty = max(0, (strain_load − 1.0) × 50)
             + (days_since_rest > 6 ? 30 : 0)

strain_debt_score = clamp(100 − debt_penalty, 0, 100)

Below 3 sessions in the chronic window the formula is unreliable, so we return a neutral 70 with "baseline forming" rather than show noise.

TRIMP per-session

Banister's Training Impulse — exponentially-weighted training stress per session, sex-adjusted.

hr_reserve = (avg_hr − rest_hr) / (max_hr − rest_hr)

trimp_male   = duration_min × hr_reserve × 0.64 × e(1.92 × hr_reserve)
trimp_female = duration_min × hr_reserve × 0.86 × e(1.67 × hr_reserve)

Banister, E.W. (1991). Modeling elite athletic performance. In Physiological Testing of the High-Performance Athlete, Human Kinetics. Sex coefficients per Morton, Fitz-Clarke & Banister (1990).

Strength sessions are tagged trimp_method: 'banister_capped' — heavy lifts spike HR without matching cardiovascular cost, so we cap a strength session's TRIMP at 25% of its theoretical max. The mechanical kJ channel below carries the strength load instead.

ACWR 7d : 28d

Acute-to-chronic workload ratio. The single best published injury-risk predictor in endurance training. We use the EWMA variant (more responsive than the simple moving average).

acute   = ewma(trimp_daily, last 7d,  α = 2/(7+1))
chronic = ewma(trimp_daily, last 28d, α = 2/(28+1))
acwr    = acute / max(chronic, 1)

band =
  acwr < 0.8        → 'undertraining'
  0.8 ≤ acwr < 1.3  → 'sweet spot'  ← target
  1.3 ≤ acwr < 1.5  → 'high'
  acwr ≥ 1.5        → 'overreaching' ← injury risk threshold
Sweet spot 0.8–1.3 per Gabbett 2016. Athletes who held this ratio had ~10× lower injury risk than those who let it climb past 1.5.

ACWR needs ~14 days of consistent training before the chronic baseline is statistically meaningful. Below that we return the value with a baseline forming tag — Coach references this when explaining why the number reads high during ramp-up.

Mechanical kJ per-set · per-session

The differentiator vs Whoop / Strava. Whole-body external-load lifts (squat, bench, deadlift) and bodyweight gymnastic movements (TTB, K2E, sit-ups) get a real mechanical work computation, not a heart-rate proxy.

mass_kg = bw_fraction × bodyweight + external_load
work_J  = mass_kg × 9.81 × ROM × k_eccentric × reps
mKj     = round(sum(work_J) / 1000, 1)

Where:

Whole-body lifted-mass fractions from Humphrey & McGill (2007) Spinal loads during squats, plus NSCA Essentials of Strength & Conditioning 4th ed.
Partial-body segmental masses from Plagenhoef, Evans & Abdelnour 1983 + Winter Biomechanics & Motor Control of Human Movement 4th ed. (2009).
Eccentric multiplier 1.3 from Hody, Croisier, Bury, Rogister & Leprince (2019) — eccentric phase costs 1.3–1.5× concentric for matched force.

Cardiac-only movements (echo bike, rower, ski-erg, run) explicitly opt out of mechanical kJ — their cost lives in the TRIMP channel only. Mixing them would double-count.

Polarization weekly

How your weekly time-in-zone splits between easy / moderate / hard. The polarized model (~80/0/20) is the most-supported approach for endurance gains.

easy_pct      = z1_pct + z2_pct
moderate_pct  = z3_pct
hard_pct      = z4_pct + z5_pct

classification =
  easy ≥ 75 AND hard ≥ 15 AND moderate < 15  → 'polarized' ← ideal
  easy ≥ 70 AND moderate ≥ 15 AND hard < 10  → 'pyramidal'
  moderate > 30                              → 'sweet-spot-heavy' ← grey zone
  else                                       → 'unbalanced'

Polarized vs threshold rationale per Stöggl & Sperlich 2014 + Seiler 2010. Athletes who polarized improved more than those who anchored in Z3 ("threshold trap").

Strain budget daily

How much training stress today's recovery + training phase entitles you to.

phase_multipliers = {
  Base:  0.7,
  Build: 1.0,
  Peak:  0.85,
  Taper: 0.5,
}

base_budget   = recovery_score
total_budget  = round(base_budget × phase_multipliers[phase])
session_strain ≈ trimp / 4   // 100-TRIMP session ≈ 25 strain

remaining = total_budget − strain_used_today

Phase multipliers fall during Peak and Taper because the engine wants you fresher near competition, not because you've earned less. Base-phase budgets are intentionally lower than Build — building aerobic capacity well needs volume + recovery, not maximal daily output.

PRs (Brzycki) strength

Estimated 1RM from your top set in any session, capped at 10 reps (formula breaks down past that).

e1rm = weight / (1.0278 − 0.0278 × reps)

Aggregated per canonical movement (e.g. "Barbell Back Squat", "Back Squat", "Paused Squat" all roll into back_squat) so you see one PR per lift, not one per name variant.

Brzycki, M. (1993). Strength testing: predicting a one-rep max from reps to fatigue. JOPERD 64(1).

Age percentiles cohort lookup

Where you sit relative to people the same age and sex as you, on three independent measures: VO₂max, resting HR, and overnight HRV. We surface the percentile band ("78th percentile · M 30-39") rather than fusing the three into a single "fitness age" — the underlying datasets aren't precise enough for that, and the three measures answer different questions.

cohort       = age_band × sex                // 6 bands × 2 sex = 12 cohorts
anchors      = [p10, p30, p50, p70, p90]     // from peer-reviewed cohort data
percentile   = linear_interpolate(value, anchors)   // continuous 5-95
band =
  percentile ≥ 90 → 'elite'
  percentile ≥ 70 → 'excellent'
  percentile ≥ 50 → 'good'
  percentile ≥ 30 → 'fair'
  else            → 'low'

Source for each metric:

What we deliberately don't do. No "fitness age" composite. Each percentile is single-metric and self-explanatory. We label the cohort right next to the number ("78th percentile · M 30-39") so the comparison is never floating. Out-of-table values clamp to 5 / 95.

Caveats: All three datasets are western, mostly self-selected healthy cohorts. The VO₂max table is verified line-by-line against the published source. The RHR and HRV tables are approximated from the published per-decade values within ±3 bpm and ±5 ms respectively — exact transcription pending. We aim for honest precision: if your cohort percentile feels off by a couple of points, that's the published precision floor, not a bug.

Caveats & confidence

Every metric exposes a confidence flag based on the data behind it.

confidence =
  days_of_data < 14  → 'low'
  < 28               → 'medium'
  ≥ 28               → 'high'

(adjusted down 1 level per >3 missing days in the window)

While confidence is low:

What's not in the formula. We deliberately don't include: caffeine, alcohol, hydration self-report, mood (other than the optional wellness check-in), menstrual phase as a multiplier (it's surfaced as context for the user, not baked into the score). These are signals the user uses to interpret a low number — not inputs we can measure objectively enough to weight.

Concurrent training training mode

Concurrent training — pursuing strength and endurance adaptations within the same training cycle — is the Koncur default mode and the harder problem. Done badly, the two adaptations interfere; done well, they're synergistic.

The interference effect (Hickson 1980; Wilson et al. 2012; Coffey & Hawley 2017) describes how high-volume endurance training blunts strength and power gains, especially when stacked in close proximity to lower-body lifting. The effect is dose-dependent and modality-dependent: running interferes with squat development more than cycling, because of the eccentric impact load on the same motor units.

Rather than treat this as a soft warning, the engine encodes it as hard rules:

// Hard rules per concurrent week template

48h gap between heavy lower-body sessions
  · respects motor-unit recovery for the same muscle pool

PM aerobic on double-session days = Zone 2 only
  · prevents high-intensity cardio after AM lower lifting
  · ≥6h gap between sessions

Polarised aerobic split (~80% Zone 2 in base phase)
  · keeps total intensity load below interference threshold

Long aerobic session grows weekly within each meso
  · deload at week 6 cuts volume 50%, intensity 60%

The PM-aerobic-after-strength rule is the most-violated rule in real-world hybrid programming. The engine refuses to schedule it, full stop — even if the user requests double days, the PM block is forced to easy aerobic.

What concurrent isn't. It isn't "two programmes glued together." If you take a marathon plan and bolt on a powerlifting plan, you'll get the worst of both. Concurrent done well is a single integrated week structure where strength and aerobic recovery windows respect each other.

Strength periodisation strength focus

The Strength Focus path drops the concurrent safeguards (no aerobic block to interfere with) and runs a strength-native template — higher per-lift frequency, more accessory volume, and block periodisation across the meso.

Per-muscle weekly volume drives hypertrophy and strength scaling. Schoenfeld et al. 2019 meta-analysis sets the minimum effective dose at ~10 sets per muscle per week, with diminishing returns above ~20. Frequency matters too — 2 sessions/week beats 1 for any given total volume (Schoenfeld 2016), with a smaller gain from 3+/week.

// Strength Focus week structure

Each main lift programmed 2–3× per week
  · squat 2–3× · bench 2× · deadlift 1–2× · OHP 2×
  · per-muscle weekly volume hits 10–20 sets MED

4–5 accessories per session (vs 2–3 concurrent)
  · adds the volume Schoenfeld 2019 calls for
  · same movement-pattern de-duplication rules

Block periodisation across the meso:
  weeks 1–2  hypertrophy bias (8–12 reps, RPE 7)
  weeks 3–4  strength bias    (4–6 reps,  RPE 8)
  weeks 5    peak             (2–3 reps,  RPE 9)
  week 6     deload           (volume −50%, intensity 60%)

1RM re-test at meso end (Brzycki estimate from week 5 working sets)

Optional maintenance cardio (1–2× Zone 2 / week) is supported as an opt-in. Murach & Bagley 2017 confirms that low-volume Zone 2 (≤2× per week, ≤45 min) does not measurably blunt hypertrophy or strength gains, so users can keep a cardiovascular floor without compromising the primary outcome.

Aerobic polarised distribution aerobic focus

The Aerobic Focus path uses a polarised intensity distribution — a structure consistently shown to outperform threshold-heavy training in elite endurance athletes (Seiler 2010; Stöggl & Sperlich 2014; Foster et al. 2017).

"Polarised" means roughly 80% easy / 20% hard, with deliberately little time spent in the moderate-intensity grey zone (the "sweet spot"). The grey zone produces fatigue without delivering proportional adaptation; polarising the distribution preserves quality on hard days and recovery on easy days.

// Weekly intensity distribution (validated as a weekly invariant)

~80% time in Zone 2          easy aerobic — most days
~15% time in Zone 4–5        quality day — tempo / intervals
~5%  Zone 3 grey-zone tax    minimised by design

// Aerobic Focus week structure (5 days example)

dow 1   easy aerobic (Zone 2)
dow 2   QUALITY (tempo or intervals — phase-dependent)
dow 4   easy aerobic (Zone 2)
dow 5   easy aerobic (Zone 2)
dow 6   long run/ride (Zone 2, grows weekly)

// Race-specific peak + taper (Mujika 2010)

Peak phase  shifts quality to VO₂max repeats (4×4 / 30-30s)
Taper       volume drops 40–60% in final 1–2 weeks
            intensity preserved, frequency preserved
            recovers freshness without losing fitness

Optional maintenance strength (1–2× full-body / week) is an opt-in for endurance athletes. Rønnestad & Mujika 2014 show that 2× heavy strength sessions per week improves running economy and time-to-exhaustion in trained endurance athletes — heavy compound work (≤6 reps), short sessions (≤45 min), inserted on rest or easy-aerobic days with ≥48h before any quality session.

Why these three modes? Concurrent rules don't apply when there's no aerobic block (Strength Focus); polarised distribution doesn't apply when there's no aerobic block either (Strength Focus); aerobic-native templates don't apply when strength is the primary outcome (Aerobic Focus). One template with feature toggles produces a programme that's right for none of them. Three distinct generation paths produce a programme that's right for each.

References

Found something off? hello@koncur.app. Source available with the methodology document at docs/load-model.md in the repo.