Daphne commited on
Commit
5ece1ba
·
1 Parent(s): 5d25a8e

Test rep metric variation detection

Browse files
Files changed (1) hide show
  1. tests/test_rep_analysis_variation.py +196 -0
tests/test_rep_analysis_variation.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import math
5
+ import sys
6
+ import unittest
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
9
+
10
+ from pozify.contracts import (
11
+ ExerciseClassification,
12
+ PoseFrame,
13
+ PoseSequence,
14
+ Rep,
15
+ Reps,
16
+ UserProfile,
17
+ )
18
+ from pozify.steps import rep_analysis, variation_detector
19
+
20
+
21
+ def _frame(frame_index: int, landmarks: dict[str, dict[str, float]]) -> PoseFrame:
22
+ return PoseFrame(
23
+ frame_index=frame_index,
24
+ timestamp_sec=round(frame_index / 30.0, 3),
25
+ landmarks=landmarks,
26
+ world_landmarks={},
27
+ pose_quality={"mean_visibility": 0.95, "normalized": True},
28
+ )
29
+
30
+
31
+ def _wave(frame_index: int, cycle_frames: int) -> float:
32
+ return (1.0 - math.cos(2.0 * math.pi * (frame_index / cycle_frames))) / 2.0
33
+
34
+
35
+ def _push_up_landmarks(depth: float, *, hand_ratio: float = 1.7) -> dict[str, dict[str, float]]:
36
+ shoulder_y = 0.38 + depth * 0.16
37
+ hip_y = 0.46 + depth * 0.16
38
+ ankle_y = 0.54 + depth * 0.16
39
+ elbow_y = 0.46 + depth * 0.1
40
+ wrist_y = 0.5 + depth * 0.04
41
+ shoulder_width = 0.4
42
+ hand_width = shoulder_width * hand_ratio
43
+ return {
44
+ "left_shoulder": {"x": 0.3, "y": shoulder_y},
45
+ "right_shoulder": {"x": 0.7, "y": shoulder_y},
46
+ "left_elbow": {"x": 0.36 + depth * 0.02, "y": elbow_y},
47
+ "right_elbow": {"x": 0.64 - depth * 0.02, "y": elbow_y},
48
+ "left_wrist": {"x": 0.5 - hand_width / 2, "y": wrist_y},
49
+ "right_wrist": {"x": 0.5 + hand_width / 2, "y": wrist_y},
50
+ "left_hip": {"x": 0.42, "y": hip_y},
51
+ "right_hip": {"x": 0.58, "y": hip_y},
52
+ "left_ankle": {"x": 0.44, "y": ankle_y},
53
+ "right_ankle": {"x": 0.56, "y": ankle_y},
54
+ }
55
+
56
+
57
+ def _squat_landmarks(depth: float, *, stance_ratio: float = 1.5) -> dict[str, dict[str, float]]:
58
+ shoulder_width = 0.2
59
+ stance_width = shoulder_width * stance_ratio
60
+ hip_y = 0.52 + depth * 0.22
61
+ shoulder_y = 0.28 + depth * 0.06
62
+ knee_y = 0.7
63
+ return {
64
+ "left_shoulder": {"x": 0.4, "y": shoulder_y},
65
+ "right_shoulder": {"x": 0.6, "y": shoulder_y},
66
+ "left_hip": {"x": 0.43, "y": hip_y},
67
+ "right_hip": {"x": 0.57, "y": hip_y},
68
+ "left_knee": {"x": 0.43 + depth * 0.05, "y": knee_y},
69
+ "right_knee": {"x": 0.57 - depth * 0.05, "y": knee_y},
70
+ "left_ankle": {"x": 0.5 - stance_width / 2, "y": 0.92},
71
+ "right_ankle": {"x": 0.5 + stance_width / 2, "y": 0.92},
72
+ }
73
+
74
+
75
+ def _shoulder_press_landmarks(lift: float, *, partial: bool = True) -> dict[str, dict[str, float]]:
76
+ wrist_top = 0.56 if partial else 0.42
77
+ wrist_y = 0.76 - lift * (0.76 - wrist_top)
78
+ elbow_y = 0.6 - lift * 0.06
79
+ return {
80
+ "left_shoulder": {"x": 0.42, "y": 0.42},
81
+ "right_shoulder": {"x": 0.58, "y": 0.42},
82
+ "left_elbow": {"x": 0.4 - lift * 0.02, "y": elbow_y},
83
+ "right_elbow": {"x": 0.6 + lift * 0.02, "y": elbow_y},
84
+ "left_wrist": {"x": 0.4, "y": wrist_y},
85
+ "right_wrist": {"x": 0.6, "y": wrist_y},
86
+ "left_hip": {"x": 0.44, "y": 0.74},
87
+ "right_hip": {"x": 0.56, "y": 0.74},
88
+ }
89
+
90
+
91
+ def _sequence(exercise: str) -> PoseSequence:
92
+ frames = []
93
+ for frame_index in range(25):
94
+ wave = _wave(frame_index, 24)
95
+ if exercise == "push_up":
96
+ landmarks = _push_up_landmarks(wave)
97
+ elif exercise == "shoulder_press":
98
+ landmarks = _shoulder_press_landmarks(wave)
99
+ else:
100
+ landmarks = _squat_landmarks(wave)
101
+ frames.append(_frame(frame_index, landmarks))
102
+ return PoseSequence(
103
+ frames=frames,
104
+ normalized=True,
105
+ smoothing_method="none",
106
+ pose_valid_ratio=1.0,
107
+ )
108
+
109
+
110
+ def _classification(exercise: str) -> ExerciseClassification:
111
+ return ExerciseClassification(
112
+ exercise=exercise,
113
+ confidence=0.95,
114
+ window_predictions=[],
115
+ fallback_required=False,
116
+ )
117
+
118
+
119
+ def _reps(exercise: str) -> Reps:
120
+ return Reps(
121
+ exercise=exercise,
122
+ reps=[Rep(1, 0, 12, 24, 0.0, 0.4, 0.8)],
123
+ partial_reps=[],
124
+ )
125
+
126
+
127
+ def _profile() -> UserProfile:
128
+ return UserProfile(
129
+ goal="beginner_practice",
130
+ experience_level="beginner",
131
+ intended_exercise="auto",
132
+ intended_variation=None,
133
+ known_limitations=[],
134
+ equipment="bodyweight",
135
+ )
136
+
137
+
138
+ class RepAnalysisVariationTests(unittest.TestCase):
139
+ def test_push_up_metrics_detect_wide_grip_variation(self) -> None:
140
+ analysis = rep_analysis.run(
141
+ _classification("push_up"),
142
+ _reps("push_up"),
143
+ _sequence("push_up"),
144
+ )
145
+ variation = variation_detector.run(_classification("push_up"), analysis, _profile())
146
+
147
+ self.assertEqual(variation.detected_variation, "wide_grip_push_up")
148
+ self.assertIn("wide_hand_placement", variation.not_issues)
149
+ self.assertGreater(analysis.items[0].metrics["hand_width_ratio"], 1.45)
150
+ self.assertIn("body_line_score", analysis.items[0].metrics)
151
+
152
+ def test_squat_metrics_detect_wide_stance_variation(self) -> None:
153
+ analysis = rep_analysis.run(_classification("squat"), _reps("squat"), _sequence("squat"))
154
+ variation = variation_detector.run(_classification("squat"), analysis, _profile())
155
+
156
+ self.assertEqual(variation.detected_variation, "wide_squat_stance")
157
+ self.assertIn("wide_stance", variation.not_issues)
158
+ self.assertGreater(analysis.items[0].metrics["stance_width_ratio"], 1.35)
159
+ self.assertIn("min_knee_angle_deg", analysis.items[0].metrics)
160
+
161
+ def test_shoulder_press_metrics_detect_partial_press_variation(self) -> None:
162
+ analysis = rep_analysis.run(
163
+ _classification("shoulder_press"),
164
+ _reps("shoulder_press"),
165
+ _sequence("shoulder_press"),
166
+ )
167
+ variation = variation_detector.run(_classification("shoulder_press"), analysis, _profile())
168
+
169
+ self.assertEqual(variation.detected_variation, "partial_press")
170
+ self.assertIn("partial_range_of_motion", variation.not_issues)
171
+ self.assertIn("lockout_quality", analysis.items[0].metrics)
172
+ self.assertLess(analysis.aggregate_metrics["avg_wrist_travel"], 0.24)
173
+
174
+ def test_profile_intended_variation_overrides_metric_rule(self) -> None:
175
+ profile = UserProfile(
176
+ goal="beginner_practice",
177
+ experience_level="beginner",
178
+ intended_exercise="push_up",
179
+ intended_variation="close_grip_push_up",
180
+ known_limitations=[],
181
+ equipment="bodyweight",
182
+ )
183
+ analysis = rep_analysis.run(
184
+ _classification("push_up"),
185
+ _reps("push_up"),
186
+ _sequence("push_up"),
187
+ )
188
+ variation = variation_detector.run(_classification("push_up"), analysis, profile)
189
+
190
+ self.assertEqual(variation.detected_variation, "close_grip_push_up")
191
+ self.assertEqual(variation.variation_confidence, 0.95)
192
+ self.assertIn("close_hand_placement", variation.not_issues)
193
+
194
+
195
+ if __name__ == "__main__":
196
+ unittest.main()