kinemotion 0.11.0__tar.gz → 0.11.1__tar.gz

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.

Files changed (69) hide show
  1. {kinemotion-0.11.0 → kinemotion-0.11.1}/CHANGELOG.md +8 -0
  2. {kinemotion-0.11.0 → kinemotion-0.11.1}/PKG-INFO +1 -1
  3. {kinemotion-0.11.0 → kinemotion-0.11.1}/pyproject.toml +1 -1
  4. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/api.py +0 -5
  5. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/analysis.py +72 -88
  6. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/cli.py +0 -5
  7. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_cmj_analysis.py +4 -16
  8. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_cmj_kinematics.py +8 -7
  9. {kinemotion-0.11.0 → kinemotion-0.11.1}/uv.lock +1 -1
  10. {kinemotion-0.11.0 → kinemotion-0.11.1}/.dockerignore +0 -0
  11. {kinemotion-0.11.0 → kinemotion-0.11.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {kinemotion-0.11.0 → kinemotion-0.11.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  13. {kinemotion-0.11.0 → kinemotion-0.11.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  14. {kinemotion-0.11.0 → kinemotion-0.11.1}/.github/pull_request_template.md +0 -0
  15. {kinemotion-0.11.0 → kinemotion-0.11.1}/.github/workflows/release.yml +0 -0
  16. {kinemotion-0.11.0 → kinemotion-0.11.1}/.gitignore +0 -0
  17. {kinemotion-0.11.0 → kinemotion-0.11.1}/.pre-commit-config.yaml +0 -0
  18. {kinemotion-0.11.0 → kinemotion-0.11.1}/.tool-versions +0 -0
  19. {kinemotion-0.11.0 → kinemotion-0.11.1}/CLAUDE.md +0 -0
  20. {kinemotion-0.11.0 → kinemotion-0.11.1}/CODE_OF_CONDUCT.md +0 -0
  21. {kinemotion-0.11.0 → kinemotion-0.11.1}/CONTRIBUTING.md +0 -0
  22. {kinemotion-0.11.0 → kinemotion-0.11.1}/Dockerfile +0 -0
  23. {kinemotion-0.11.0 → kinemotion-0.11.1}/GEMINI.md +0 -0
  24. {kinemotion-0.11.0 → kinemotion-0.11.1}/LICENSE +0 -0
  25. {kinemotion-0.11.0 → kinemotion-0.11.1}/README.md +0 -0
  26. {kinemotion-0.11.0 → kinemotion-0.11.1}/SECURITY.md +0 -0
  27. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/BULK_PROCESSING.md +0 -0
  28. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/CAMERA_SETUP.md +0 -0
  29. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/CAMERA_SETUP_ES.md +0 -0
  30. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/CMJ_GUIDE.md +0 -0
  31. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/ERRORS_FINDINGS.md +0 -0
  32. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/FRAMERATE.md +0 -0
  33. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/IMU_METADATA_PRESERVATION.md +0 -0
  34. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/PARAMETERS.md +0 -0
  35. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/REAL_TIME_ANALYSIS.md +0 -0
  36. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/TRIPLE_EXTENSION.md +0 -0
  37. {kinemotion-0.11.0 → kinemotion-0.11.1}/docs/VALIDATION_PLAN.md +0 -0
  38. {kinemotion-0.11.0 → kinemotion-0.11.1}/examples/bulk/README.md +0 -0
  39. {kinemotion-0.11.0 → kinemotion-0.11.1}/examples/bulk/bulk_processing.py +0 -0
  40. {kinemotion-0.11.0 → kinemotion-0.11.1}/examples/bulk/simple_example.py +0 -0
  41. {kinemotion-0.11.0 → kinemotion-0.11.1}/examples/programmatic_usage.py +0 -0
  42. {kinemotion-0.11.0 → kinemotion-0.11.1}/samples/cmjs/README.md +0 -0
  43. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/__init__.py +0 -0
  44. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cli.py +0 -0
  45. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/__init__.py +0 -0
  46. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/debug_overlay.py +0 -0
  47. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/joint_angles.py +0 -0
  48. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/cmj/kinematics.py +0 -0
  49. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/__init__.py +0 -0
  50. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/auto_tuning.py +0 -0
  51. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/filtering.py +0 -0
  52. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/pose.py +0 -0
  53. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/smoothing.py +0 -0
  54. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/core/video_io.py +0 -0
  55. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/dropjump/__init__.py +0 -0
  56. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/dropjump/analysis.py +0 -0
  57. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/dropjump/cli.py +0 -0
  58. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/dropjump/debug_overlay.py +0 -0
  59. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/dropjump/kinematics.py +0 -0
  60. {kinemotion-0.11.0 → kinemotion-0.11.1}/src/kinemotion/py.typed +0 -0
  61. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/__init__.py +0 -0
  62. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_adaptive_threshold.py +0 -0
  63. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_api.py +0 -0
  64. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_aspect_ratio.py +0 -0
  65. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_com_estimation.py +0 -0
  66. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_contact_detection.py +0 -0
  67. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_filtering.py +0 -0
  68. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_kinematics.py +0 -0
  69. {kinemotion-0.11.0 → kinemotion-0.11.1}/tests/test_polyorder.py +0 -0
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v0.11.1 (2025-11-06)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **cmj**: Remove unused parameters and fix code quality issues
15
+ ([`72a1e43`](https://github.com/feniix/kinemotion/commit/72a1e43ec107e5b1c132efb10a08a09ea2864ae4))
16
+
17
+
10
18
  ## v0.11.0 (2025-11-06)
11
19
 
12
20
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemotion
3
- Version: 0.11.0
3
+ Version: 0.11.1
4
4
  Summary: Video-based kinematic analysis for athletic performance
5
5
  Project-URL: Homepage, https://github.com/feniix/kinemotion
6
6
  Project-URL: Repository, https://github.com/feniix/kinemotion
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kinemotion"
3
- version = "0.11.0"
3
+ version = "0.11.1"
4
4
  description = "Video-based kinematic analysis for athletic performance"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.13"
@@ -754,11 +754,6 @@ def process_cmj_video(
754
754
  phases = detect_cmj_phases(
755
755
  vertical_positions,
756
756
  video.fps,
757
- velocity_threshold=params.velocity_threshold,
758
- countermovement_threshold=cm_threshold,
759
- min_contact_frames=params.min_contact_frames,
760
- min_eccentric_frames=params.min_contact_frames,
761
- use_curvature=params.use_curvature,
762
757
  window_length=params.smoothing_window,
763
758
  polyorder=params.polyorder,
764
759
  )
@@ -102,7 +102,6 @@ def find_standing_phase(
102
102
 
103
103
  def find_countermovement_start(
104
104
  velocities: np.ndarray,
105
- fps: float,
106
105
  countermovement_threshold: float = 0.015,
107
106
  min_eccentric_frames: int = 3,
108
107
  standing_start: int | None = None,
@@ -114,7 +113,6 @@ def find_countermovement_start(
114
113
 
115
114
  Args:
116
115
  velocities: Array of SIGNED vertical velocities
117
- fps: Video frame rate
118
116
  countermovement_threshold: Velocity threshold for detecting downward motion (POSITIVE)
119
117
  min_eccentric_frames: Minimum consecutive frames of downward motion
120
118
  standing_start: Optional frame where standing phase ended
@@ -143,7 +141,6 @@ def find_countermovement_start(
143
141
  def find_lowest_point(
144
142
  positions: np.ndarray,
145
143
  velocities: np.ndarray,
146
- eccentric_start: int | None = None,
147
144
  min_search_frame: int = 80,
148
145
  ) -> int:
149
146
  """
@@ -155,7 +152,6 @@ def find_lowest_point(
155
152
  Args:
156
153
  positions: Array of vertical positions (higher value = lower in video)
157
154
  velocities: Array of SIGNED vertical velocities (positive=down, negative=up)
158
- eccentric_start: Optional frame where eccentric phase started
159
155
  min_search_frame: Minimum frame to start searching (default: frame 80)
160
156
 
161
157
  Returns:
@@ -381,9 +377,6 @@ def find_interpolated_takeoff_landing(
381
377
  positions: np.ndarray,
382
378
  velocities: np.ndarray,
383
379
  lowest_point_frame: int,
384
- velocity_threshold: float = 0.02,
385
- min_flight_frames: int = 3,
386
- use_curvature: bool = True,
387
380
  window_length: int = 5,
388
381
  polyorder: int = 2,
389
382
  ) -> tuple[float, float] | None:
@@ -397,9 +390,6 @@ def find_interpolated_takeoff_landing(
397
390
  positions: Array of vertical positions
398
391
  velocities: Array of vertical velocities
399
392
  lowest_point_frame: Frame at lowest point
400
- velocity_threshold: Velocity threshold (unused for CMJ, kept for API compatibility)
401
- min_flight_frames: Minimum consecutive frames for valid flight phase
402
- use_curvature: Whether to use trajectory curvature refinement
403
393
  window_length: Window size for derivative calculations
404
394
  polyorder: Polynomial order for Savitzky-Golay filter
405
395
 
@@ -428,14 +418,76 @@ def find_interpolated_takeoff_landing(
428
418
  return (takeoff_frame, landing_frame)
429
419
 
430
420
 
421
+ def _find_takeoff_frame(
422
+ velocities: np.ndarray, peak_height_frame: int, fps: float
423
+ ) -> float:
424
+ """Find takeoff frame as peak upward velocity before peak height."""
425
+ takeoff_search_start = max(0, peak_height_frame - int(fps * 0.35))
426
+ takeoff_search_end = peak_height_frame - 2
427
+
428
+ takeoff_velocities = velocities[takeoff_search_start:takeoff_search_end]
429
+
430
+ if len(takeoff_velocities) > 0:
431
+ peak_vel_idx = int(np.argmin(takeoff_velocities))
432
+ return float(takeoff_search_start + peak_vel_idx)
433
+ else:
434
+ return float(peak_height_frame - int(fps * 0.3))
435
+
436
+
437
+ def _find_lowest_frame(
438
+ velocities: np.ndarray, positions: np.ndarray, takeoff_frame: float, fps: float
439
+ ) -> float:
440
+ """Find lowest point frame before takeoff."""
441
+ lowest_search_start = max(0, int(takeoff_frame) - int(fps * 0.4))
442
+ lowest_search_end = int(takeoff_frame)
443
+
444
+ # Find where velocity crosses from positive to negative
445
+ for i in range(lowest_search_end - 1, lowest_search_start, -1):
446
+ if i > 0 and velocities[i] < 0 and velocities[i - 1] >= 0:
447
+ return float(i)
448
+
449
+ # Fallback: use maximum position
450
+ lowest_positions = positions[lowest_search_start:lowest_search_end]
451
+ if len(lowest_positions) > 0:
452
+ lowest_idx = int(np.argmax(lowest_positions))
453
+ return float(lowest_search_start + lowest_idx)
454
+ else:
455
+ return float(int(takeoff_frame) - int(fps * 0.2))
456
+
457
+
458
+ def _find_landing_frame(
459
+ accelerations: np.ndarray, peak_height_frame: int, fps: float
460
+ ) -> float:
461
+ """Find landing frame after peak height."""
462
+ landing_search_start = peak_height_frame
463
+ landing_search_end = min(len(accelerations), peak_height_frame + int(fps * 0.5))
464
+ landing_accelerations = accelerations[landing_search_start:landing_search_end]
465
+
466
+ if len(landing_accelerations) > 0:
467
+ landing_idx = int(np.argmin(landing_accelerations))
468
+ return float(landing_search_start + landing_idx)
469
+ else:
470
+ return float(peak_height_frame + int(fps * 0.3))
471
+
472
+
473
+ def _find_standing_end(velocities: np.ndarray, lowest_point: float) -> float | None:
474
+ """Find end of standing phase before lowest point."""
475
+ if lowest_point <= 20:
476
+ return None
477
+
478
+ standing_search = velocities[: int(lowest_point)]
479
+ low_vel = np.abs(standing_search) < 0.005
480
+ if np.any(low_vel):
481
+ standing_frames = np.nonzero(low_vel)[0]
482
+ if len(standing_frames) > 10:
483
+ return float(standing_frames[-1])
484
+
485
+ return None
486
+
487
+
431
488
  def detect_cmj_phases(
432
489
  positions: np.ndarray,
433
490
  fps: float,
434
- velocity_threshold: float = 0.02,
435
- countermovement_threshold: float = -0.015,
436
- min_contact_frames: int = 3,
437
- min_eccentric_frames: int = 3,
438
- use_curvature: bool = True,
439
491
  window_length: int = 5,
440
492
  polyorder: int = 2,
441
493
  ) -> tuple[float | None, float, float, float] | None:
@@ -451,11 +503,6 @@ def detect_cmj_phases(
451
503
  Args:
452
504
  positions: Array of vertical positions (normalized 0-1)
453
505
  fps: Video frame rate
454
- velocity_threshold: Velocity threshold (not used)
455
- countermovement_threshold: Velocity threshold (not used)
456
- min_contact_frames: Minimum frames for ground contact
457
- min_eccentric_frames: Minimum frames for eccentric phase
458
- use_curvature: Whether to use trajectory curvature refinement
459
506
  window_length: Window size for derivative calculations
460
507
  polyorder: Polynomial order for Savitzky-Golay filter
461
508
 
@@ -473,76 +520,13 @@ def detect_cmj_phases(
473
520
 
474
521
  # Step 1: Find peak height (global minimum y = highest point in frame)
475
522
  peak_height_frame = int(np.argmin(positions))
476
-
477
523
  if peak_height_frame < 10:
478
524
  return None # Peak too early, invalid
479
525
 
480
- # Step 2: Find takeoff as peak upward velocity
481
- # Takeoff occurs at maximum upward velocity (most negative) before peak height
482
- # Typical: 0.3 seconds before peak (9 frames at 30fps)
483
- takeoff_search_start = max(0, peak_height_frame - int(fps * 0.35))
484
- takeoff_search_end = peak_height_frame - 2 # Must be at least 2 frames before peak
485
-
486
- takeoff_velocities = velocities[takeoff_search_start:takeoff_search_end]
487
-
488
- if len(takeoff_velocities) > 0:
489
- # Takeoff = peak upward velocity (most negative)
490
- peak_vel_idx = int(np.argmin(takeoff_velocities))
491
- takeoff_frame = float(takeoff_search_start + peak_vel_idx)
492
- else:
493
- # Fallback
494
- takeoff_frame = float(peak_height_frame - int(fps * 0.3))
495
-
496
- # Step 3: Find lowest point (countermovement bottom) before takeoff
497
- # This is where velocity crosses from positive (squatting) to negative (jumping)
498
- # Search backward from takeoff for where velocity was last positive/zero
499
- lowest_search_start = max(0, int(takeoff_frame) - int(fps * 0.4))
500
- lowest_search_end = int(takeoff_frame)
501
-
502
- # Find where velocity crosses from positive to negative (transition point)
503
- lowest_frame_found = None
504
- for i in range(lowest_search_end - 1, lowest_search_start, -1):
505
- if i > 0:
506
- # Look for velocity crossing from positive/zero to negative
507
- if velocities[i] < 0 and velocities[i - 1] >= 0:
508
- lowest_frame_found = float(i)
509
- break
510
-
511
- # Fallback: use maximum position (lowest point in frame) if no velocity crossing
512
- if lowest_frame_found is None:
513
- lowest_positions = positions[lowest_search_start:lowest_search_end]
514
- if len(lowest_positions) > 0:
515
- lowest_idx = int(np.argmax(lowest_positions))
516
- lowest_point = float(lowest_search_start + lowest_idx)
517
- else:
518
- lowest_point = float(int(takeoff_frame) - int(fps * 0.2))
519
- else:
520
- lowest_point = lowest_frame_found
521
-
522
- # Step 4: Find landing (impact after peak height)
523
- # Landing shows as large negative acceleration spike (impact deceleration)
524
- landing_search_start = peak_height_frame
525
- landing_search_end = min(len(accelerations), peak_height_frame + int(fps * 0.5))
526
- landing_accelerations = accelerations[landing_search_start:landing_search_end]
527
-
528
- if len(landing_accelerations) > 0:
529
- # Find most negative acceleration (maximum impact deceleration)
530
- # Landing acceleration should be around -0.008 to -0.010
531
- landing_idx = int(np.argmin(landing_accelerations)) # Most negative = impact
532
- landing_frame = float(landing_search_start + landing_idx)
533
- else:
534
- landing_frame = float(peak_height_frame + int(fps * 0.3))
535
-
536
- # Optional: Find standing phase (not critical)
537
- standing_end = None
538
- if lowest_point > 20:
539
- # Look for low-velocity period before lowest point
540
- standing_search = velocities[: int(lowest_point)]
541
- low_vel = np.abs(standing_search) < 0.005
542
- if np.any(low_vel):
543
- # Find last low-velocity frame before countermovement
544
- standing_frames = np.where(low_vel)[0]
545
- if len(standing_frames) > 10:
546
- standing_end = float(standing_frames[-1])
526
+ # Step 2-4: Find all phases using helper functions
527
+ takeoff_frame = _find_takeoff_frame(velocities, peak_height_frame, fps)
528
+ lowest_point = _find_lowest_frame(velocities, positions, takeoff_frame, fps)
529
+ landing_frame = _find_landing_frame(accelerations, peak_height_frame, fps)
530
+ standing_end = _find_standing_end(velocities, lowest_point)
547
531
 
548
532
  return (standing_end, lowest_point, takeoff_frame, landing_frame)
@@ -474,11 +474,6 @@ def _process_single(
474
474
  phases = detect_cmj_phases(
475
475
  vertical_positions,
476
476
  video.fps,
477
- velocity_threshold=params.velocity_threshold,
478
- countermovement_threshold=countermovement_threshold,
479
- min_contact_frames=params.min_contact_frames,
480
- min_eccentric_frames=params.min_contact_frames,
481
- use_curvature=params.use_curvature,
482
477
  window_length=params.smoothing_window,
483
478
  polyorder=params.polyorder,
484
479
  )
@@ -58,7 +58,6 @@ def test_find_countermovement_start() -> None:
58
58
 
59
59
  eccentric_start = find_countermovement_start(
60
60
  velocities,
61
- fps=30.0,
62
61
  countermovement_threshold=-0.008, # More lenient threshold for test
63
62
  min_eccentric_frames=3,
64
63
  standing_start=30,
@@ -86,9 +85,7 @@ def test_find_lowest_point() -> None:
86
85
 
87
86
  # New algorithm searches with min_search_frame=80 by default
88
87
  # For this short test, use min_search_frame=0
89
- lowest = find_lowest_point(
90
- positions, velocities, eccentric_start=0, min_search_frame=0
91
- )
88
+ lowest = find_lowest_point(positions, velocities, min_search_frame=0)
92
89
 
93
90
  # Should detect lowest point around frame 50 (with new algorithm may vary)
94
91
  assert 30 <= lowest <= 70 # Wider tolerance for new algorithm
@@ -112,19 +109,14 @@ def test_detect_cmj_phases_full() -> None:
112
109
  result = detect_cmj_phases(
113
110
  positions,
114
111
  fps,
115
- velocity_threshold=0.015, # Slightly lower threshold
116
- countermovement_threshold=-0.010, # More lenient for test
117
- min_contact_frames=3,
118
- min_eccentric_frames=3,
119
- use_curvature=False, # Disable for simpler test
120
112
  window_length=5,
121
113
  polyorder=2,
122
114
  )
123
115
 
124
116
  assert result is not None
125
- standing_end, lowest_point, takeoff, landing = result
117
+ _, lowest_point, takeoff, landing = result
126
118
 
127
- # Verify phases are in correct order (standing_end may be None or incorrect in synthetic data)
119
+ # Verify phases are in correct order
128
120
  assert lowest_point < takeoff
129
121
  assert takeoff < landing
130
122
 
@@ -153,10 +145,6 @@ def test_cmj_phases_without_standing() -> None:
153
145
  result = detect_cmj_phases(
154
146
  positions,
155
147
  fps,
156
- velocity_threshold=0.012, # Lower threshold for better detection
157
- countermovement_threshold=-0.008, # More lenient
158
- min_contact_frames=3,
159
- use_curvature=False,
160
148
  window_length=5,
161
149
  polyorder=2,
162
150
  )
@@ -164,7 +152,7 @@ def test_cmj_phases_without_standing() -> None:
164
152
  # Result may be None with synthetic data - that's okay for this test
165
153
  # The main goal is to verify the function handles edge cases without crashing
166
154
  if result is not None:
167
- standing_end, lowest_point, takeoff, landing = result
155
+ _, lowest_point, takeoff, landing = result
168
156
  # Basic sanity checks if phases were detected
169
157
  assert lowest_point < takeoff
170
158
  assert takeoff < landing
@@ -1,6 +1,7 @@
1
1
  """Tests for CMJ kinematics calculations."""
2
2
 
3
3
  import numpy as np
4
+ import pytest
4
5
 
5
6
  from kinemotion.cmj.kinematics import CMJMetrics, calculate_cmj_metrics
6
7
 
@@ -48,14 +49,14 @@ def test_calculate_cmj_metrics_basic() -> None:
48
49
 
49
50
  # Verify basic properties
50
51
  assert isinstance(metrics, CMJMetrics)
51
- assert metrics.video_fps == fps
52
+ assert metrics.video_fps == pytest.approx(fps)
52
53
  assert metrics.tracking_method == "foot"
53
54
 
54
55
  # Verify frames are set correctly
55
- assert metrics.standing_start_frame == standing_start
56
- assert metrics.lowest_point_frame == lowest_point
57
- assert metrics.takeoff_frame == takeoff
58
- assert metrics.landing_frame == landing
56
+ assert metrics.standing_start_frame == pytest.approx(standing_start)
57
+ assert metrics.lowest_point_frame == pytest.approx(lowest_point)
58
+ assert metrics.takeoff_frame == pytest.approx(takeoff)
59
+ assert metrics.landing_frame == pytest.approx(landing)
59
60
 
60
61
  # Verify durations
61
62
  assert metrics.flight_time > 0
@@ -179,5 +180,5 @@ def test_cmj_velocity_calculations() -> None:
179
180
  assert metrics.concentric_duration > 0
180
181
 
181
182
  # Verify velocities are calculated (may need to check signs based on coordinate system)
182
- assert metrics.peak_eccentric_velocity != 0.0
183
- assert metrics.peak_concentric_velocity != 0.0
183
+ assert abs(metrics.peak_eccentric_velocity) > 1e-6
184
+ assert abs(metrics.peak_concentric_velocity) > 1e-6
@@ -603,7 +603,7 @@ wheels = [
603
603
 
604
604
  [[package]]
605
605
  name = "kinemotion"
606
- version = "0.11.0"
606
+ version = "0.11.1"
607
607
  source = { editable = "." }
608
608
  dependencies = [
609
609
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes