171 lines
5.4 KiB
Python
171 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
import numpy as np
|
|
|
|
from evaluate_annotated_floor_events import AnnotatedOpportunity
|
|
from evaluate_egospeak_event_proxy import (
|
|
decision_source_rows,
|
|
evaluate_family,
|
|
horizon_indices,
|
|
score_activity_proxy,
|
|
)
|
|
from evaluate_floor_state_changes import NUM_CLASSES, NUM_HORIZONS
|
|
|
|
|
|
def opportunity(
|
|
uid: str,
|
|
*,
|
|
family: str = "entry",
|
|
positive: bool = True,
|
|
start: int = 20,
|
|
end: int = 21,
|
|
anchor: float = 4.0,
|
|
region_start: float = 4.0,
|
|
region_end: float = 4.0,
|
|
source: str = "WEARER_ONSET",
|
|
) -> AnnotatedOpportunity:
|
|
return AnnotatedOpportunity(
|
|
uid=uid,
|
|
session="Session_4",
|
|
family=family, # type: ignore[arg-type]
|
|
positive=positive,
|
|
subtype=("WEARER_ENTRY" if family == "entry" else "INT_YIELD")
|
|
if positive
|
|
else ("ENTRY_OTHER_TO_OTHER" if family == "entry" else "INT_OTHER_BACKCHANNEL"),
|
|
start_row=start,
|
|
end_row_exclusive=end,
|
|
anchor_seconds=anchor,
|
|
region_start_seconds=region_start,
|
|
region_end_seconds=region_end,
|
|
candidate_source=source,
|
|
)
|
|
|
|
|
|
def arrays(length: int = 50) -> tuple[np.ndarray, np.ndarray]:
|
|
return (
|
|
np.zeros((length, NUM_CLASSES, NUM_HORIZONS), dtype=np.float32),
|
|
np.ones((length, NUM_HORIZONS), dtype=bool),
|
|
)
|
|
|
|
|
|
class EgoSpeakEventProxyTests(unittest.TestCase):
|
|
def test_positive_uses_three_pre_anchor_source_rows(self) -> None:
|
|
item = opportunity("positive", anchor=4.0)
|
|
self.assertEqual(
|
|
decision_source_rows(item, window_rows=3),
|
|
(17, 20, "pre_behavior_anchor"),
|
|
)
|
|
|
|
def test_independent_hard_negative_uses_only_equal_width_region_prefix(self) -> None:
|
|
item = opportunity(
|
|
"negative",
|
|
positive=False,
|
|
start=10,
|
|
end=40,
|
|
anchor=2.0,
|
|
region_start=2.0,
|
|
region_end=8.0,
|
|
source="OTHER_TO_OTHER",
|
|
)
|
|
self.assertEqual(
|
|
decision_source_rows(item, window_rows=3),
|
|
(10, 13, "hard_negative_opportunity_onset"),
|
|
)
|
|
|
|
def test_short_hard_negative_is_not_silently_padded(self) -> None:
|
|
item = opportunity(
|
|
"short",
|
|
positive=False,
|
|
start=10,
|
|
end=12,
|
|
source="OTHER_TO_OTHER",
|
|
)
|
|
self.assertIsNone(decision_source_rows(item, window_rows=3))
|
|
|
|
def test_relabelled_raw_boundary_uses_pre_anchor_rows(self) -> None:
|
|
item = opportunity(
|
|
"backchannel",
|
|
positive=False,
|
|
start=5,
|
|
end=35,
|
|
anchor=4.0,
|
|
region_start=1.0,
|
|
region_end=7.0,
|
|
source="WEARER_ONSET",
|
|
)
|
|
self.assertEqual(
|
|
decision_source_rows(item, window_rows=3),
|
|
(17, 20, "pre_behavior_anchor"),
|
|
)
|
|
|
|
def test_trajectory_follows_target_aligned_diagonal(self) -> None:
|
|
item = opportunity("positive", anchor=4.0)
|
|
prediction, valid = arrays()
|
|
selected = horizon_indices([0.6, 0.8, 1.0])
|
|
# Best decision row is 18. Target rows are source+horizon_index+1.
|
|
prediction[21, 1, 2] = 0.6
|
|
prediction[22, 1, 3] = 0.9
|
|
prediction[23, 1, 4] = 0.3
|
|
scored = score_activity_proxy(
|
|
item,
|
|
{"Session_4": (prediction, valid)},
|
|
window_rows=3,
|
|
selected_horizons=selected,
|
|
)
|
|
self.assertIsNotNone(scored)
|
|
score, details = scored # type: ignore[misc]
|
|
self.assertAlmostEqual(score, 0.6)
|
|
self.assertEqual(details["best_source_row"], 18)
|
|
self.assertEqual(len(details["best_wearer_trajectory"]), 3)
|
|
|
|
def test_yield_inverts_sustained_wearer_activity(self) -> None:
|
|
item = opportunity("yield", family="int", anchor=4.0, source="WEARER_OFFSET")
|
|
prediction, valid = arrays()
|
|
selected = horizon_indices([0.6, 0.8])
|
|
for source_row in range(17, 20):
|
|
prediction[source_row + 3, 1, 2] = 0.2
|
|
prediction[source_row + 4, 1, 3] = 0.2
|
|
scored = score_activity_proxy(
|
|
item,
|
|
{"Session_4": (prediction, valid)},
|
|
window_rows=3,
|
|
selected_horizons=selected,
|
|
)
|
|
self.assertIsNotNone(scored)
|
|
self.assertAlmostEqual(scored[0], 0.8) # type: ignore[index]
|
|
|
|
def test_each_opportunity_contributes_one_metric_row(self) -> None:
|
|
positive = opportunity("p", anchor=4.0)
|
|
negative = opportunity(
|
|
"n",
|
|
positive=False,
|
|
start=25,
|
|
end=30,
|
|
anchor=5.0,
|
|
region_start=5.0,
|
|
region_end=6.0,
|
|
source="OTHER_TO_OTHER",
|
|
)
|
|
prediction, valid = arrays()
|
|
selected = horizon_indices([0.6])
|
|
for source_row in range(17, 20):
|
|
prediction[source_row + 3, 1, 2] = 0.9
|
|
metric, rows = evaluate_family(
|
|
[positive, negative],
|
|
{"Session_4": (prediction, valid)},
|
|
"entry",
|
|
[],
|
|
window_rows=3,
|
|
selected_horizons=selected,
|
|
)
|
|
self.assertEqual(len(rows), 2)
|
|
self.assertEqual(metric["num_positive"], 1)
|
|
self.assertEqual(metric["num_hard_negative"], 1)
|
|
self.assertAlmostEqual(metric["average_precision"], 1.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|