# Workout Generation ## Weekly Plan Generation Flow ``` User Preferences | v [days_per_week] -----> Pick WeeklySplitPattern (from ML patterns or defaults) e.g. 4 days -> push / pull / legs / full_body | v [preferred_workout_types] --> Distribute workout types across training days e.g. Day 1: cross_training, Day 2: functional_strength, ... | v [preferred_days] ---------> Assign training days to weekday slots, fill rest days | v For each training day: | +---> generate_single_workout() | v _get_workout_type_params() | +-- Query WorkoutStructureRule where: | workout_type = day's type (e.g. cross_training) | section_type = 'working' | goal_type = primary_goal (e.g. 'strength') | Falls back to unfiltered if no match | +-- _apply_fitness_scaling() | Beginner: +2 reps, -1 round, +15s rest | Intermediate: baseline | Advanced: -1 rep min, +1 round, -10s rest | Elite: -2 rep min, +1 round, -15s rest | v _build_warmup() | Beginner: 5-7 exercises | Intermediate: 4-6 | Advanced/Elite: 3-5 v _build_working_supersets() | +-- Override duration_bias by primary_goal: | strength=0.1, hypertrophy=0.15, | endurance=0.7, weight_loss=0.6 | +-- Scale superset counts by fitness_level: | Beginner: max 3 supersets, max 3 exercises each | Elite: +1 to both | +-- For each superset: | Select exercises via ExerciseSelector | Assign reps or duration per exercise | Position-based movement patterns (compound early, isolation late) | v _build_cooldown() | Beginner: 4-5 exercises | Intermediate: 3-4 | Advanced/Elite: 2-3 v _adjust_to_time_target() | preferred_workout_duration * active_time_ratio | Beginner: 55%, Intermediate: 65% | Advanced: 70%, Elite: 75% | Trims supersets if over, pads if under v Final workout_spec ``` ## Exercise Selection Flow ``` ExerciseSelector.select_exercises(muscle_groups, count, movement_pattern_preference) | v _get_filtered_queryset() | +-- Start: All 1132 exercises | +-- REMOVE: user's excluded_exercises | +-- REMOVE: already used in this workout (no duplicates) | +-- FILTER by equipment: | Exercises whose required equipment is in user's available_equipment | + all bodyweight exercises (no WorkoutEquipment row) | If no equipment set, skip this filter (all exercises available) | +-- FILTER by muscle groups: | Match target muscles through ExerciseMuscle join table | Uses normalized muscle names (e.g. "Quads" -> "quads") | +-- FILTER by duration flag: | If duration-based superset, only is_duration=True | +-- FILTER by fitness level: | Beginner: exclude movement_patterns containing "olympic" or "plyometric" | v Apply movement pattern preference | +-- Split queryset into preferred_qs and other_qs | based on MovementPatternOrder position (early/middle/late) | +-- Advanced/Elite: auto-boost "compound" and "multi-joint" if no | explicit preference set | v _weighted_pick(preferred_qs, other_qs, count) | +-- Build pool: preferred exercises appear 3x, others 1x +-- Shuffle and randomly pick until count unique exercises selected | v Fallbacks (if not enough exercises found): | +-- Widen to bodyweight-only exercises +-- Widen to any muscle group (for warmup/cooldown) +-- Try without muscle filter as last resort (for working sets) | v _pair_sided_exercises() | +-- If exercise has side="Left", find matching "Right" version +-- Insert partner immediately after the original | v For each selected exercise, assign: | +-- Roll random() < duration_bias | Yes + is_duration: assign random duration (duration_min to duration_max) | No or is_reps: assign random reps (rep_min to rep_max) | If is_weight: add weight=null placeholder | v Final exercise list for superset ``` --- # Workout Generation Parameters ## Fitness Level Scaling | Parameter | Beginner (1) | Intermediate (2) | Advanced (3) | Elite (4) | |---|---|---|---|---| | **Rep min adjustment** | +2 | baseline | -1 | -2 | | **Rep max adjustment** | +2 | baseline | 0 | 0 | | **Rounds adjustment** | -1 | baseline | +1 | +1 | | **Rest between sets** | +15s | baseline | -10s | -15s | | **Active time ratio** | 55% | 65% | 70% | 75% | | **Max supersets** | capped at 3 | default | default | +1 allowed | | **Max exercises/superset** | capped at 3 | default | default | +1 allowed | | **Warmup exercises** | 5-7 | 4-6 | 3-5 | 3-5 | | **Cooldown exercises** | 4-5 | 3-4 | 2-3 | 2-3 | | **Exercise filtering** | excludes olympic, plyometric | none | boosts compound | boosts compound | ## Primary Goal Duration Bias Blending The goal's duration bias is **blended** with the workout type's native bias (70% workout type / 30% goal) so the workout type's character stays dominant. A strength workout remains mostly rep-based even for endurance or weight-loss goals. `final_bias = (workout_type_bias * 0.7) + (goal_bias * 0.3)` | Goal | Goal Bias | Effect | |---|---|---| | **strength** | 0.1 | nudges toward rep-based | | **hypertrophy** | 0.15 | slight nudge toward rep-based | | **endurance** | 0.7 | nudges toward duration-based | | **weight_loss** | 0.6 | slight nudge toward duration-based | | **general_fitness** | (uses workout type default) | no blending applied | Example: Traditional Strength (native 0.1) + weight_loss (0.6) + strength secondary (0.1): - goal_bias = 0.6 * 0.7 + 0.1 * 0.3 = 0.45 - final = 0.1 * 0.7 + 0.45 * 0.3 = **0.21** (still ~80% rep-based) ## Goal-Based Structure Rule Adjustments `analyze_workouts` generates 5 variants of each `WorkoutStructureRule` (one per goal), applying these adjustments to the baseline stats extracted from historical data: | Parameter | strength | hypertrophy | endurance | weight_loss | general_fitness | |---|---|---|---|---|---| | **Rep min multiplier** | 0.6x | 0.9x | 1.3x | 1.2x | 1.0x | | **Rep max multiplier** | 0.7x | 1.1x | 1.5x | 1.3x | 1.0x | | **Rounds adjustment** | +1 | 0 | -1 | 0 | 0 | | **Duration min adjustment** | 0s | 0s | +10s | +5s | 0s | | **Duration max adjustment** | 0s | 0s | +15s | +10s | 0s | Example: if baseline reps are 8-12, a strength variant gets 5-8, endurance gets 10-18. ## Goal-Based Structure Rule Matching | Field | Behavior | |---|---| | **primary_goal** | `WorkoutStructureRule` queried with `goal_type=primary_goal` first | | **secondary_goal** | falls back to `goal_type=secondary_goal` if no primary match; also blends into duration_bias (70% primary / 30% secondary) | ## Target Muscle Groups When the user has `target_muscle_groups` set, those muscles are injected into every workout day's target muscle list (normalized, deduplicated). This ensures exercises for those muscles always get representation regardless of which split pattern was selected. ## Other Preferences | Preference | Effect | |---|---| | **days_per_week** | determines split pattern and rest day distribution | | **preferred_workout_duration** | time target in minutes (active time = duration * active_ratio) | | **preferred_days** | which weekdays to schedule training | | **preferred_workout_types** | distributed across training days | | **available_equipment** | filters exercise pool to matching equipment + bodyweight | | **excluded_exercises** | hard-excluded from all selection | ## Unused Fields | Field | Status | |---|---| | **injuries_limitations** | removed from API serializers; field still exists on model but not exposed |