workout-tracker / tracker /starting_load_research.py
ronheichman's picture
Deploy workout tracker
cc34c37 verified
Raw
History Blame Contribute Delete
7.04 kB
"""Evidence-based first-session external loads (novice-friendly bands).
This maps each catalog exercise to a **starter load** using:
1. Optional per-row overrides on ``Exercise`` (``starting_bodyweight_fraction``,
``starting_load_kg``, ``starting_load_reference``).
2. Otherwise **movement-pattern defaults**: total external load as a fraction of
**body mass** (for dumbbell work, total load for both hands combined), or a
small fixed load when %BW does not apply.
**Sources** (operationalize "light to moderate" first sessions):
- American College of Sports Medicine. Progression models in resistance training
for healthy adults. *Med Sci Sports Exerc.* 2009;41(3):687-708 (initial intensity
light–moderate, progressive overload).
- Rhea MR, et al. A meta-analysis to determine the dose response for strength
development. *Med Sci Sports Exerc.* 2003;35(3):456-464 (intensity must be
individualized; novices use lower bands).
- Kraemer WJ, Ratamess NA. Fundamentals of resistance training: progression and
exercise prescription. *Med Sci Sports Exerc.* 2004;36(4):674-688 (exercise
selection and loading vary by movement).
Fractions are **conservative** midpoints within common coaching ranges for
**total** dumbbell load / body mass for bilateral dumbbell work. They are not
trial outcomes; users should adjust to RPE and equipment jumps.
If body weight is unknown, return no computed kg and instruct the user to log
body weight (used as the scalar for %BW).
Optional ``calibration_*_lb_per_hand`` in config scales %BW dumbbell starters
toward your tested bilateral dumbbell loads (bench pattern vs squat pattern
fractions in ``default_bodyweight_fraction_total_load``).
"""
from __future__ import annotations
import math
from tracker.models import Exercise
# Exact NIST definition used for lb/kg conversion in UI and calibration.
LB_TO_KG = 0.45359237
def _quantize_half_kg(value: float) -> float:
return math.floor(value * 2 + 0.5) / 2
REFERENCE_SHORT = (
"ACSM 2009 novice light–moderate band; %BW mapping from NSCA-style "
"movement patterns (approximate)."
)
def equipment_has_dumbbell(equipment: str) -> bool:
return "dumbbell" in equipment.lower()
def equipment_is_bodyweight_only(equipment: str) -> bool:
normalized = equipment.strip().lower()
return normalized == "none" or normalized == ""
def default_bodyweight_fraction_total_load(exercise: Exercise) -> tuple[float | None, str]:
"""Total external load / body mass, or None if not applicable."""
if equipment_is_bodyweight_only(exercise.equipment):
return None, REFERENCE_SHORT
if equipment_has_dumbbell(exercise.equipment):
muscle = exercise.muscle_group.strip().lower()
if muscle in ("chest", "shoulders", "triceps"):
return 0.28, REFERENCE_SHORT
if muscle == "biceps":
return 0.12, REFERENCE_SHORT
if muscle in ("lats", "middle back", "traps"):
return 0.30, REFERENCE_SHORT
if muscle in ("quads", "hamstrings", "glutes", "calves"):
return 0.24, REFERENCE_SHORT
if muscle in ("forearms", "neck"):
return 0.08, REFERENCE_SHORT
if muscle in ("abs", "lower back"):
return 0.06, REFERENCE_SHORT
return 0.22, REFERENCE_SHORT
equipment_lower = exercise.equipment.lower()
if "bench" in equipment_lower and not equipment_has_dumbbell(exercise.equipment):
return None, REFERENCE_SHORT
return None, REFERENCE_SHORT
def apply_anchor_calibration_to_research_kg(
research_kg: float,
exercise: Exercise,
body_weight_kg: float,
bench_lb_per_hand: float | None,
squat_lb_per_hand: float | None,
) -> float:
"""Scale a %BW dumbbell starter toward tested bilateral dumbbell anchors.
Anchors are **per-hand** lb for bilateral dumbbells (total external load = 2 × hand).
Bench anchor scales upper-body patterns that use the same 0.28 fraction as chest in
``default_bodyweight_fraction_total_load``; squat anchor scales leg patterns (0.24).
Back uses the mean of the two scale factors when both are set (pulling between
upper and lower strength). See module docstring for research fractions.
"""
if not equipment_has_dumbbell(exercise.equipment):
return research_kg
if bench_lb_per_hand is None and squat_lb_per_hand is None:
return research_kg
bench_total_kg = 2.0 * bench_lb_per_hand * LB_TO_KG if bench_lb_per_hand is not None else None
squat_total_kg = 2.0 * squat_lb_per_hand * LB_TO_KG if squat_lb_per_hand is not None else None
bench_scale = bench_total_kg / (0.28 * body_weight_kg) if bench_total_kg is not None else None
squat_scale = squat_total_kg / (0.24 * body_weight_kg) if squat_total_kg is not None else None
muscle = exercise.muscle_group.strip().lower()
scale: float | None
if muscle in ("chest", "shoulders", "triceps", "forearms", "biceps", "abs", "lower back", "neck"):
scale = bench_scale
elif muscle in ("quads", "hamstrings", "glutes", "calves"):
scale = squat_scale
elif muscle in ("lats", "middle back", "traps"):
if bench_scale is not None and squat_scale is not None:
scale = (bench_scale + squat_scale) / 2.0
elif bench_scale is not None:
scale = bench_scale
else:
scale = squat_scale
else:
if bench_scale is not None and squat_scale is not None:
scale = (bench_scale + squat_scale) / 2.0
else:
scale = bench_scale if bench_scale is not None else squat_scale
if scale is None:
return research_kg
return research_kg * scale
def resolve_research_starting_weight_kg(
exercise: Exercise,
body_weight_kg: float | None,
*,
calibration_bench_lb_per_hand: float | None = None,
calibration_squat_lb_per_hand: float | None = None,
) -> tuple[float | None, str]:
"""Return (quantized kg for first session, reference blurb)."""
ref = exercise.starting_load_reference.strip() if exercise.starting_load_reference else REFERENCE_SHORT
if exercise.starting_load_kg is not None and exercise.starting_load_kg > 0:
return _quantize_half_kg(exercise.starting_load_kg), ref
fraction: float | None
if exercise.starting_bodyweight_fraction is not None:
fraction = exercise.starting_bodyweight_fraction
else:
fraction, ref = default_bodyweight_fraction_total_load(exercise)
if fraction is None or fraction <= 0:
return None, ref
if body_weight_kg is None or body_weight_kg <= 0:
return None, ref
raw_kg = fraction * body_weight_kg
if calibration_bench_lb_per_hand is not None or calibration_squat_lb_per_hand is not None:
raw_kg = apply_anchor_calibration_to_research_kg(
raw_kg,
exercise,
body_weight_kg,
calibration_bench_lb_per_hand,
calibration_squat_lb_per_hand,
)
return _quantize_half_kg(raw_kg), ref