kinemotion 0.75.0__py3-none-any.whl → 0.76.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,348 @@
1
+ """Squat Jump (SJ) metrics calculation."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, TypedDict
5
+
6
+ import numpy as np
7
+
8
+ from ..core.formatting import format_float_metric
9
+ from ..core.types import FloatArray
10
+
11
+ if TYPE_CHECKING:
12
+ from ..core.metadata import ResultMetadata
13
+
14
+
15
+ class SJDataDict(TypedDict, total=False):
16
+ """Type-safe dictionary for SJ measurement data."""
17
+
18
+ jump_height_m: float
19
+ flight_time_ms: float
20
+ squat_hold_duration_ms: float
21
+ concentric_duration_ms: float
22
+ peak_concentric_velocity_m_s: float
23
+ peak_force_n: float | None
24
+ peak_power_w: float | None
25
+ mean_power_w: float | None
26
+ squat_hold_start_frame: float | None
27
+ concentric_start_frame: float | None
28
+ takeoff_frame: float | None
29
+ landing_frame: float | None
30
+ mass_kg: float | None
31
+ tracking_method: str
32
+
33
+
34
+ class SJResultDict(TypedDict, total=False):
35
+ """Type-safe dictionary for complete SJ result with data and metadata."""
36
+
37
+ data: SJDataDict
38
+ metadata: dict # ResultMetadata.to_dict()
39
+ validation: dict # ValidationResult.to_dict()
40
+
41
+
42
+ @dataclass
43
+ class SJMetrics:
44
+ """Metrics for a squat jump analysis.
45
+
46
+ Attributes:
47
+ jump_height: Maximum jump height in meters
48
+ flight_time: Time spent in the air in milliseconds
49
+ squat_hold_duration: Duration of static squat hold phase in milliseconds
50
+ concentric_duration: Duration of concentric phase in milliseconds
51
+ peak_concentric_velocity: Maximum upward velocity during concentric phase in m/s
52
+ peak_force: Maximum force during concentric phase in Newtons
53
+ peak_power: Maximum power during concentric phase in Watts
54
+ mean_power: Mean power during concentric phase in Watts
55
+ squat_hold_start_frame: Frame index where squat hold begins (0-indexed)
56
+ concentric_start_frame: Frame index where concentric phase begins (0-indexed)
57
+ takeoff_frame: Frame index where takeoff occurs (0-indexed)
58
+ landing_frame: Frame index where landing occurs (0-indexed)
59
+ mass_kg: Athlete mass in kilograms
60
+ tracking_method: Method used for position tracking
61
+ result_metadata: Metadata about the analysis process
62
+ """
63
+
64
+ jump_height: float
65
+ flight_time: float
66
+ squat_hold_duration: float
67
+ concentric_duration: float
68
+ peak_concentric_velocity: float
69
+ peak_force: float | None = None
70
+ peak_power: float | None = None
71
+ mean_power: float | None = None
72
+ squat_hold_start_frame: float | None = None
73
+ concentric_start_frame: float | None = None
74
+ takeoff_frame: float | None = None
75
+ landing_frame: float | None = None
76
+ mass_kg: float | None = None
77
+ tracking_method: str = "hip"
78
+ result_metadata: "ResultMetadata | None" = None
79
+
80
+ def to_dict(self) -> SJResultDict:
81
+ """Convert metrics to JSON-serializable dictionary.
82
+
83
+ Returns:
84
+ Dictionary containing all SJ metrics with proper formatting.
85
+ """
86
+ data: dict[str, float | None | str] = {
87
+ "jump_height_m": format_float_metric(self.jump_height, 1, 3),
88
+ "flight_time_ms": format_float_metric(self.flight_time, 1000, 2),
89
+ "squat_hold_duration_ms": format_float_metric(self.squat_hold_duration, 1000, 2),
90
+ "concentric_duration_ms": format_float_metric(self.concentric_duration, 1000, 2),
91
+ "peak_concentric_velocity_m_s": format_float_metric(
92
+ self.peak_concentric_velocity, 1, 4
93
+ ),
94
+ }
95
+
96
+ if self.peak_force is not None:
97
+ data["peak_force_n"] = format_float_metric(self.peak_force, 1, 1)
98
+ if self.peak_power is not None:
99
+ data["peak_power_w"] = format_float_metric(self.peak_power, 1, 1)
100
+ if self.mean_power is not None:
101
+ data["mean_power_w"] = format_float_metric(self.mean_power, 1, 1)
102
+
103
+ if self.squat_hold_start_frame is not None:
104
+ data["squat_hold_start_frame"] = float(self.squat_hold_start_frame)
105
+ if self.concentric_start_frame is not None:
106
+ data["concentric_start_frame"] = float(self.concentric_start_frame)
107
+ if self.takeoff_frame is not None:
108
+ data["takeoff_frame"] = float(self.takeoff_frame)
109
+ if self.landing_frame is not None:
110
+ data["landing_frame"] = float(self.landing_frame)
111
+ if self.mass_kg is not None:
112
+ data["mass_kg"] = float(self.mass_kg)
113
+ data["tracking_method"] = self.tracking_method
114
+
115
+ result: SJResultDict = {"data": data} # type: ignore[typeddict-item]
116
+
117
+ if self.result_metadata is not None:
118
+ result["metadata"] = self.result_metadata.to_dict()
119
+
120
+ return result
121
+
122
+
123
+ def calculate_sj_metrics(
124
+ positions: FloatArray,
125
+ velocities: FloatArray,
126
+ squat_hold_start: int,
127
+ concentric_start: int,
128
+ takeoff_frame: int,
129
+ landing_frame: int,
130
+ fps: float,
131
+ mass_kg: float | None = None,
132
+ tracking_method: str = "hip",
133
+ ) -> SJMetrics:
134
+ """Calculate Squat Jump metrics from phase transitions.
135
+
136
+ Args:
137
+ positions: 1D array of vertical positions in normalized coordinates
138
+ velocities: 1D array of vertical velocities in normalized coordinates
139
+ squat_hold_start: Frame index where squat hold begins
140
+ concentric_start: Frame index where concentric phase begins
141
+ takeoff_frame: Frame index where takeoff occurs
142
+ landing_frame: Frame index where landing occurs
143
+ fps: Video frames per second
144
+ mass_kg: Athlete mass in kilograms (for power calculations)
145
+ tracking_method: Method used for position tracking
146
+
147
+ Returns:
148
+ SJMetrics object containing all calculated metrics
149
+ """
150
+ # Calculate jump height from flight time
151
+ g = 9.81 # Gravity acceleration (m/s²)
152
+ flight_time = (landing_frame - takeoff_frame) / fps
153
+ jump_height = (g * flight_time**2) / 8
154
+
155
+ # Calculate concentric duration
156
+ concentric_duration = (takeoff_frame - concentric_start) / fps
157
+
158
+ # Calculate squat hold duration
159
+ squat_hold_duration = (concentric_start - squat_hold_start) / fps
160
+
161
+ # Calculate peak concentric velocity (upward is positive)
162
+ if takeoff_frame > concentric_start:
163
+ peak_concentric_velocity = np.max(np.abs(velocities[concentric_start:takeoff_frame]))
164
+ else:
165
+ peak_concentric_velocity = 0.0
166
+
167
+ # Calculate power and force if mass is provided
168
+ peak_power = calculate_peak_power(
169
+ positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
170
+ )
171
+ mean_power = calculate_mean_power(
172
+ positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
173
+ )
174
+ peak_force = calculate_peak_force(
175
+ positions, velocities, concentric_start, takeoff_frame, fps, mass_kg
176
+ )
177
+
178
+ return SJMetrics(
179
+ jump_height=jump_height,
180
+ flight_time=flight_time,
181
+ squat_hold_duration=squat_hold_duration,
182
+ concentric_duration=concentric_duration,
183
+ peak_concentric_velocity=peak_concentric_velocity,
184
+ peak_force=peak_force,
185
+ peak_power=peak_power,
186
+ mean_power=mean_power,
187
+ squat_hold_start_frame=float(squat_hold_start),
188
+ concentric_start_frame=float(concentric_start),
189
+ takeoff_frame=float(takeoff_frame),
190
+ landing_frame=float(landing_frame),
191
+ mass_kg=mass_kg,
192
+ tracking_method=tracking_method,
193
+ )
194
+
195
+
196
+ def calculate_peak_power(
197
+ positions: FloatArray,
198
+ velocities: FloatArray,
199
+ concentric_start: int,
200
+ takeoff_frame: int,
201
+ fps: float,
202
+ mass_kg: float | None,
203
+ ) -> float | None:
204
+ """Calculate peak power using Sayers et al. (1999) regression equation.
205
+
206
+ Formula: Peak Power (W) = 60.7 × jump_height_cm + 45.3 × mass_kg − 2055
207
+
208
+ Validation (Sayers et al., 1999, N=108):
209
+ - R² = 0.87 (strong correlation with force plate data)
210
+ - SEE = 355.0 W
211
+ - Error: < 1% underestimation
212
+ - Superior to Lewis formula (73% error) and Harman equation
213
+
214
+ Args:
215
+ positions: 1D array of vertical positions (not used in regression)
216
+ velocities: 1D array of vertical velocities
217
+ concentric_start: Frame index where concentric phase begins
218
+ takeoff_frame: Frame index where takeoff occurs
219
+ fps: Video frames per second
220
+ mass_kg: Athlete mass in kilograms
221
+
222
+ Returns:
223
+ Peak power in Watts, or None if mass is not provided
224
+ """
225
+ if mass_kg is None:
226
+ return None
227
+
228
+ g = 9.81
229
+
230
+ # Calculate takeoff velocity (negative = upward in normalized coords)
231
+ if takeoff_frame > concentric_start:
232
+ takeoff_velocity = np.min(velocities[concentric_start:takeoff_frame])
233
+ else:
234
+ takeoff_velocity = 0.0
235
+
236
+ # Calculate jump height from takeoff velocity: h = v² / (2g)
237
+ # Use absolute value since v is negative for upward motion
238
+ jump_height_m = (takeoff_velocity**2) / (2 * g)
239
+
240
+ # Convert to centimeters for Sayers formula
241
+ jump_height_cm = jump_height_m * 100
242
+
243
+ # Sayers et al. (1999) regression equation
244
+ peak_power = 60.7 * jump_height_cm + 45.3 * mass_kg - 2055
245
+
246
+ return float(peak_power)
247
+
248
+
249
+ def calculate_mean_power(
250
+ positions: FloatArray,
251
+ velocities: FloatArray,
252
+ concentric_start: int,
253
+ takeoff_frame: int,
254
+ fps: float,
255
+ mass_kg: float | None,
256
+ ) -> float | None:
257
+ """Calculate mean power during concentric phase using work-energy theorem.
258
+
259
+ Formula: Mean Power (W) = (mass × g × jump_height) / concentric_duration
260
+
261
+ This represents the true mean power output during the concentric phase.
262
+ Typical mean-to-peak power ratio: 60-75%.
263
+
264
+ Args:
265
+ positions: 1D array of vertical positions
266
+ velocities: 1D array of vertical velocities
267
+ concentric_start: Frame index where concentric phase begins
268
+ takeoff_frame: Frame index where takeoff occurs
269
+ fps: Video frames per second
270
+ mass_kg: Athlete mass in kilograms
271
+
272
+ Returns:
273
+ Mean power in Watts, or None if mass is not provided
274
+ """
275
+ if mass_kg is None:
276
+ return None
277
+
278
+ # Calculate concentric duration
279
+ concentric_duration = (takeoff_frame - concentric_start) / fps
280
+
281
+ if concentric_duration <= 0:
282
+ return None
283
+
284
+ # Calculate takeoff velocity (negative = upward in normalized coords)
285
+ if takeoff_frame > concentric_start:
286
+ takeoff_velocity = np.min(velocities[concentric_start:takeoff_frame])
287
+ else:
288
+ takeoff_velocity = 0.0
289
+
290
+ # Calculate jump height from takeoff velocity: h = v² / (2g)
291
+ g = 9.81
292
+ jump_height_m = (takeoff_velocity**2) / (2 * g)
293
+
294
+ # Work-energy theorem: Mean Power = Work / Time
295
+ mean_power = (mass_kg * g * jump_height_m) / concentric_duration
296
+
297
+ return float(mean_power)
298
+
299
+
300
+ def calculate_peak_force(
301
+ positions: FloatArray,
302
+ velocities: FloatArray,
303
+ concentric_start: int,
304
+ takeoff_frame: int,
305
+ fps: float,
306
+ mass_kg: float | None,
307
+ ) -> float | None:
308
+ """Calculate peak force during concentric phase.
309
+
310
+ Args:
311
+ positions: 1D array of vertical positions
312
+ velocities: 1D array of vertical velocities
313
+ concentric_start: Frame index where concentric phase begins
314
+ takeoff_frame: Frame index where takeoff occurs
315
+ fps: Video frames per second
316
+ mass_kg: Athlete mass in kilograms
317
+
318
+ Returns:
319
+ Peak force in Newtons, or None if mass is not provided
320
+ """
321
+ if mass_kg is None:
322
+ return None
323
+
324
+ # Calculate concentric duration
325
+ concentric_duration = (takeoff_frame - concentric_start) / fps
326
+
327
+ if concentric_duration <= 0:
328
+ return None
329
+
330
+ # Calculate takeoff velocity (negative = upward in normalized coords)
331
+ if takeoff_frame > concentric_start:
332
+ takeoff_velocity = np.min(velocities[concentric_start:takeoff_frame])
333
+ else:
334
+ takeoff_velocity = 0.0
335
+
336
+ # Calculate average acceleration: a = v / t
337
+ # Use absolute value since we want magnitude of upward acceleration
338
+ g = 9.81
339
+ avg_acceleration = np.abs(takeoff_velocity) / concentric_duration
340
+
341
+ # Average force: F = ma + mg (overcoming gravity + accelerating)
342
+ avg_force = mass_kg * (avg_acceleration + g)
343
+
344
+ # Peak force is typically 1.2-1.5× average force
345
+ # Use 1.3 as validated in biomechanics literature
346
+ peak_force = 1.3 * avg_force
347
+
348
+ return float(peak_force)