refactor: move eval/ and data_adapters/ to tools/
This commit is contained in:
@@ -9,7 +9,7 @@ import pytest
|
|||||||
|
|
||||||
def test_module_exports():
|
def test_module_exports():
|
||||||
"""The adapter module must export the public functions used by CLI."""
|
"""The adapter module must export the public functions used by CLI."""
|
||||||
from ai_mouse.data_adapters import balabit
|
from tools.data_adapters import balabit
|
||||||
assert hasattr(balabit, "parse_session_csv")
|
assert hasattr(balabit, "parse_session_csv")
|
||||||
assert hasattr(balabit, "segment_by_clicks")
|
assert hasattr(balabit, "segment_by_clicks")
|
||||||
assert hasattr(balabit, "filter_segments")
|
assert hasattr(balabit, "filter_segments")
|
||||||
@@ -20,7 +20,7 @@ def test_module_exports():
|
|||||||
|
|
||||||
def test_mouse_event_dataclass():
|
def test_mouse_event_dataclass():
|
||||||
"""MouseEvent has expected fields."""
|
"""MouseEvent has expected fields."""
|
||||||
from ai_mouse.data_adapters.balabit import MouseEvent
|
from tools.data_adapters.balabit import MouseEvent
|
||||||
e = MouseEvent(t_ms=100, button="NoButton", state="Move", x=300, y=400)
|
e = MouseEvent(t_ms=100, button="NoButton", state="Move", x=300, y=400)
|
||||||
assert e.t_ms == 100
|
assert e.t_ms == 100
|
||||||
assert e.state == "Move"
|
assert e.state == "Move"
|
||||||
@@ -29,7 +29,7 @@ def test_mouse_event_dataclass():
|
|||||||
|
|
||||||
def test_segment_dataclass():
|
def test_segment_dataclass():
|
||||||
"""Segment has expected fields."""
|
"""Segment has expected fields."""
|
||||||
from ai_mouse.data_adapters.balabit import MouseEvent, Segment
|
from tools.data_adapters.balabit import MouseEvent, Segment
|
||||||
events = [MouseEvent(t_ms=0, button="NoButton", state="Move", x=10, y=20)]
|
events = [MouseEvent(t_ms=0, button="NoButton", state="Move", x=10, y=20)]
|
||||||
s = Segment(events=events, click_x=100, click_y=200, click_t_ms=500, session_id="user1_s1")
|
s = Segment(events=events, click_x=100, click_y=200, click_t_ms=500, session_id="user1_s1")
|
||||||
assert s.events == events
|
assert s.events == events
|
||||||
@@ -45,7 +45,7 @@ def _write_csv(path: Path, rows: list[str]) -> None:
|
|||||||
|
|
||||||
class TestParseSessionCsv:
|
class TestParseSessionCsv:
|
||||||
def test_parses_basic_rows(self, tmp_path):
|
def test_parses_basic_rows(self, tmp_path):
|
||||||
from ai_mouse.data_adapters.balabit import parse_session_csv
|
from tools.data_adapters.balabit import parse_session_csv
|
||||||
csv = tmp_path / "session_1"
|
csv = tmp_path / "session_1"
|
||||||
_write_csv(csv, [
|
_write_csv(csv, [
|
||||||
"1500000000.000,0.000,NoButton,Move,100,200",
|
"1500000000.000,0.000,NoButton,Move,100,200",
|
||||||
@@ -63,7 +63,7 @@ class TestParseSessionCsv:
|
|||||||
|
|
||||||
def test_handles_float_timestamps(self, tmp_path):
|
def test_handles_float_timestamps(self, tmp_path):
|
||||||
"""Client timestamps are floats in seconds; we convert to int ms."""
|
"""Client timestamps are floats in seconds; we convert to int ms."""
|
||||||
from ai_mouse.data_adapters.balabit import parse_session_csv
|
from tools.data_adapters.balabit import parse_session_csv
|
||||||
csv = tmp_path / "session_2"
|
csv = tmp_path / "session_2"
|
||||||
_write_csv(csv, [
|
_write_csv(csv, [
|
||||||
"0,1.234,NoButton,Move,50,60",
|
"0,1.234,NoButton,Move,50,60",
|
||||||
@@ -75,7 +75,7 @@ class TestParseSessionCsv:
|
|||||||
|
|
||||||
def test_skips_malformed_rows(self, tmp_path):
|
def test_skips_malformed_rows(self, tmp_path):
|
||||||
"""Rows with bad data are logged and skipped, not raised."""
|
"""Rows with bad data are logged and skipped, not raised."""
|
||||||
from ai_mouse.data_adapters.balabit import parse_session_csv
|
from tools.data_adapters.balabit import parse_session_csv
|
||||||
csv = tmp_path / "session_3"
|
csv = tmp_path / "session_3"
|
||||||
_write_csv(csv, [
|
_write_csv(csv, [
|
||||||
"0,0.000,NoButton,Move,100,200",
|
"0,0.000,NoButton,Move,100,200",
|
||||||
@@ -89,7 +89,7 @@ class TestParseSessionCsv:
|
|||||||
assert events[1].x == 150
|
assert events[1].x == 150
|
||||||
|
|
||||||
def test_returns_empty_list_for_empty_file(self, tmp_path):
|
def test_returns_empty_list_for_empty_file(self, tmp_path):
|
||||||
from ai_mouse.data_adapters.balabit import parse_session_csv
|
from tools.data_adapters.balabit import parse_session_csv
|
||||||
csv = tmp_path / "session_4"
|
csv = tmp_path / "session_4"
|
||||||
csv.write_text("record timestamp,client timestamp,button,state,x,y\n", encoding="utf-8")
|
csv.write_text("record timestamp,client timestamp,button,state,x,y\n", encoding="utf-8")
|
||||||
events = parse_session_csv(csv)
|
events = parse_session_csv(csv)
|
||||||
@@ -98,11 +98,11 @@ class TestParseSessionCsv:
|
|||||||
|
|
||||||
class TestSegmentByClicks:
|
class TestSegmentByClicks:
|
||||||
def _make_event(self, t_ms: int, state: str, x: int, y: int, button: str = "NoButton"):
|
def _make_event(self, t_ms: int, state: str, x: int, y: int, button: str = "NoButton"):
|
||||||
from ai_mouse.data_adapters.balabit import MouseEvent
|
from tools.data_adapters.balabit import MouseEvent
|
||||||
return MouseEvent(t_ms=t_ms, button=button, state=state, x=x, y=y)
|
return MouseEvent(t_ms=t_ms, button=button, state=state, x=x, y=y)
|
||||||
|
|
||||||
def test_one_click_one_segment(self):
|
def test_one_click_one_segment(self):
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20),
|
self._make_event(0, "Move", 10, 20),
|
||||||
self._make_event(100, "Move", 50, 60),
|
self._make_event(100, "Move", 50, 60),
|
||||||
@@ -120,7 +120,7 @@ class TestSegmentByClicks:
|
|||||||
|
|
||||||
def test_window_excludes_old_events(self):
|
def test_window_excludes_old_events(self):
|
||||||
"""Move events earlier than (click_t - window_ms) are dropped."""
|
"""Move events earlier than (click_t - window_ms) are dropped."""
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20), # too old
|
self._make_event(0, "Move", 10, 20), # too old
|
||||||
self._make_event(100, "Move", 20, 30), # too old
|
self._make_event(100, "Move", 20, 30), # too old
|
||||||
@@ -133,7 +133,7 @@ class TestSegmentByClicks:
|
|||||||
assert segments[0].events[0].t_ms == 900
|
assert segments[0].events[0].t_ms == 900
|
||||||
|
|
||||||
def test_multiple_clicks_multiple_segments(self):
|
def test_multiple_clicks_multiple_segments(self):
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20),
|
self._make_event(0, "Move", 10, 20),
|
||||||
self._make_event(100, "Pressed", 50, 50, button="Left"),
|
self._make_event(100, "Pressed", 50, 50, button="Left"),
|
||||||
@@ -152,7 +152,7 @@ class TestSegmentByClicks:
|
|||||||
|
|
||||||
def test_skips_pressed_with_non_left_button(self):
|
def test_skips_pressed_with_non_left_button(self):
|
||||||
"""Right-clicks and wheel-clicks don't anchor segments (only Left)."""
|
"""Right-clicks and wheel-clicks don't anchor segments (only Left)."""
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20),
|
self._make_event(0, "Move", 10, 20),
|
||||||
self._make_event(100, "Pressed", 50, 50, button="Right"), # ignored
|
self._make_event(100, "Pressed", 50, 50, button="Right"), # ignored
|
||||||
@@ -164,7 +164,7 @@ class TestSegmentByClicks:
|
|||||||
assert segments[0].click_x == 70
|
assert segments[0].click_x == 70
|
||||||
|
|
||||||
def test_no_clicks_returns_empty(self):
|
def test_no_clicks_returns_empty(self):
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20),
|
self._make_event(0, "Move", 10, 20),
|
||||||
self._make_event(100, "Move", 20, 30),
|
self._make_event(100, "Move", 20, 30),
|
||||||
@@ -174,7 +174,7 @@ class TestSegmentByClicks:
|
|||||||
|
|
||||||
def test_excludes_drag_events(self):
|
def test_excludes_drag_events(self):
|
||||||
"""Drag events are not Move; segment should only include Move."""
|
"""Drag events are not Move; segment should only include Move."""
|
||||||
from ai_mouse.data_adapters.balabit import segment_by_clicks
|
from tools.data_adapters.balabit import segment_by_clicks
|
||||||
events = [
|
events = [
|
||||||
self._make_event(0, "Move", 10, 20),
|
self._make_event(0, "Move", 10, 20),
|
||||||
self._make_event(100, "Drag", 30, 40), # not Move
|
self._make_event(100, "Drag", 30, 40), # not Move
|
||||||
@@ -191,7 +191,7 @@ class TestSegmentByClicks:
|
|||||||
class TestFilterSegments:
|
class TestFilterSegments:
|
||||||
def _seg(self, events_data: list[tuple[int, int, int]], click=(500, 500), session="s"):
|
def _seg(self, events_data: list[tuple[int, int, int]], click=(500, 500), session="s"):
|
||||||
"""events_data: list of (t_ms, x, y) tuples."""
|
"""events_data: list of (t_ms, x, y) tuples."""
|
||||||
from ai_mouse.data_adapters.balabit import MouseEvent, Segment
|
from tools.data_adapters.balabit import MouseEvent, Segment
|
||||||
events = [MouseEvent(t_ms=t, button="NoButton", state="Move", x=x, y=y)
|
events = [MouseEvent(t_ms=t, button="NoButton", state="Move", x=x, y=y)
|
||||||
for (t, x, y) in events_data]
|
for (t, x, y) in events_data]
|
||||||
click_t = events[-1].t_ms + 50 if events else 100
|
click_t = events[-1].t_ms + 50 if events else 100
|
||||||
@@ -199,14 +199,14 @@ class TestFilterSegments:
|
|||||||
click_t_ms=click_t, session_id=session)
|
click_t_ms=click_t, session_id=session)
|
||||||
|
|
||||||
def test_drops_segment_with_too_few_events(self):
|
def test_drops_segment_with_too_few_events(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
seg = self._seg([(0, 100, 100), (50, 105, 105)]) # 2 events, min=5
|
seg = self._seg([(0, 100, 100), (50, 105, 105)]) # 2 events, min=5
|
||||||
result = filter_segments([seg], min_events=5, min_dist=10,
|
result = filter_segments([seg], min_events=5, min_dist=10,
|
||||||
max_span_ms=5000, max_gap_ms=200)
|
max_span_ms=5000, max_gap_ms=200)
|
||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
def test_drops_segment_with_short_distance(self):
|
def test_drops_segment_with_short_distance(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# Start (100,100), end click=(105,100) → dist=5, min_dist=50
|
# Start (100,100), end click=(105,100) → dist=5, min_dist=50
|
||||||
events = [(i*10, 100+i, 100) for i in range(10)]
|
events = [(i*10, 100+i, 100) for i in range(10)]
|
||||||
seg = self._seg(events, click=(105, 100))
|
seg = self._seg(events, click=(105, 100))
|
||||||
@@ -215,7 +215,7 @@ class TestFilterSegments:
|
|||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
def test_drops_segment_with_too_long_span(self):
|
def test_drops_segment_with_too_long_span(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# Span 6000ms, max_span=5000
|
# Span 6000ms, max_span=5000
|
||||||
events = [(0, 100, 100), (2000, 200, 200), (4000, 300, 300), (6000, 400, 400),
|
events = [(0, 100, 100), (2000, 200, 200), (4000, 300, 300), (6000, 400, 400),
|
||||||
(6010, 410, 400), (6020, 420, 400)]
|
(6010, 410, 400), (6020, 420, 400)]
|
||||||
@@ -225,7 +225,7 @@ class TestFilterSegments:
|
|||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
def test_drops_segment_with_gap(self):
|
def test_drops_segment_with_gap(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# Gap of 500ms between events 2 and 3, max_gap=200
|
# Gap of 500ms between events 2 and 3, max_gap=200
|
||||||
events = [(0, 100, 100), (50, 110, 110), (100, 120, 120),
|
events = [(0, 100, 100), (50, 110, 110), (100, 120, 120),
|
||||||
(600, 200, 200), (650, 210, 210), (700, 220, 220)]
|
(600, 200, 200), (650, 210, 210), (700, 220, 220)]
|
||||||
@@ -235,7 +235,7 @@ class TestFilterSegments:
|
|||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
def test_drops_segment_with_out_of_range_coords(self):
|
def test_drops_segment_with_out_of_range_coords(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# x=10000 out of range
|
# x=10000 out of range
|
||||||
events = [(i*10, 10000, 100) for i in range(6)]
|
events = [(i*10, 10000, 100) for i in range(6)]
|
||||||
seg = self._seg(events, click=(10100, 100))
|
seg = self._seg(events, click=(10100, 100))
|
||||||
@@ -245,7 +245,7 @@ class TestFilterSegments:
|
|||||||
|
|
||||||
def test_drops_segment_with_short_arc_length(self):
|
def test_drops_segment_with_short_arc_length(self):
|
||||||
"""Total arc < 50 even though endpoints are far → high-frequency jitter only."""
|
"""Total arc < 50 even though endpoints are far → high-frequency jitter only."""
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# Tiny back-and-forth with click 100px away — unrealistic, drop it
|
# Tiny back-and-forth with click 100px away — unrealistic, drop it
|
||||||
events = [(i*10, 100 + (i % 2), 100) for i in range(10)]
|
events = [(i*10, 100 + (i % 2), 100) for i in range(10)]
|
||||||
seg = self._seg(events, click=(200, 100))
|
seg = self._seg(events, click=(200, 100))
|
||||||
@@ -255,7 +255,7 @@ class TestFilterSegments:
|
|||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
def test_keeps_valid_segment(self):
|
def test_keeps_valid_segment(self):
|
||||||
from ai_mouse.data_adapters.balabit import filter_segments
|
from tools.data_adapters.balabit import filter_segments
|
||||||
# Smooth 100→500 px straight line, 10 events, span 500ms
|
# Smooth 100→500 px straight line, 10 events, span 500ms
|
||||||
events = [(i*50, 100 + i*40, 100) for i in range(10)]
|
events = [(i*50, 100 + i*40, 100) for i in range(10)]
|
||||||
seg = self._seg(events, click=(500, 100))
|
seg = self._seg(events, click=(500, 100))
|
||||||
@@ -267,7 +267,7 @@ class TestFilterSegments:
|
|||||||
class TestProcessSession:
|
class TestProcessSession:
|
||||||
def test_writes_jsonl_in_expected_format(self, tmp_path):
|
def test_writes_jsonl_in_expected_format(self, tmp_path):
|
||||||
from tools.config import BalabitAdapterConfig
|
from tools.config import BalabitAdapterConfig
|
||||||
from ai_mouse.data_adapters.balabit import process_session
|
from tools.data_adapters.balabit import process_session
|
||||||
|
|
||||||
# Construct a Balabit-format CSV with one valid segment
|
# Construct a Balabit-format CSV with one valid segment
|
||||||
csv_path = tmp_path / "user_session_42"
|
csv_path = tmp_path / "user_session_42"
|
||||||
@@ -304,7 +304,7 @@ class TestProcessSession:
|
|||||||
|
|
||||||
def test_returns_zero_for_session_with_no_valid_segments(self, tmp_path):
|
def test_returns_zero_for_session_with_no_valid_segments(self, tmp_path):
|
||||||
from tools.config import BalabitAdapterConfig
|
from tools.config import BalabitAdapterConfig
|
||||||
from ai_mouse.data_adapters.balabit import process_session
|
from tools.data_adapters.balabit import process_session
|
||||||
|
|
||||||
csv_path = tmp_path / "empty_session"
|
csv_path = tmp_path / "empty_session"
|
||||||
_write_csv(csv_path, ["0,0.000,NoButton,Move,100,100"]) # no clicks
|
_write_csv(csv_path, ["0,0.000,NoButton,Move,100,100"]) # no clicks
|
||||||
@@ -319,7 +319,7 @@ class TestProcessSession:
|
|||||||
|
|
||||||
def test_appends_to_existing_jsonl(self, tmp_path):
|
def test_appends_to_existing_jsonl(self, tmp_path):
|
||||||
from tools.config import BalabitAdapterConfig
|
from tools.config import BalabitAdapterConfig
|
||||||
from ai_mouse.data_adapters.balabit import process_session
|
from tools.data_adapters.balabit import process_session
|
||||||
|
|
||||||
out = tmp_path / "out.jsonl"
|
out = tmp_path / "out.jsonl"
|
||||||
out.write_text('{"meta":{"start":[0,0],"end":[1,1]},"events":[]}\n', encoding="utf-8")
|
out.write_text('{"meta":{"start":[0,0],"end":[1,1]},"events":[]}\n', encoding="utf-8")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import pytest
|
|||||||
class TestKinematics:
|
class TestKinematics:
|
||||||
def test_compute_speed_constant_velocity(self):
|
def test_compute_speed_constant_velocity(self):
|
||||||
"""Constant-velocity trajectory has constant speed."""
|
"""Constant-velocity trajectory has constant speed."""
|
||||||
from ai_mouse.eval.metrics import compute_speed
|
from tools.eval.metrics import compute_speed
|
||||||
# 10 points, moving 10 px in 100 ms each step → speed = 0.1 px/ms
|
# 10 points, moving 10 px in 100 ms each step → speed = 0.1 px/ms
|
||||||
xs = np.arange(0, 100, 10, dtype=float)
|
xs = np.arange(0, 100, 10, dtype=float)
|
||||||
ys = np.zeros(10, dtype=float)
|
ys = np.zeros(10, dtype=float)
|
||||||
@@ -20,7 +20,7 @@ class TestKinematics:
|
|||||||
|
|
||||||
def test_compute_speed_handles_zero_dt(self):
|
def test_compute_speed_handles_zero_dt(self):
|
||||||
"""Adjacent points with same timestamp must not produce NaN/inf."""
|
"""Adjacent points with same timestamp must not produce NaN/inf."""
|
||||||
from ai_mouse.eval.metrics import compute_speed
|
from tools.eval.metrics import compute_speed
|
||||||
xs = np.array([0.0, 10.0, 20.0])
|
xs = np.array([0.0, 10.0, 20.0])
|
||||||
ys = np.array([0.0, 0.0, 0.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]
|
ts = np.array([0.0, 0.0, 100.0]) # zero dt between [0] and [1]
|
||||||
@@ -29,7 +29,7 @@ class TestKinematics:
|
|||||||
|
|
||||||
def test_compute_acceleration(self):
|
def test_compute_acceleration(self):
|
||||||
"""Linearly increasing speed → constant acceleration."""
|
"""Linearly increasing speed → constant acceleration."""
|
||||||
from ai_mouse.eval.metrics import compute_acceleration
|
from tools.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: 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])
|
speeds = np.array([0.1, 0.2, 0.3, 0.4])
|
||||||
ts = np.array([100.0, 200.0, 300.0, 400.0])
|
ts = np.array([100.0, 200.0, 300.0, 400.0])
|
||||||
@@ -37,7 +37,7 @@ class TestKinematics:
|
|||||||
np.testing.assert_allclose(a, 0.001, rtol=1e-4)
|
np.testing.assert_allclose(a, 0.001, rtol=1e-4)
|
||||||
|
|
||||||
def test_compute_jerk(self):
|
def test_compute_jerk(self):
|
||||||
from ai_mouse.eval.metrics import compute_jerk
|
from tools.eval.metrics import compute_jerk
|
||||||
# accelerations: 0.001, 0.002, 0.003 over dt = 100 ms → j = 0.00001
|
# accelerations: 0.001, 0.002, 0.003 over dt = 100 ms → j = 0.00001
|
||||||
accels = np.array([0.001, 0.002, 0.003])
|
accels = np.array([0.001, 0.002, 0.003])
|
||||||
ts = np.array([200.0, 300.0, 400.0])
|
ts = np.array([200.0, 300.0, 400.0])
|
||||||
@@ -47,7 +47,7 @@ class TestKinematics:
|
|||||||
|
|
||||||
class TestStatsSummary:
|
class TestStatsSummary:
|
||||||
def test_compute_stats_returns_expected_keys(self):
|
def test_compute_stats_returns_expected_keys(self):
|
||||||
from ai_mouse.eval.metrics import compute_stats
|
from tools.eval.metrics import compute_stats
|
||||||
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||||
s = compute_stats(x)
|
s = compute_stats(x)
|
||||||
assert "mean" in s
|
assert "mean" in s
|
||||||
@@ -59,7 +59,7 @@ class TestStatsSummary:
|
|||||||
assert "p95" in s
|
assert "p95" in s
|
||||||
|
|
||||||
def test_cv_for_constant_is_zero(self):
|
def test_cv_for_constant_is_zero(self):
|
||||||
from ai_mouse.eval.metrics import compute_stats
|
from tools.eval.metrics import compute_stats
|
||||||
x = np.full(10, 3.0)
|
x = np.full(10, 3.0)
|
||||||
s = compute_stats(x)
|
s = compute_stats(x)
|
||||||
assert s["cv"] == 0.0
|
assert s["cv"] == 0.0
|
||||||
@@ -68,7 +68,7 @@ class TestStatsSummary:
|
|||||||
class TestFftSpectrum:
|
class TestFftSpectrum:
|
||||||
def test_finds_dominant_frequency(self):
|
def test_finds_dominant_frequency(self):
|
||||||
"""A pure 8 Hz signal should have its peak near 8 Hz."""
|
"""A pure 8 Hz signal should have its peak near 8 Hz."""
|
||||||
from ai_mouse.eval.metrics import fft_spectrum
|
from tools.eval.metrics import fft_spectrum
|
||||||
# Sample at 100 Hz for 1 second
|
# Sample at 100 Hz for 1 second
|
||||||
sample_rate_hz = 100.0
|
sample_rate_hz = 100.0
|
||||||
ts_ms = np.arange(0, 1000, 1000 / sample_rate_hz)
|
ts_ms = np.arange(0, 1000, 1000 / sample_rate_hz)
|
||||||
@@ -78,7 +78,7 @@ class TestFftSpectrum:
|
|||||||
assert abs(peak_freq - 8.0) < 1.0 # within 1 Hz
|
assert abs(peak_freq - 8.0) < 1.0 # within 1 Hz
|
||||||
|
|
||||||
def test_returns_only_positive_frequencies(self):
|
def test_returns_only_positive_frequencies(self):
|
||||||
from ai_mouse.eval.metrics import fft_spectrum
|
from tools.eval.metrics import fft_spectrum
|
||||||
signal = np.random.randn(64)
|
signal = np.random.randn(64)
|
||||||
freqs, mags = fft_spectrum(signal, 50.0)
|
freqs, mags = fft_spectrum(signal, 50.0)
|
||||||
assert (freqs >= 0).all()
|
assert (freqs >= 0).all()
|
||||||
@@ -88,7 +88,7 @@ class TestFftSpectrum:
|
|||||||
class TestKlDivergence:
|
class TestKlDivergence:
|
||||||
def test_identical_distributions_zero_kl(self):
|
def test_identical_distributions_zero_kl(self):
|
||||||
"""KL(p, p) ≈ 0."""
|
"""KL(p, p) ≈ 0."""
|
||||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
from tools.eval.metrics import kl_divergence_histograms
|
||||||
rng = np.random.default_rng(42)
|
rng = np.random.default_rng(42)
|
||||||
x = rng.normal(0, 1, 5000)
|
x = rng.normal(0, 1, 5000)
|
||||||
y = rng.normal(0, 1, 5000)
|
y = rng.normal(0, 1, 5000)
|
||||||
@@ -97,7 +97,7 @@ class TestKlDivergence:
|
|||||||
|
|
||||||
def test_different_distributions_positive_kl(self):
|
def test_different_distributions_positive_kl(self):
|
||||||
"""Different means → positive KL."""
|
"""Different means → positive KL."""
|
||||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
from tools.eval.metrics import kl_divergence_histograms
|
||||||
rng = np.random.default_rng(42)
|
rng = np.random.default_rng(42)
|
||||||
x = rng.normal(0, 1, 5000)
|
x = rng.normal(0, 1, 5000)
|
||||||
y = rng.normal(3, 1, 5000)
|
y = rng.normal(3, 1, 5000)
|
||||||
@@ -106,7 +106,7 @@ class TestKlDivergence:
|
|||||||
|
|
||||||
def test_handles_disjoint_supports(self):
|
def test_handles_disjoint_supports(self):
|
||||||
"""No NaN even when histograms have non-overlapping bins."""
|
"""No NaN even when histograms have non-overlapping bins."""
|
||||||
from ai_mouse.eval.metrics import kl_divergence_histograms
|
from tools.eval.metrics import kl_divergence_histograms
|
||||||
x = np.array([1.0, 1.1, 1.2, 1.3, 1.4])
|
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])
|
y = np.array([10.0, 10.1, 10.2, 10.3, 10.4])
|
||||||
kl = kl_divergence_histograms(x, y, bins=10)
|
kl = kl_divergence_histograms(x, y, bins=10)
|
||||||
@@ -116,7 +116,7 @@ class TestKlDivergence:
|
|||||||
class TestReportGeneration:
|
class TestReportGeneration:
|
||||||
def test_generates_report_md(self, tmp_path):
|
def test_generates_report_md(self, tmp_path):
|
||||||
"""Smoke test: build_report writes an MD file with all expected sections."""
|
"""Smoke test: build_report writes an MD file with all expected sections."""
|
||||||
from ai_mouse.eval.report import build_report
|
from tools.eval.report import build_report
|
||||||
|
|
||||||
# Synthetic generated traces (3 traces, 50 points each)
|
# Synthetic generated traces (3 traces, 50 points each)
|
||||||
rng = np.random.default_rng(0)
|
rng = np.random.default_rng(0)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ai_mouse.data_adapters.balabit import main
|
from tools.data_adapters.balabit import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
@@ -110,7 +110,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
logger.error("Empty trace sets — aborting")
|
logger.error("Empty trace sets — aborting")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
from ai_mouse.eval.report import build_report
|
from tools.eval.report import build_report
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
build_report(
|
build_report(
|
||||||
generated_traces=gen_traces,
|
generated_traces=gen_traces,
|
||||||
@@ -15,7 +15,7 @@ import matplotlib
|
|||||||
matplotlib.use("Agg") # headless
|
matplotlib.use("Agg") # headless
|
||||||
import matplotlib.pyplot as plt # noqa: E402
|
import matplotlib.pyplot as plt # noqa: E402
|
||||||
|
|
||||||
from ai_mouse.eval.metrics import (
|
from tools.eval.metrics import (
|
||||||
compute_acceleration,
|
compute_acceleration,
|
||||||
compute_jerk,
|
compute_jerk,
|
||||||
compute_speed,
|
compute_speed,
|
||||||
Reference in New Issue
Block a user