kinemotion 0.10.6__py3-none-any.whl → 0.67.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.
Potentially problematic release.
This version of kinemotion might be problematic. Click here for more details.
- kinemotion/__init__.py +31 -6
- kinemotion/api.py +39 -598
- kinemotion/cli.py +2 -0
- kinemotion/cmj/__init__.py +5 -0
- kinemotion/cmj/analysis.py +621 -0
- kinemotion/cmj/api.py +563 -0
- kinemotion/cmj/cli.py +324 -0
- kinemotion/cmj/debug_overlay.py +457 -0
- kinemotion/cmj/joint_angles.py +307 -0
- kinemotion/cmj/kinematics.py +360 -0
- kinemotion/cmj/metrics_validator.py +767 -0
- kinemotion/cmj/validation_bounds.py +341 -0
- kinemotion/core/__init__.py +28 -0
- kinemotion/core/auto_tuning.py +71 -37
- kinemotion/core/cli_utils.py +60 -0
- kinemotion/core/debug_overlay_utils.py +385 -0
- kinemotion/core/determinism.py +83 -0
- kinemotion/core/experimental.py +103 -0
- kinemotion/core/filtering.py +9 -6
- kinemotion/core/formatting.py +75 -0
- kinemotion/core/metadata.py +231 -0
- kinemotion/core/model_downloader.py +172 -0
- kinemotion/core/pipeline_utils.py +433 -0
- kinemotion/core/pose.py +298 -141
- kinemotion/core/pose_landmarks.py +67 -0
- kinemotion/core/quality.py +393 -0
- kinemotion/core/smoothing.py +250 -154
- kinemotion/core/timing.py +247 -0
- kinemotion/core/types.py +42 -0
- kinemotion/core/validation.py +201 -0
- kinemotion/core/video_io.py +135 -50
- kinemotion/dropjump/__init__.py +1 -1
- kinemotion/dropjump/analysis.py +367 -182
- kinemotion/dropjump/api.py +665 -0
- kinemotion/dropjump/cli.py +156 -466
- kinemotion/dropjump/debug_overlay.py +136 -206
- kinemotion/dropjump/kinematics.py +232 -255
- kinemotion/dropjump/metrics_validator.py +240 -0
- kinemotion/dropjump/validation_bounds.py +157 -0
- kinemotion/models/__init__.py +0 -0
- kinemotion/models/pose_landmarker_lite.task +0 -0
- kinemotion-0.67.0.dist-info/METADATA +726 -0
- kinemotion-0.67.0.dist-info/RECORD +47 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/WHEEL +1 -1
- kinemotion-0.10.6.dist-info/METADATA +0 -561
- kinemotion-0.10.6.dist-info/RECORD +0 -20
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.10.6.dist-info → kinemotion-0.67.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Drop jump metrics validation using physiological bounds.
|
|
2
|
+
|
|
3
|
+
Comprehensive validation of Drop Jump metrics against biomechanical bounds,
|
|
4
|
+
consistency checks, and cross-validation of RSI calculation.
|
|
5
|
+
|
|
6
|
+
Provides severity levels (ERROR, WARNING, INFO) for different categories
|
|
7
|
+
of metric issues.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from kinemotion.core.types import MetricsDict
|
|
13
|
+
from kinemotion.core.validation import (
|
|
14
|
+
MetricsValidator,
|
|
15
|
+
ValidationResult,
|
|
16
|
+
)
|
|
17
|
+
from kinemotion.dropjump.validation_bounds import (
|
|
18
|
+
DropJumpBounds,
|
|
19
|
+
estimate_athlete_profile,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DropJumpValidationResult(ValidationResult):
|
|
25
|
+
"""Drop jump-specific validation result."""
|
|
26
|
+
|
|
27
|
+
rsi: float | None = None
|
|
28
|
+
contact_flight_ratio: float | None = None
|
|
29
|
+
height_kinematic_trajectory_consistency: float | None = None # % error
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
"""Convert validation result to JSON-serializable dictionary.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dictionary with status, issues, and consistency metrics.
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
"status": self.status,
|
|
39
|
+
"issues": [
|
|
40
|
+
{
|
|
41
|
+
"severity": issue.severity.value,
|
|
42
|
+
"metric": issue.metric,
|
|
43
|
+
"message": issue.message,
|
|
44
|
+
"value": issue.value,
|
|
45
|
+
"bounds": issue.bounds,
|
|
46
|
+
}
|
|
47
|
+
for issue in self.issues
|
|
48
|
+
],
|
|
49
|
+
"athlete_profile": (self.athlete_profile.value if self.athlete_profile else None),
|
|
50
|
+
"rsi": self.rsi,
|
|
51
|
+
"contact_flight_ratio": self.contact_flight_ratio,
|
|
52
|
+
"height_kinematic_trajectory_consistency_percent": (
|
|
53
|
+
self.height_kinematic_trajectory_consistency
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DropJumpMetricsValidator(MetricsValidator):
|
|
59
|
+
"""Comprehensive drop jump metrics validator."""
|
|
60
|
+
|
|
61
|
+
def validate(self, metrics: MetricsDict) -> DropJumpValidationResult:
|
|
62
|
+
"""Validate drop jump metrics comprehensively.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
metrics: Dictionary with drop jump metric values
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
DropJumpValidationResult with all issues and status
|
|
69
|
+
"""
|
|
70
|
+
result = DropJumpValidationResult()
|
|
71
|
+
|
|
72
|
+
# Estimate athlete profile if not provided
|
|
73
|
+
if self.assumed_profile:
|
|
74
|
+
result.athlete_profile = self.assumed_profile
|
|
75
|
+
else:
|
|
76
|
+
result.athlete_profile = estimate_athlete_profile(metrics)
|
|
77
|
+
|
|
78
|
+
# Extract metric values (handle nested "data" structure)
|
|
79
|
+
data = metrics.get("data", metrics) # Support both structures
|
|
80
|
+
|
|
81
|
+
contact_time_ms = data.get("ground_contact_time_ms")
|
|
82
|
+
flight_time_ms = data.get("flight_time_ms")
|
|
83
|
+
jump_height_m = data.get("jump_height_m")
|
|
84
|
+
jump_height_kinematic_m = data.get("jump_height_kinematic_m")
|
|
85
|
+
jump_height_trajectory_m = data.get("jump_height_trajectory_m")
|
|
86
|
+
|
|
87
|
+
# Validate individual metrics
|
|
88
|
+
if contact_time_ms is not None:
|
|
89
|
+
self._check_contact_time(contact_time_ms, result)
|
|
90
|
+
|
|
91
|
+
if flight_time_ms is not None:
|
|
92
|
+
self._check_flight_time(flight_time_ms, result)
|
|
93
|
+
|
|
94
|
+
if jump_height_m is not None:
|
|
95
|
+
self._check_jump_height(jump_height_m, result)
|
|
96
|
+
|
|
97
|
+
# Cross-validation
|
|
98
|
+
if contact_time_ms is not None and flight_time_ms is not None:
|
|
99
|
+
self._check_rsi(contact_time_ms, flight_time_ms, result)
|
|
100
|
+
|
|
101
|
+
# Dual height validation (kinematic vs trajectory)
|
|
102
|
+
if jump_height_kinematic_m is not None and jump_height_trajectory_m is not None:
|
|
103
|
+
self._check_dual_height_consistency(
|
|
104
|
+
jump_height_kinematic_m, jump_height_trajectory_m, result
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Finalize status
|
|
108
|
+
result.finalize_status()
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def _check_contact_time(
|
|
113
|
+
self, contact_time_ms: float, result: DropJumpValidationResult
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Validate contact time."""
|
|
116
|
+
contact_time_s = contact_time_ms / 1000.0
|
|
117
|
+
bounds = DropJumpBounds.CONTACT_TIME
|
|
118
|
+
|
|
119
|
+
if not bounds.is_physically_possible(contact_time_s):
|
|
120
|
+
result.add_error(
|
|
121
|
+
"contact_time",
|
|
122
|
+
f"Contact time {contact_time_s:.3f}s physically impossible",
|
|
123
|
+
value=contact_time_s,
|
|
124
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
125
|
+
)
|
|
126
|
+
elif result.athlete_profile and not bounds.contains(
|
|
127
|
+
contact_time_s, result.athlete_profile
|
|
128
|
+
):
|
|
129
|
+
profile_name = result.athlete_profile.value
|
|
130
|
+
result.add_warning(
|
|
131
|
+
"contact_time",
|
|
132
|
+
f"Contact time {contact_time_s:.3f}s unusual for {profile_name} athlete",
|
|
133
|
+
value=contact_time_s,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _check_flight_time(self, flight_time_ms: float, result: DropJumpValidationResult) -> None:
|
|
137
|
+
"""Validate flight time."""
|
|
138
|
+
flight_time_s = flight_time_ms / 1000.0
|
|
139
|
+
bounds = DropJumpBounds.FLIGHT_TIME
|
|
140
|
+
|
|
141
|
+
if not bounds.is_physically_possible(flight_time_s):
|
|
142
|
+
result.add_error(
|
|
143
|
+
"flight_time",
|
|
144
|
+
f"Flight time {flight_time_s:.3f}s physically impossible",
|
|
145
|
+
value=flight_time_s,
|
|
146
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
147
|
+
)
|
|
148
|
+
elif result.athlete_profile and not bounds.contains(flight_time_s, result.athlete_profile):
|
|
149
|
+
profile_name = result.athlete_profile.value
|
|
150
|
+
result.add_warning(
|
|
151
|
+
"flight_time",
|
|
152
|
+
f"Flight time {flight_time_s:.3f}s unusual for {profile_name} athlete",
|
|
153
|
+
value=flight_time_s,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _check_jump_height(self, jump_height_m: float, result: DropJumpValidationResult) -> None:
|
|
157
|
+
"""Validate jump height."""
|
|
158
|
+
bounds = DropJumpBounds.JUMP_HEIGHT
|
|
159
|
+
|
|
160
|
+
if not bounds.is_physically_possible(jump_height_m):
|
|
161
|
+
result.add_error(
|
|
162
|
+
"jump_height",
|
|
163
|
+
f"Jump height {jump_height_m:.3f}m physically impossible",
|
|
164
|
+
value=jump_height_m,
|
|
165
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
166
|
+
)
|
|
167
|
+
elif result.athlete_profile and not bounds.contains(jump_height_m, result.athlete_profile):
|
|
168
|
+
profile_name = result.athlete_profile.value
|
|
169
|
+
result.add_warning(
|
|
170
|
+
"jump_height",
|
|
171
|
+
f"Jump height {jump_height_m:.3f}m unusual for {profile_name} athlete",
|
|
172
|
+
value=jump_height_m,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _check_rsi(
|
|
176
|
+
self,
|
|
177
|
+
contact_time_ms: float,
|
|
178
|
+
flight_time_ms: float,
|
|
179
|
+
result: DropJumpValidationResult,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Validate RSI and cross-check consistency."""
|
|
182
|
+
contact_time_s = contact_time_ms / 1000.0
|
|
183
|
+
flight_time_s = flight_time_ms / 1000.0
|
|
184
|
+
|
|
185
|
+
if contact_time_s > 0 and flight_time_s > 0:
|
|
186
|
+
rsi = flight_time_s / contact_time_s
|
|
187
|
+
result.rsi = rsi
|
|
188
|
+
result.contact_flight_ratio = contact_time_s / flight_time_s
|
|
189
|
+
|
|
190
|
+
bounds = DropJumpBounds.RSI
|
|
191
|
+
|
|
192
|
+
if not bounds.is_physically_possible(rsi):
|
|
193
|
+
result.add_error(
|
|
194
|
+
"rsi",
|
|
195
|
+
f"RSI {rsi:.2f} physically impossible",
|
|
196
|
+
value=rsi,
|
|
197
|
+
bounds=(bounds.absolute_min, bounds.absolute_max),
|
|
198
|
+
)
|
|
199
|
+
elif result.athlete_profile and not bounds.contains(rsi, result.athlete_profile):
|
|
200
|
+
result.add_warning(
|
|
201
|
+
"rsi",
|
|
202
|
+
f"RSI {rsi:.2f} unusual for {result.athlete_profile.value} athlete",
|
|
203
|
+
value=rsi,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _check_dual_height_consistency(
|
|
207
|
+
self,
|
|
208
|
+
jump_height_kinematic_m: float,
|
|
209
|
+
jump_height_trajectory_m: float,
|
|
210
|
+
result: DropJumpValidationResult,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Validate consistency between kinematic and trajectory-based heights.
|
|
213
|
+
|
|
214
|
+
Kinematic height (h = g*t²/8) comes from flight time (objective).
|
|
215
|
+
Trajectory height comes from position tracking (subject to landmark
|
|
216
|
+
detection noise).
|
|
217
|
+
|
|
218
|
+
Expected correlation: r > 0.95, absolute difference < 5% for quality video.
|
|
219
|
+
"""
|
|
220
|
+
if jump_height_kinematic_m <= 0 or jump_height_trajectory_m <= 0:
|
|
221
|
+
return # Skip if either value is missing or invalid
|
|
222
|
+
|
|
223
|
+
# Calculate percentage difference
|
|
224
|
+
avg_height = (jump_height_kinematic_m + jump_height_trajectory_m) / 2.0
|
|
225
|
+
if avg_height > 0:
|
|
226
|
+
abs_diff = abs(jump_height_kinematic_m - jump_height_trajectory_m)
|
|
227
|
+
percent_error = (abs_diff / avg_height) * 100.0
|
|
228
|
+
result.height_kinematic_trajectory_consistency = percent_error
|
|
229
|
+
|
|
230
|
+
# Allow 10% tolerance for typical video processing noise
|
|
231
|
+
if percent_error > 10.0:
|
|
232
|
+
result.add_warning(
|
|
233
|
+
"height_consistency",
|
|
234
|
+
f"Kinematic ({jump_height_kinematic_m:.3f}m) and trajectory "
|
|
235
|
+
f"({jump_height_trajectory_m:.3f}m) heights differ by "
|
|
236
|
+
f"{percent_error:.1f}%. May indicate landmark detection "
|
|
237
|
+
"issues or video quality problems.",
|
|
238
|
+
value=percent_error,
|
|
239
|
+
bounds=(0, 10),
|
|
240
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Drop jump metrics physiological bounds for validation testing.
|
|
2
|
+
|
|
3
|
+
This module defines realistic physiological bounds for Drop Jump metrics
|
|
4
|
+
based on biomechanical literature and real-world athlete performance.
|
|
5
|
+
|
|
6
|
+
Drop jump metrics differ from CMJ:
|
|
7
|
+
- Contact time (ground interaction during landing)
|
|
8
|
+
- Flight time (time in air after landing)
|
|
9
|
+
- RSI (Reactive Strength Index) = flight_time / contact_time
|
|
10
|
+
- Jump height (calculated from flight time)
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- Komi & Bosco (1978): Drop jump RSI and elastic properties
|
|
14
|
+
- Flanagan & Comyns (2008): RSI reliability and athlete assessment
|
|
15
|
+
- Covens et al. (2019): Drop jump kinetics across athletes
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from kinemotion.core.types import MetricsDict
|
|
19
|
+
from kinemotion.core.validation import AthleteProfile, MetricBounds
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DropJumpBounds:
|
|
23
|
+
"""Collection of physiological bounds for all drop jump metrics."""
|
|
24
|
+
|
|
25
|
+
# GROUND CONTACT TIME (seconds, landing interaction)
|
|
26
|
+
CONTACT_TIME = MetricBounds(
|
|
27
|
+
absolute_min=0.08, # Physiological minimum: neural delay + deceleration
|
|
28
|
+
practical_min=0.15, # Extreme plyometric
|
|
29
|
+
recreational_min=0.35, # Typical landing
|
|
30
|
+
recreational_max=0.70, # Slower absorption
|
|
31
|
+
elite_min=0.20,
|
|
32
|
+
elite_max=0.50,
|
|
33
|
+
absolute_max=1.50,
|
|
34
|
+
unit="s",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# FLIGHT TIME (seconds, after landing)
|
|
38
|
+
FLIGHT_TIME = MetricBounds(
|
|
39
|
+
absolute_min=0.30,
|
|
40
|
+
practical_min=0.40, # Minimal jump
|
|
41
|
+
recreational_min=0.50,
|
|
42
|
+
recreational_max=0.85,
|
|
43
|
+
elite_min=0.65,
|
|
44
|
+
elite_max=1.10,
|
|
45
|
+
absolute_max=1.40,
|
|
46
|
+
unit="s",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# JUMP HEIGHT (meters, calculated from flight time)
|
|
50
|
+
JUMP_HEIGHT = MetricBounds(
|
|
51
|
+
absolute_min=0.05,
|
|
52
|
+
practical_min=0.10,
|
|
53
|
+
recreational_min=0.25,
|
|
54
|
+
recreational_max=0.65,
|
|
55
|
+
elite_min=0.50,
|
|
56
|
+
elite_max=1.00,
|
|
57
|
+
absolute_max=1.30,
|
|
58
|
+
unit="m",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# REACTIVE STRENGTH INDEX (RSI) = flight_time / contact_time (ratio, no unit)
|
|
62
|
+
RSI = MetricBounds(
|
|
63
|
+
absolute_min=0.30, # Very poor reactive ability
|
|
64
|
+
practical_min=0.50,
|
|
65
|
+
recreational_min=0.70,
|
|
66
|
+
recreational_max=1.80,
|
|
67
|
+
elite_min=1.50, # Elite: fast contact, long flight
|
|
68
|
+
elite_max=3.50,
|
|
69
|
+
absolute_max=5.00,
|
|
70
|
+
unit="ratio",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _score_jump_height(jump_height: float) -> float:
|
|
75
|
+
"""Convert jump height to athlete profile score (0-4).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
jump_height: Jump height in meters
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Score from 0 (elderly) to 4 (elite)
|
|
82
|
+
"""
|
|
83
|
+
thresholds = [(0.25, 0), (0.35, 1), (0.50, 2), (0.70, 3)]
|
|
84
|
+
for threshold, score in thresholds:
|
|
85
|
+
if jump_height < threshold:
|
|
86
|
+
return float(score)
|
|
87
|
+
return 4.0 # Elite
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _score_contact_time(contact_time_s: float) -> float:
|
|
91
|
+
"""Convert contact time to athlete profile score (0-4).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
contact_time_s: Ground contact time in seconds
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Score from 0 (elderly) to 4 (elite)
|
|
98
|
+
"""
|
|
99
|
+
thresholds = [(0.60, 0), (0.50, 1), (0.45, 2), (0.40, 3)]
|
|
100
|
+
for threshold, score in thresholds:
|
|
101
|
+
if contact_time_s > threshold:
|
|
102
|
+
return float(score)
|
|
103
|
+
return 4.0 # Elite
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _classify_combined_score(combined_score: float) -> AthleteProfile:
|
|
107
|
+
"""Classify combined score into athlete profile.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
combined_score: Weighted score from height and contact time
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Athlete profile classification
|
|
114
|
+
"""
|
|
115
|
+
thresholds = [
|
|
116
|
+
(1.0, AthleteProfile.ELDERLY),
|
|
117
|
+
(1.7, AthleteProfile.UNTRAINED),
|
|
118
|
+
(2.7, AthleteProfile.RECREATIONAL),
|
|
119
|
+
(3.7, AthleteProfile.TRAINED),
|
|
120
|
+
]
|
|
121
|
+
for threshold, profile in thresholds:
|
|
122
|
+
if combined_score < threshold:
|
|
123
|
+
return profile
|
|
124
|
+
return AthleteProfile.ELITE
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def estimate_athlete_profile(metrics: MetricsDict, gender: str | None = None) -> AthleteProfile:
|
|
128
|
+
"""Estimate athlete profile from drop jump metrics.
|
|
129
|
+
|
|
130
|
+
Uses jump_height and contact_time to classify athlete level.
|
|
131
|
+
|
|
132
|
+
NOTE: Bounds are calibrated for adult males. Female athletes typically achieve
|
|
133
|
+
60-70% of male heights due to lower muscle mass and strength. If analyzing
|
|
134
|
+
female athletes, interpret results one level lower than classification suggests.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
metrics: Dictionary with drop jump metric values
|
|
138
|
+
gender: Optional gender for context ("M"/"F"). Currently informational only.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Estimated AthleteProfile (ELDERLY, UNTRAINED, RECREATIONAL, TRAINED, or ELITE)
|
|
142
|
+
"""
|
|
143
|
+
jump_height = metrics.get("data", {}).get("jump_height_m")
|
|
144
|
+
contact_time = metrics.get("data", {}).get("ground_contact_time_ms")
|
|
145
|
+
|
|
146
|
+
if jump_height is None or contact_time is None:
|
|
147
|
+
return AthleteProfile.RECREATIONAL
|
|
148
|
+
|
|
149
|
+
contact_time_s = contact_time / 1000.0
|
|
150
|
+
|
|
151
|
+
# Calculate weighted combination: height (70%) + contact time (30%)
|
|
152
|
+
# Height is more reliable indicator across populations
|
|
153
|
+
height_score = _score_jump_height(jump_height)
|
|
154
|
+
contact_score = _score_contact_time(contact_time_s)
|
|
155
|
+
combined_score = (height_score * 0.70) + (contact_score * 0.30)
|
|
156
|
+
|
|
157
|
+
return _classify_combined_score(combined_score)
|
|
File without changes
|
|
Binary file
|