"""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