From 909c75d8ee169dd528a19c4a471c7661f09badeb Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 23 Feb 2026 11:25:11 -0600 Subject: [PATCH] Tighten warmup selection to dynamic prep only --- generator/services/exercise_selector.py | 33 ++++++---- generator/tests/test_warmup_selector.py | 86 +++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 generator/tests/test_warmup_selector.py diff --git a/generator/services/exercise_selector.py b/generator/services/exercise_selector.py index a6fc610..595983a 100644 --- a/generator/services/exercise_selector.py +++ b/generator/services/exercise_selector.py @@ -117,8 +117,8 @@ class ExerciseSelector: # Movement patterns considered appropriate for warm-up / cool-down WARMUP_PATTERNS = [ - 'dynamic stretch', 'activation', 'mobility', 'warm up', - 'warmup', 'stretch', 'foam roll', + 'dynamic stretch', 'mobility - dynamic', 'activation', 'warm up', + 'warmup', 'cardio/locomotion', 'balance', ] COOLDOWN_PATTERNS = [ 'static stretch', 'stretch', 'cool down', 'cooldown', @@ -129,6 +129,10 @@ class ExerciseSelector: COOLDOWN_EXCLUDED_PATTERNS = [ 'plyometric', 'combat', 'cardio/locomotion', 'olympic', ] + # Warm-up must avoid working-set patterns. + WARMUP_EXCLUDED_PATTERNS = [ + 'upper push', 'upper pull', 'olympic', 'combat', 'arms', + ] def __init__(self, user_preference, recently_used_ids=None, hard_exclude_ids=None): self.user_preference = user_preference @@ -335,23 +339,27 @@ class ExerciseSelector: for kw in self.WARMUP_PATTERNS: warmup_q |= Q(movement_patterns__icontains=kw) + # Warm-up should be dynamic movement prep, not loaded working sets. + qs = qs.exclude(is_weight=True) # Exclude heavy compounds (no barbell squats in warmup) - qs = qs.exclude(is_weight=True, is_compound=True) + qs = qs.exclude(is_compound=True) # Exclude primary-tier exercises (no primary lifts in warmup) qs = qs.exclude(exercise_tier='primary') # Exclude technically complex movements qs = qs.exclude(complexity_rating__gte=4) + # Exclude common working-set movement families from warmup. + warmup_exclude_q = Q() + for pat in self.WARMUP_EXCLUDED_PATTERNS: + warmup_exclude_q |= Q(movement_patterns__icontains=pat) + qs = qs.exclude(warmup_exclude_q) # Tightened HR filter for warmup (1-4 instead of 2-5) hr_warmup_q = Q(hr_elevation_rating__gte=1, hr_elevation_rating__lte=4) preferred = qs.filter(warmup_q).filter( hr_warmup_q | Q(hr_elevation_rating__isnull=True) ) - other = qs.exclude(pk__in=preferred.values_list('pk', flat=True)).filter( - hr_warmup_q | Q(hr_elevation_rating__isnull=True) - ) - - selected = self._weighted_pick(preferred, other, count) + # STRICT: warmup must come from warmup-pattern exercises only. + selected = self._weighted_pick(preferred, preferred.none(), count) # Fallback: if not enough duration-based warmup exercises, widen to # any duration exercise regardless of muscle group @@ -362,17 +370,16 @@ class ExerciseSelector: fitness_level=fitness_level, ).exclude(pk__in={e.pk for e in selected}) # Apply same warmup safety exclusions - wide_qs = wide_qs.exclude(is_weight=True, is_compound=True) + wide_qs = wide_qs.exclude(is_weight=True) + wide_qs = wide_qs.exclude(is_compound=True) wide_qs = wide_qs.exclude(exercise_tier='primary') wide_qs = wide_qs.exclude(complexity_rating__gte=4) + wide_qs = wide_qs.exclude(warmup_exclude_q) wide_preferred = wide_qs.filter(warmup_q).filter( hr_warmup_q | Q(hr_elevation_rating__isnull=True) ) - wide_other = wide_qs.exclude(pk__in=wide_preferred.values_list('pk', flat=True)).filter( - hr_warmup_q | Q(hr_elevation_rating__isnull=True) - ) selected.extend( - self._weighted_pick(wide_preferred, wide_other, count - len(selected)) + self._weighted_pick(wide_preferred, wide_preferred.none(), count - len(selected)) ) for ex in selected: diff --git a/generator/tests/test_warmup_selector.py b/generator/tests/test_warmup_selector.py new file mode 100644 index 0000000..0fdbfba --- /dev/null +++ b/generator/tests/test_warmup_selector.py @@ -0,0 +1,86 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from exercise.models import Exercise +from generator.models import UserPreference +from generator.services.exercise_selector import ExerciseSelector +from registered_user.models import RegisteredUser + + +class TestWarmupSelector(TestCase): + def setUp(self): + django_user = User.objects.create_user( + username='warmup_selector_user', + password='testpass123', + ) + registered_user = RegisteredUser.objects.create( + user=django_user, + first_name='Warmup', + last_name='Tester', + ) + self.preference = UserPreference.objects.create( + registered_user=registered_user, + days_per_week=4, + fitness_level=2, + ) + + def test_warmup_excludes_working_set_movements(self): + dynamic_1 = Exercise.objects.create( + name='Dynamic Warmup A', + movement_patterns='dynamic stretch, mobility - dynamic, activation, warm up', + is_duration=True, + is_reps=False, + is_weight=False, + is_compound=False, + exercise_tier='accessory', + hr_elevation_rating=2, + complexity_rating=2, + difficulty_level='beginner', + ) + dynamic_2 = Exercise.objects.create( + name='Dynamic Warmup B', + movement_patterns='mobility - dynamic, cardio/locomotion, balance', + is_duration=True, + is_reps=False, + is_weight=False, + is_compound=False, + exercise_tier='accessory', + hr_elevation_rating=3, + complexity_rating=2, + difficulty_level='beginner', + ) + + weighted_press = Exercise.objects.create( + name='Lying Dumbbell Tricep Extension', + movement_patterns='upper push - horizontal, upper push, arms', + is_duration=True, + is_reps=False, + is_weight=True, + is_compound=False, + exercise_tier='secondary', + hr_elevation_rating=2, + complexity_rating=2, + difficulty_level='intermediate', + ) + duration_push = Exercise.objects.create( + name='Floor Press Hold', + movement_patterns='upper push - horizontal, upper push', + is_duration=True, + is_reps=False, + is_weight=False, + is_compound=False, + exercise_tier='secondary', + hr_elevation_rating=2, + complexity_rating=2, + difficulty_level='intermediate', + ) + + selector = ExerciseSelector(self.preference) + selected = selector.select_warmup_exercises(target_muscles=[], count=4) + + selected_ids = {ex.pk for ex in selected} + + self.assertIn(dynamic_1.pk, selected_ids) + self.assertIn(dynamic_2.pk, selected_ids) + self.assertNotIn(weighted_press.pk, selected_ids) + self.assertNotIn(duration_push.pk, selected_ids)