Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import math | |
| from pydantic import BaseModel | |
| from tracker.models import Exercise, ProgramExercise, WorkoutLog | |
| from tracker.starting_load_research import ( | |
| LB_TO_KG, | |
| equipment_has_dumbbell, | |
| resolve_research_starting_weight_kg, | |
| ) | |
| class SuggestedSet(BaseModel): | |
| """What the next session should look like for one exercise.""" | |
| weight_kg: float | None | |
| target_reps: int | |
| note: str | |
| load_reference: str | None = None | |
| def quantize_half_kg(value: float) -> float: | |
| """Nearest 0.5 kg (typical plate resolution). Half-up tiebreak.""" | |
| return math.floor(value * 2 + 0.5) / 2 | |
| def quantize_suggested_load_kg( | |
| value_kg: float, | |
| exercise: Exercise, | |
| dumbbell_quantum_lb: float | None, | |
| ) -> float: | |
| """Barbell-style half-kg, or dumbbell rack steps in whole lb (default from config).""" | |
| if dumbbell_quantum_lb is None or dumbbell_quantum_lb <= 0: | |
| return quantize_half_kg(value_kg) | |
| if not equipment_has_dumbbell(exercise.equipment): | |
| return quantize_half_kg(value_kg) | |
| lb = value_kg / LB_TO_KG | |
| quantum = dumbbell_quantum_lb | |
| rounded_lb = round(lb / quantum) * quantum | |
| if lb > 0: | |
| rounded_lb = max(quantum, rounded_lb) | |
| return rounded_lb * LB_TO_KG | |
| def suggest_next( | |
| pe: ProgramExercise, | |
| last_session_logs: list[WorkoutLog], | |
| exercise: Exercise, | |
| body_weight_kg: float | None, | |
| *, | |
| calibration_bench_lb_per_hand: float | None = None, | |
| calibration_squat_lb_per_hand: float | None = None, | |
| dumbbell_weight_quantum_lb: float | None = None, | |
| ) -> SuggestedSet: | |
| """Compute suggested weight and reps from program rules and logged performance. | |
| **Double progression** (reps within range, then add load and reset reps) matches | |
| common prescription in ACSM progression models for healthy adults (Med Sci Sports | |
| Exerc 2009;41(3):687-708). | |
| **First session** (no logs): program ``starting_weight_kg`` overrides; else | |
| research-based starter from ``starting_load_research`` and latest body weight. | |
| **Later sessions** use logged loads from the last session for this slot. | |
| **Failure** (any set below ``target_rep_min``): reduce weight by one **program** | |
| ``weight_increment_kg`` step (your chosen bar/plate resolution), quantized to | |
| 0.5 kg. If increment is zero (e.g. bodyweight-only slot), use a 0.5 kg minimum | |
| step when load is positive; at ~0 kg load, keep load at 0 and reset rep target | |
| to the bottom of the range. | |
| """ | |
| if not last_session_logs: | |
| if pe.weight_increment_kg <= 0: | |
| return SuggestedSet( | |
| weight_kg=None, | |
| target_reps=pe.target_rep_min, | |
| note="No prior log: log reps for each set.", | |
| ) | |
| if pe.starting_weight_kg is not None and pe.starting_weight_kg > 0: | |
| return SuggestedSet( | |
| weight_kg=quantize_suggested_load_kg( | |
| pe.starting_weight_kg, | |
| exercise, | |
| dumbbell_weight_quantum_lb, | |
| ), | |
| target_reps=pe.target_rep_min, | |
| note="Starting load from program (PATCH exercise if your gym differs).", | |
| load_reference="Program exercise override.", | |
| ) | |
| research_kg, ref = resolve_research_starting_weight_kg( | |
| exercise, | |
| body_weight_kg, | |
| calibration_bench_lb_per_hand=calibration_bench_lb_per_hand, | |
| calibration_squat_lb_per_hand=calibration_squat_lb_per_hand, | |
| ) | |
| if research_kg is not None and research_kg > 0: | |
| return SuggestedSet( | |
| weight_kg=quantize_suggested_load_kg( | |
| research_kg, | |
| exercise, | |
| dumbbell_weight_quantum_lb, | |
| ), | |
| target_reps=pe.target_rep_min, | |
| note="Evidence-based starter load for this movement; adjust to RPE and plates.", | |
| load_reference=ref, | |
| ) | |
| if body_weight_kg is None or body_weight_kg <= 0: | |
| return SuggestedSet( | |
| weight_kg=None, | |
| target_reps=pe.target_rep_min, | |
| note=( | |
| "Log body weight above to compute a research-based starter " | |
| f"from your mass. ({ref})" | |
| ), | |
| load_reference=ref, | |
| ) | |
| return SuggestedSet( | |
| weight_kg=None, | |
| target_reps=pe.target_rep_min, | |
| note=( | |
| "Enter the load you will use; no default fraction for this equipment. " | |
| f"({ref})" | |
| ), | |
| load_reference=ref, | |
| ) | |
| last_weight = max(log.weight_kg for log in last_session_logs) | |
| all_hit_max = all(log.reps_done >= pe.target_rep_max for log in last_session_logs) | |
| any_below_min = any(log.reps_done < pe.target_rep_min for log in last_session_logs) | |
| if all_hit_max: | |
| if pe.weight_increment_kg <= 0: | |
| return SuggestedSet( | |
| weight_kg=None, | |
| target_reps=pe.target_rep_min, | |
| note="All sets at top of rep range: reset to bottom; progress with reps or harder variation.", | |
| ) | |
| return SuggestedSet( | |
| weight_kg=quantize_suggested_load_kg( | |
| last_weight + pe.weight_increment_kg, | |
| exercise, | |
| dumbbell_weight_quantum_lb, | |
| ), | |
| target_reps=pe.target_rep_min, | |
| note="All sets at top of rep range: add load, reset reps.", | |
| ) | |
| if any_below_min: | |
| if pe.weight_increment_kg <= 0: | |
| return SuggestedSet( | |
| weight_kg=None, | |
| target_reps=pe.target_rep_min, | |
| note="Below rep target last session: reset to bottom of rep range.", | |
| ) | |
| step_down = pe.weight_increment_kg if pe.weight_increment_kg > 0 else 0.5 | |
| if last_weight < 0.25: | |
| return SuggestedSet( | |
| weight_kg=0.0, | |
| target_reps=pe.target_rep_min, | |
| note="Below rep target: reset to bottom of rep range at bodyweight.", | |
| ) | |
| reduced = quantize_suggested_load_kg( | |
| last_weight - step_down, | |
| exercise, | |
| dumbbell_weight_quantum_lb, | |
| ) | |
| floor_kg = max(0.5, reduced) | |
| return SuggestedSet( | |
| weight_kg=floor_kg, | |
| target_reps=pe.target_rep_min, | |
| note="Below rep target last session: one increment lighter.", | |
| ) | |
| best_reps = max(log.reps_done for log in last_session_logs) | |
| rep_note = ( | |
| "Add reps within range before increasing load." | |
| if pe.weight_increment_kg > 0 | |
| else "Add reps within range before a harder variation." | |
| ) | |
| return SuggestedSet( | |
| weight_kg=( | |
| quantize_suggested_load_kg(last_weight, exercise, dumbbell_weight_quantum_lb) | |
| if pe.weight_increment_kg > 0 | |
| else None | |
| ), | |
| target_reps=min(best_reps + 1, pe.target_rep_max), | |
| note=rep_note, | |
| ) | |