feat(eval): kinematics metrics, FFT spectrum, KL divergence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
113
tests/test_eval_metrics.py
Normal file
113
tests/test_eval_metrics.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Tests for the eval metrics module."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
class TestKinematics:
|
||||
def test_compute_speed_constant_velocity(self):
|
||||
"""Constant-velocity trajectory has constant speed."""
|
||||
from ai_mouse.eval.metrics import compute_speed
|
||||
# 10 points, moving 10 px in 100 ms each step → speed = 0.1 px/ms
|
||||
xs = np.arange(0, 100, 10, dtype=float)
|
||||
ys = np.zeros(10, dtype=float)
|
||||
ts = np.arange(0, 1000, 100, dtype=float)
|
||||
v = compute_speed(xs, ys, ts)
|
||||
# All speeds should be ≈ 0.1 px/ms
|
||||
assert v.shape == (9,) # n-1 differences
|
||||
np.testing.assert_allclose(v, 0.1, rtol=1e-4)
|
||||
|
||||
def test_compute_speed_handles_zero_dt(self):
|
||||
"""Adjacent points with same timestamp must not produce NaN/inf."""
|
||||
from ai_mouse.eval.metrics import compute_speed
|
||||
xs = np.array([0.0, 10.0, 20.0])
|
||||
ys = np.array([0.0, 0.0, 0.0])
|
||||
ts = np.array([0.0, 0.0, 100.0]) # zero dt between [0] and [1]
|
||||
v = compute_speed(xs, ys, ts)
|
||||
assert np.isfinite(v).all()
|
||||
|
||||
def test_compute_acceleration(self):
|
||||
"""Linearly increasing speed → constant acceleration."""
|
||||
from ai_mouse.eval.metrics import compute_acceleration
|
||||
# speeds: 0.1, 0.2, 0.3, 0.4 over dt = 100 ms each → a = 0.001 px/ms²
|
||||
speeds = np.array([0.1, 0.2, 0.3, 0.4])
|
||||
ts = np.array([100.0, 200.0, 300.0, 400.0])
|
||||
a = compute_acceleration(speeds, ts)
|
||||
np.testing.assert_allclose(a, 0.001, rtol=1e-4)
|
||||
|
||||
def test_compute_jerk(self):
|
||||
from ai_mouse.eval.metrics import compute_jerk
|
||||
# accelerations: 0.001, 0.002, 0.003 over dt = 100 ms → j = 0.00001
|
||||
accels = np.array([0.001, 0.002, 0.003])
|
||||
ts = np.array([200.0, 300.0, 400.0])
|
||||
j = compute_jerk(accels, ts)
|
||||
np.testing.assert_allclose(j, 1e-5, rtol=1e-4)
|
||||
|
||||
|
||||
class TestStatsSummary:
|
||||
def test_compute_stats_returns_expected_keys(self):
|
||||
from ai_mouse.eval.metrics import compute_stats
|
||||
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
s = compute_stats(x)
|
||||
assert "mean" in s
|
||||
assert "std" in s
|
||||
assert "cv" in s
|
||||
assert "p25" in s
|
||||
assert "p50" in s
|
||||
assert "p75" in s
|
||||
assert "p95" in s
|
||||
|
||||
def test_cv_for_constant_is_zero(self):
|
||||
from ai_mouse.eval.metrics import compute_stats
|
||||
x = np.full(10, 3.0)
|
||||
s = compute_stats(x)
|
||||
assert s["cv"] == 0.0
|
||||
|
||||
|
||||
class TestFftSpectrum:
|
||||
def test_finds_dominant_frequency(self):
|
||||
"""A pure 8 Hz signal should have its peak near 8 Hz."""
|
||||
from ai_mouse.eval.metrics import fft_spectrum
|
||||
# Sample at 100 Hz for 1 second
|
||||
sample_rate_hz = 100.0
|
||||
ts_ms = np.arange(0, 1000, 1000 / sample_rate_hz)
|
||||
signal = np.sin(2 * np.pi * 8 * ts_ms / 1000) # 8 Hz sine
|
||||
freqs, mags = fft_spectrum(signal, sample_rate_hz)
|
||||
peak_freq = freqs[np.argmax(mags)]
|
||||
assert abs(peak_freq - 8.0) < 1.0 # within 1 Hz
|
||||
|
||||
def test_returns_only_positive_frequencies(self):
|
||||
from ai_mouse.eval.metrics import fft_spectrum
|
||||
signal = np.random.randn(64)
|
||||
freqs, mags = fft_spectrum(signal, 50.0)
|
||||
assert (freqs >= 0).all()
|
||||
assert len(freqs) == len(mags)
|
||||
|
||||
|
||||
class TestKlDivergence:
|
||||
def test_identical_distributions_zero_kl(self):
|
||||
"""KL(p, p) ≈ 0."""
|
||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
||||
rng = np.random.default_rng(42)
|
||||
x = rng.normal(0, 1, 5000)
|
||||
y = rng.normal(0, 1, 5000)
|
||||
kl = kl_divergence_histograms(x, y, bins=50)
|
||||
assert kl < 0.05
|
||||
|
||||
def test_different_distributions_positive_kl(self):
|
||||
"""Different means → positive KL."""
|
||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
||||
rng = np.random.default_rng(42)
|
||||
x = rng.normal(0, 1, 5000)
|
||||
y = rng.normal(3, 1, 5000)
|
||||
kl = kl_divergence_histograms(x, y, bins=50)
|
||||
assert kl > 0.5
|
||||
|
||||
def test_handles_disjoint_supports(self):
|
||||
"""No NaN even when histograms have non-overlapping bins."""
|
||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
||||
x = np.array([1.0, 1.1, 1.2, 1.3, 1.4])
|
||||
y = np.array([10.0, 10.1, 10.2, 10.3, 10.4])
|
||||
kl = kl_divergence_histograms(x, y, bins=10)
|
||||
assert np.isfinite(kl)
|
||||
Reference in New Issue
Block a user