"""Tests for the movement-analysis engine — no GPU, no model downloads.""" import math import numpy as np from formscout.types import Pose2DResult def _pose_from(traj_by_joint, n, base_conf=0.9): """Build a Pose2DResult; traj_by_joint maps joint->callable(i)->(x,y).""" kps = [] for i in range(n): frame = {} for j in range(17): if j in traj_by_joint: x, y = traj_by_joint[j](i) else: x, y = float(100 + j * 5), float(100 + j * 5) frame[j] = {"x": float(x), "y": float(y), "conf": base_conf} kps.append(frame) return Pose2DResult(keypoints=kps, fps=30.0, confidence=0.9) # ── relevant_joints ──────────────────────────────────────────────────────────── def test_relevant_joints_and_primary_consistent(): from formscout.analysis import relevant_joints as RJ for test in RJ.RELEVANT: joints = RJ.relevant_joints(test) angles = RJ.relevant_angles(test) prim = RJ.primary_angle(test) assert joints, f"{test} has no relevant joints" assert prim in angles, f"{test} primary angle not in angles" def test_openness_label_monotonic(): from formscout.analysis.relevant_joints import openness_label assert "open" in openness_label(175) assert openness_label(30).endswith("closed") # ── timeseries ────────────────────────────────────────────────────────────────── def test_angle_series_length_matches_frames(): from formscout.analysis.timeseries import angle_series pose = _pose_from({}, n=8) series = angle_series(pose, "deep_squat") assert "left_knee_flexion" in series for name, vals in series.items(): assert len(vals) == 8 def test_relevant_flexion_reports_degrees_and_openness(): from formscout.analysis.timeseries import relevant_flexion_at # Straight leg: hip, knee, ankle collinear vertical → ~180° straight = { 11: lambda i: (200, 100), 13: lambda i: (200, 200), 15: lambda i: (200, 300), 12: lambda i: (260, 100), 14: lambda i: (260, 200), 16: lambda i: (260, 300), 5: lambda i: (200, 40), 6: lambda i: (260, 40), } pose = _pose_from(straight, n=5) flex = relevant_flexion_at(pose, "deep_squat", 2) assert "left_knee_flexion" in flex assert flex["left_knee_flexion"]["deg"] > 160 assert "open" in flex["left_knee_flexion"]["openness"] # ── laban ─────────────────────────────────────────────────────────────────────── def test_laban_factors_in_unit_range(): from formscout.analysis.laban import compute_laban pose = _pose_from({13: lambda i: (100 + i * 8, 200)}, n=20) res = compute_laban(pose, "deep_squat", fps=30.0) for k, v in res["effort"].items(): assert 0.0 <= v <= 1.0, f"{k}={v} out of range" assert set(res["labels"]) == {"space", "weight", "time", "flow"} def test_laban_straight_line_is_direct(): from formscout.analysis.laban import compute_laban # Knee travels in a straight horizontal line → high directness (Space) pose = _pose_from({13: lambda i: (100 + i * 10, 200)}, n=20) res = compute_laban(pose, "deep_squat", fps=30.0) assert res["effort"]["space"] > 0.8 def test_laban_static_clip_low_energy(): from formscout.analysis.laban import compute_laban pose = _pose_from({13: lambda i: (200, 200)}, n=20) # no motion res = compute_laban(pose, "deep_squat", fps=30.0) assert res["effort"]["weight"] < 0.2 # ── charts ────────────────────────────────────────────────────────────────────── def test_angle_over_time_chart(tmp_path): from formscout.analysis import charts out = str(tmp_path / "angle.png") series = {"left_knee_flexion": [90, 100, 110, 95], "right_knee_flexion": [88, 99, 108, 94]} p = charts.angle_over_time(series, "left_knee_flexion", 2, out) assert p == out import os assert os.path.getsize(out) > 0 def test_velocity_profile_chart(tmp_path): from formscout.analysis import charts out = str(tmp_path / "vel.png") kps = [{j: {"x": 100 + j + i * 3, "y": 100 + j, "conf": 0.9} for j in range(17)} for i in range(10)] p = charts.velocity_profile(kps, 30.0, [13, 14, 11, 12], out) import os assert p == out and os.path.getsize(out) > 0 def test_laban_radar_chart(tmp_path): from formscout.analysis import charts out = str(tmp_path / "radar.png") p = charts.laban_radar({"space": 0.8, "weight": 0.4, "time": 0.6, "flow": 0.3}, out) import os assert p == out and os.path.getsize(out) > 0 def test_flexion_bars_chart(tmp_path): from formscout.analysis import charts out = str(tmp_path / "flex.png") flex = {"left_knee_flexion": {"deg": 95.0, "openness": "flexed"}, "left_hip_flexion": {"deg": 120.0, "openness": "mid-range"}} p = charts.flexion_bars(flex, out) import os assert p == out and os.path.getsize(out) > 0 def test_symmetry_bars_chart(tmp_path): from formscout.analysis import charts out = str(tmp_path / "sym.png") asym = [{"test": "hurdle_step", "left_score": 2, "right_score": 3, "delta": 1}] p = charts.symmetry_bars(asym, out) import os assert p == out and os.path.getsize(out) > 0 def test_charts_return_none_on_empty(tmp_path): from formscout.analysis import charts assert charts.angle_over_time({}, None, None, str(tmp_path / "a.png")) is None assert charts.flexion_bars({}, str(tmp_path / "f.png")) is None assert charts.symmetry_bars([], str(tmp_path / "s.png")) is None