dispatchpulse / tests /test_simulation.py
Arun-Sanjay's picture
Update tests to use inline heuristic helper after inference.py rewrite
37d5c95
"""Tests that the DispatchSimulation engine behaves correctly end-to-end."""
import os
import sys
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, _ROOT)
from grader import grade_simulation # noqa: E402
from scenario_loader import load_scenario, list_tasks # noqa: E402
from simulation import DispatchSimulation # noqa: E402
def test_load_all_scenarios():
"""All three task YAMLs load and parse correctly."""
for name in list_tasks():
s = load_scenario(name)
assert s["name"] == name
assert "calls" in s
assert "units" in s
assert "hospitals" in s
def test_easy_scenario_starts_with_no_pending():
"""Reset on easy: at t=0 there should be 0 pending calls (first call at t=1)."""
sim = DispatchSimulation(load_scenario("easy"), seed=42)
assert sim.current_time == 0
assert len(sim.get_pending_calls()) == 0
assert sim.total_calls() == 5
def test_easy_first_call_arrives_at_t1():
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(1)
assert len(sim.get_pending_calls()) == 1
def test_dispatch_marks_unit_busy():
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(1) # release CALL-001
pending = sim.get_pending_calls()
assert len(pending) == 1
call = pending[0]
# Pick any unit
unit = next(iter(sim.units.values()))
n_avail_before = len(sim.get_available_units())
reward, msg = sim.dispatch(call.call_id, unit.unit_id)
assert "Dispatched" in msg
assert reward >= 0.0
assert len(sim.get_available_units()) == n_avail_before - 1
def test_dispatch_unknown_call_returns_negative():
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(1)
reward, msg = sim.dispatch("CALL-NONEXISTENT", "ALS-1")
assert reward < 0
assert "not found" in msg.lower()
def test_dispatch_unavailable_unit_returns_negative():
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(1)
pending = sim.get_pending_calls()
sim.dispatch(pending[0].call_id, "ALS-1")
# ALS-1 is now busy. Try again with a fresh call (after time passes).
sim.advance_time(2)
pending2 = sim.get_pending_calls()
if pending2:
reward, msg = sim.dispatch(pending2[0].call_id, "ALS-1")
assert reward < 0
def test_episode_eventually_completes():
"""Run the easy scenario forward time-only — episode should hit time limit."""
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(60) # over the 30-min limit
assert sim.episode_done is True
def test_silent_agent_easy_scores_low():
"""Doing nothing on easy should score below 0.15."""
sim = DispatchSimulation(load_scenario("easy"), seed=42)
sim.advance_time(60)
r = grade_simulation(sim)
assert r.total < 0.15, f"silent agent scored {r.total}"
def _run_simple_heuristic(sim: "DispatchSimulation", max_steps: int = 200) -> None:
"""Tiny rule-based agent used by tests to confirm the env discriminates.
Picks the most-critical pending call and assigns the most effective
available unit to it; otherwise waits one minute.
"""
from reward import get_effectiveness
from utils import calculate_distance
steps = 0
while not sim.episode_done and steps < max_steps:
pending = sim.get_pending_calls()
avail = sim.get_available_units()
if not pending or not avail:
sim.advance_time(1)
steps += 1
continue
sev_w = {1: 6.0, 2: 4.0, 3: 2.0, 4: 1.0, 5: 0.5}
best = None
best_score = float("-inf")
for call in pending:
w = sev_w.get(
call.reported_severity.value if call.reported_severity else 5, 1.0
)
for unit in avail:
eff = get_effectiveness(unit.unit_type, call.true_type)
if eff < 0.30:
continue
dist = calculate_distance(unit.position, call.location)
penalty = 0.5 if (
unit.unit_type.value == "als_ambulance"
and call.true_severity.value >= 4
) else 0.0
score = w * eff - penalty - 0.05 * dist
if score > best_score:
best_score = score
best = (call, unit)
if best is None:
sim.advance_time(1)
else:
call, unit = best
sim.dispatch(call_id=call.call_id, unit_id=unit.unit_id)
sim.advance_time(1)
steps += 1
def test_heuristic_easy_beats_silent():
"""A simple heuristic agent should score noticeably above silent."""
sim = DispatchSimulation(load_scenario("easy"), seed=42)
_run_simple_heuristic(sim)
r = grade_simulation(sim)
assert r.total > 0.30, f"heuristic on easy scored only {r.total}"
def test_difficulty_progression():
"""Heuristic should score higher on easy than on hard."""
scores = {}
for task in ("easy", "medium", "hard"):
sim = DispatchSimulation(load_scenario(task), seed=42)
_run_simple_heuristic(sim)
scores[task] = grade_simulation(sim).total
assert scores["easy"] > scores["hard"], (
f"heuristic easy {scores['easy']} should beat hard {scores['hard']}"
)
if __name__ == "__main__":
test_load_all_scenarios()
test_easy_scenario_starts_with_no_pending()
test_easy_first_call_arrives_at_t1()
test_dispatch_marks_unit_busy()
test_dispatch_unknown_call_returns_negative()
test_dispatch_unavailable_unit_returns_negative()
test_episode_eventually_completes()
test_silent_agent_easy_scores_low()
test_heuristic_easy_beats_silent()
test_difficulty_progression()
print("All simulation tests passed!")