FitCoach / utils /overload.py
coffeine16's picture
Upload folder using huggingface_hub
1a74e1c verified
"""
Progressive Overload Engine — standalone, stdlib only.
Given exercise performance history, determines whether the agent's
proposed prescription follows correct double-progression rules.
Used by the grader to score the 'progressive_overload' dimension.
"""
from __future__ import annotations
from typing import Optional
def _parse_rep_range(s: str) -> tuple[int, int]:
if not s:
return (8, 12)
s = s.strip()
if "-" in s:
parts = s.split("-", 1)
try:
return (int(parts[0]), int(parts[1]))
except ValueError:
return (8, 12)
try:
n = int(s)
return (n, n)
except ValueError:
return (8, 12)
def _parse_reps_completed(s: str) -> list[int]:
if not s:
return []
out = []
for part in s.split(","):
try:
out.append(int(part.strip()))
except ValueError:
pass
return out
def _is_compound(name: str) -> bool:
compound = ["squat", "deadlift", "bench", "press", "row",
"pull-up", "pullup", "chin-up", "dip", "lunge", "thrust"]
isolation = ["curl", "extension", "raise", "fly", "flye", "kickback"]
nl = name.lower()
if any(k in nl for k in isolation):
return False
return any(k in nl for k in compound)
def expected_progression(
exercise_name: str,
last_weight_kg: float,
last_reps_str: str,
target_reps: str = "8-12",
target_sets: int = 3,
) -> dict:
"""
Compute the correct next-session prescription given last performance.
Returns {"progression_type", "expected_weight_kg", "expected_reps"}.
"""
last_reps = _parse_reps_completed(last_reps_str)
target_lo, target_hi = _parse_rep_range(target_reps)
compound = _is_compound(exercise_name)
if not last_reps:
return {"progression_type": "repeat",
"expected_weight_kg": last_weight_kg,
"expected_reps": target_reps}
all_hit_top = (
len(last_reps) >= target_sets
and all(r >= target_hi for r in last_reps[:target_sets])
)
heavy_miss = sum(1 for r in last_reps if r < target_lo) >= 2
single_miss = any(r < target_lo for r in last_reps) and not all_hit_top
if heavy_miss:
dw = round(last_weight_kg * 0.9 / 2.5) * 2.5 if last_weight_kg > 0 else 0
return {"progression_type": "deload",
"expected_weight_kg": dw,
"expected_reps": target_reps}
if single_miss:
return {"progression_type": "repeat",
"expected_weight_kg": last_weight_kg,
"expected_reps": target_reps}
if all_hit_top:
if last_weight_kg == 0:
new_hi = target_hi + 2
return {"progression_type": "add_reps",
"expected_weight_kg": 0,
"expected_reps": f"{target_lo + 1}-{new_hi}"}
inc = 2.5 if compound else 1.25
nw = round((last_weight_kg + inc) / 2.5) * 2.5
return {"progression_type": "add_weight",
"expected_weight_kg": nw,
"expected_reps": target_reps}
return {"progression_type": "repeat",
"expected_weight_kg": last_weight_kg,
"expected_reps": target_reps}
def verify_agent_overload(
exercise_name: str,
agent_weight_kg: float,
agent_reps: str,
last_weight_kg: float,
last_reps_str: str,
target_reps: str = "8-12",
target_sets: int = 3,
) -> tuple[bool, str]:
"""
Check whether agent's prescription follows correct overload logic.
Returns (is_correct, explanation).
"""
expected = expected_progression(
exercise_name, last_weight_kg, last_reps_str, target_reps, target_sets
)
ptype = expected["progression_type"]
exp_w = expected["expected_weight_kg"]
if ptype == "add_weight":
min_ok = last_weight_kg + 1.0 # must be meaningfully heavier
if agent_weight_kg >= min_ok:
return True, (
f"Correct: added weight "
f"(agent={agent_weight_kg}kg, expected≥{exp_w}kg)."
)
return False, (
f"Should add weight to {exp_w}kg "
f"(agent submitted {agent_weight_kg}kg, last was {last_weight_kg}kg)."
)
if ptype == "deload":
if agent_weight_kg <= last_weight_kg * 0.95:
return True, f"Correct deload (agent={agent_weight_kg}kg < {last_weight_kg}kg)."
return False, f"Should deload to ~{exp_w}kg, agent kept {agent_weight_kg}kg."
if ptype == "add_reps":
_, target_hi = _parse_rep_range(target_reps)
_, agent_hi = _parse_rep_range(agent_reps)
if agent_hi > target_hi:
return True, f"Correct rep progression (agent={agent_reps})."
return False, (
f"Should increase rep target above {target_hi}, "
f"agent submitted {agent_reps}."
)
# repeat — weight should stay same ±2.5kg
if abs(agent_weight_kg - last_weight_kg) <= 2.5:
return True, f"Correct: repeating prescription ({last_weight_kg}kg)."
return False, (
f"Should repeat {last_weight_kg}kg, agent changed to {agent_weight_kg}kg."
)