Skip to content

v0.7.1 -- Updated formulas

Latest

Choose a tag to compare

@bvpotapenko bvpotapenko released this 15 Jun 05:24

A very big update, plenty of issues were fixed in this release, looking forward to better planning, fitting sportsmen of different level.

Removed

  • refresh_plan() removed — this function reset plan_start_date to today, permanently
    erasing prescription history for all sessions between the old and new anchor. There was no
    recovery path. The timeline showed a gap; planned-but-unlogged sessions became unrecoverable.
    The use case ("I want to skip ahead") doesn't require this: the plan already shows missed
    sessions as-is, and the user can simply train the next upcoming session. set_plan_start_date
    is retained for test fixtures and internal tooling.

Fixed

  • 1RM estimation now uses a rep-range-aware formula throughout the planner — the
    TM-derived fallback and history-based estimate in _estimate_effective_leff_1rm() both now
    call best_1rm_from_leff() (Brzycki+Lander blend ≤5 reps, +Epley ≤10, Lombardi+Epley
    above). Previously, both paths applied a plain Epley formula capped at 12 reps. At TM > 12
    (intermediate to advanced bodyweight athletes), the cap froze the estimated 1RM, producing
    identical weight prescriptions regardless of actual strength — e.g., the same +6 kg for
    pull-ups whether your TM was 12 or 25.
  • TEST session target now shows beat-last-result intent — the prescribed target reps for
    a TEST session is now round(TM / TM_FACTOR) + 1, which corresponds to one rep above the
    athlete's last test result. Previously the target was TM (90% of last test max), which
    caused many athletes to stop at the displayed number rather than achieving a true max effort,
    systematically suppressing TM over successive cycles.
  • 4-day schedule: S→H gap increased from 24h to 48h — day offsets changed from
    [0, 1, 3, 5] (Mon/Tue/Thu/Sat) to [0, 2, 4, 5] (Mon/Wed/Fri/Sat). The 24-hour gap
    between Strength and Hypertrophy sessions did not provide sufficient recovery for heavy
    pulling or pushing. The DAY_SPACING["S"] = 1 constant in exercises.yaml was not
    enforced by schedule_builder.py (it is only read by test_session_inserter.py); the fix
    corrects the hardcoded day offsets directly.
  • Technique (T) session intensity corrected — T sessions were prescribed at 20–40% of TM
    with RIR target 4–5, which for any athlete above TM=10 produced sets at RIR 10–15 (no
    stimulus). Rep fractions updated to 0.45–0.65 (pull-up / dip) and 0.55–0.75 (BSS), RIR
    target lowered to 2 across all four exercises. Sets reduced by 1 per level compared to H
    sessions (T is quality work, not volume work).
  • Endurance (E) session volume formula removed — the kE(TM) × TM formula targeted 84–150
    total reps for TM 20–30, but sets_max=10 and reps_max=8 caps made the target structurally
    unreachable: maximum achievable volume was ~45 reps regardless of TM. Every E session above
    TM=12 was identical (10 sets, all capped). The broken formula has been replaced with
    sets_by_level (same mechanism as S/H/T sessions): novice→advanced maps to
    [6,7,8,10] (pull-up), [5,6,7,8] (dip), [3,4,5,5] (BSS), [3,4,4,5] (incline DB
    press). Volume now grows as the athlete advances, with the descending rep ladder preserved.
    endurance_volume_multiplier() removed from config.py.
  • Autoregulation gates loweredMIN_SESSIONS_FOR_AUTOREG reduced from 10 to 3 (was
    gating on 6–10 weeks for 3-day practitioners, leaving new users on a fully open-loop plan
    too long). Readiness z-score thresholds narrowed: READINESS_Z_LOW from −1.0 to −0.5 and
    READINESS_Z_HIGH from 1.0 to 0.5, so moderate fatigue and moderate freshness both now
    influence prescription rather than only extremes.

[0.7.0] - 2026-04-03

Added

  • Level-based adaptive set countsExerciseDefinition gains level_thresholds: list[int]
    (e.g. [4, 13, 24] for pull-up, based on Strength Level database) and each S/H/T session
    block gains sets_by_level: list[int] (indexed 0=novice→3=advanced). The planner classifies
    the user's level from their latest test max and prescribes the corresponding set count
    directly. Replaces the fixed midpoint (sets_min + sets_max) // 2 which always produced
    the same value regardless of user progress.
  • Intra-session rep decayExerciseDefinition gains set_fatigue_curve: list[float]
    (e.g. [1.0, 0.85, 0.75, 0.68, 0.63]). Each set's target_reps is multiplied by the
    curve factor for its position, modelling empirical ~15–30% rep drop-off per set at moderate
    rest (PMC11057609). Set 1 receives full reps; subsequent sets descend. E sessions (volume
    ladder) and TEST sessions are unaffected.
  • _classify_level(latest_test_max, level_thresholds) -> int in set_prescriptor.py
    returns 0-indexed level (0=lowest) using the first threshold the test max is ≤.
    Falls back to middle level when either argument is None.
  • All 4 exercise YAMLs updated with level_thresholds and set_fatigue_curve. S/H/T blocks
    now use sets_by_level and omit sets_min/sets_max (now optional, defaulting to 1/10).

Fixed

  • Epley 1RM over-prescription for high-rep users_estimate_effective_leff_1rm and
    the TM-derived leff_1rm_tm formula both now cap reps at _MAX_EPLEY_REPS = 12 before
    applying 1 + reps/30. Epley's formula is unreliable above ~12 reps. A dip user with
    test_max=20 was previously prescribed +21.5 kg (impossible); now correctly ~+6 kg.
  • Grip rotation resumes correctly after deviant grip_init_grip_counts now records
    the last logged grip per session type and uses cycle.index(last_grip) + 1 as the rotation
    position. Previously it counted total sessions, causing rotation to restart from the same
    grip after any deviation (e.g. pronated → neutral → pronated-deviant → pronated again).
  • Autoregulation set floor no longer hardcoded at 3apply_autoregulation accepts
    sets_min: int = 1 and uses it as the floor when reducing sets for low readiness. The
    old max(3, ...) over-prescribed for beginners whose level-based set count was 1–2.

Removed

  • assist_progression field removed from exercise YAML and ExerciseDefinition
    resistance bands (BAND_SET) are now treated identically to MACHINE_ASSISTED: the
    user declares available resistance values and the planner ceiling-snaps each session.
    No automatic step-down from bands to unassisted. To graduate, the user removes
    BAND_SET from available_items.
  • check_band_progression(data_dir, exercise_id) removed — no replacement;
    automatic band step-down logic has been deleted.
  • get_next_band_step(item_id, exercise_id) removed — no replacement.
  • get_assist_progression(exercise_id) removed — no replacement.
  • get_equipment_catalog() no longer returns "assist_progression" — return shape
    is now {"default_item": str, "items": dict}.

Added

  • default_item per exercise — each exercise YAML now declares a default_item
    (e.g. BAR_ONLY for pull-up, PARALLEL_BARS for dip, DUMBBELLS for BSS/incline).
    Exposed in get_exercise_info(), list_exercises(), and get_equipment_catalog().
    Allows clients to pre-select a sensible starting equipment without hardcoding
    exercise knowledge.
  • requires_weight_declaration flag per catalog item — boolean field in each
    equipment catalog entry (true for BAND_SET, MACHINE_ASSISTED, DUMBBELLS).
    Clients read this flag to know when they must prompt the user for specific kg values
    before the item can be used by the planner.
  • BAND_SET replaces BAND_HEAVY / BAND_MEDIUM / BAND_LIGHT — resistance bands
    now use the same model as MACHINE_ASSISTED: the user declares their actual band
    resistance values in kg via update_equipment(..., available_band_assistance_kg=[...]),
    and the planner ceiling-snaps the computed ideal to the smallest available value ≥ ideal.
    Eliminates the hardcoded midpoint estimates (17/35/57 kg) and the three-tier distinction.
  • calculate_band_assistance() in core/planner/load_calculator.py — mirrors
    calculate_machine_assistance() for band resistance values; both share a private
    _calculate_variable_assistance() helper.
  • available_band_assistance_kg field on EquipmentState and serialized to profile.json.
  • get_equipment_catalog() now returns a structured dict with default_item,
    assist_progression, and items (each item includes requires_weight_declaration).
    Breaking change from the previous flat dict[str, dict] return type.

Removed

  • BAND_HEAVY, BAND_MEDIUM, BAND_LIGHT equipment items from pull_up.yaml and
    dip.yaml (superseded by BAND_SET).
  • machine_to_nearest_band() from core/equipment.py — no longer needed without
    fixed band tiers.