171 lines
7.4 KiB
Python
171 lines
7.4 KiB
Python
import json
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from test_audit_regressions import launch_fixture, measurement, seed_stable_inbound
|
|
|
|
|
|
def detect_once(sim):
|
|
sim.radar_tracks.clear()
|
|
sim.radar_arrivals.append({**measurement(sim.time_s), 'arrival_time_s': sim.time_s})
|
|
sim._deliver_radar_tracks()
|
|
|
|
|
|
def deliver_commands(sim):
|
|
sim.time_s = max(c['delivery_time_s'] for c in sim.pending_drone_commands)+.01
|
|
sim._deliver_drone_commands()
|
|
|
|
|
|
def test_first_point_does_not_launch_and_a_stable_heading_sends_one_drone():
|
|
sim = launch_fixture()
|
|
detect_once(sim)
|
|
assert not sim.pending_drone_commands
|
|
assert not sim._early_climbs
|
|
seed_stable_inbound(sim, position=(1000., 0., -20.), velocity=(-50., 0., 0.))
|
|
detect_once(sim)
|
|
assert len(sim.pending_drone_commands) == 1
|
|
assert sim.defense_assignments['p']['defender_ids'] == ['d0']
|
|
assert not sim._early_climbs
|
|
deliver_commands(sim)
|
|
launched = next(d for d in sim.drones if d.entity_id == 'd0')
|
|
assert launched.flight_reference['path_follow']
|
|
assert launched.activated_for == 'p'
|
|
start = launched.position_ned_m.copy()
|
|
for _ in range(1500):
|
|
sim.step()
|
|
moved = launched.position_ned_m - start
|
|
assert moved[0] < -150.
|
|
assert abs(moved[0]) > abs(moved[2]) * 3.
|
|
snapshot = sim.snapshot()
|
|
row = next(r for r in snapshot['entities'] if r['id'] == 'd0')
|
|
assert row['flight_phase'] == 'assigned'
|
|
intention = np.array(row['intended_trajectory_ned_m'])
|
|
assert intention[-1, 0] < intention[0, 0] - 100.
|
|
json.dumps(snapshot, allow_nan=False)
|
|
|
|
|
|
def test_later_radar_samples_stay_on_the_same_drone():
|
|
sim = launch_fixture()
|
|
seed_stable_inbound(sim, position=(1000., 0., -20.), velocity=(-50., 0., 0.))
|
|
detect_once(sim)
|
|
committed = set(sim.threat_responses['p']['defender_ids'])
|
|
assert committed == set(sim.defense_assignments['p']['defender_ids'])
|
|
assert len(committed) == 1
|
|
sim.time_s = 5.
|
|
far = {**measurement(5.), 'position_ned_m': np.array([-8000., 8000., -20.]), 'arrival_time_s': 5.}
|
|
sim.radar_arrivals.append(far)
|
|
sim._deliver_radar_tracks()
|
|
assert set(sim.defense_assignments['p']['defender_ids']) == committed
|
|
assert set(sim.threat_responses['p']['defender_ids']) == committed
|
|
assert sum(d.entity_id in committed for d in sim.drones) == 1
|
|
|
|
|
|
def test_a_tentative_track_climbs_one_drone_to_the_attacker_altitude():
|
|
sim = launch_fixture()
|
|
position = np.array([1000., 0., -800.])
|
|
velocity = np.array([-80., 0., 0.])
|
|
sim.radar_sample_history['r'] = [{
|
|
'target_id': 'p', 'measurement_time_s': sim.time_s - 0.8 + step,
|
|
'position_ned_m': (position + velocity * (step - 0.8)).tolist(),
|
|
'lateral_sigma_m': 40.0, 'vertical_sigma_m': 25.0, 'accepted': True,
|
|
} for step in (0.0, 0.8)]
|
|
sim._request_defender('p')
|
|
climbs = [c for c in sim.pending_drone_commands if c.get('kind') == 'attacker_climb']
|
|
assert len(climbs) == 1
|
|
assert climbs[0]['drone_id'] == 'd0'
|
|
assert climbs[0]['altitude_m'] > 400.
|
|
assert 'p' not in sim.defense_assignments
|
|
deliver_commands(sim)
|
|
climber = next(d for d in sim.drones if d.entity_id == 'd0')
|
|
assert climber.activated_for is None and climber.flight_reference is None
|
|
assert climber.target_position_ned_m[2] == pytest.approx(-climbs[0]['altitude_m'])
|
|
assert all(d.target_position_ned_m is None for d in sim.drones if d.entity_id != 'd0')
|
|
seed_stable_inbound(sim, position=(1000., 0., -800.), velocity=(-80., 0., 0.))
|
|
sim._request_defender('p')
|
|
assert sim.defense_assignments['p']['defender_ids'] == ['d0']
|
|
assert len(sim.defense_assignments) == 1
|
|
assert 'p' not in sim._early_climbs
|
|
|
|
|
|
def test_total_command_loss_does_not_launch():
|
|
sim = launch_fixture(loss=1.)
|
|
seed_stable_inbound(sim, position=(1000., 0., -20.), velocity=(-50., 0., 0.))
|
|
detect_once(sim)
|
|
for t in (sim.time_s + 1., sim.time_s + 2., sim.time_s + 3.):
|
|
sim.time_s = t
|
|
sim._service_assignments()
|
|
assert not sim.pending_drone_commands
|
|
assert not sim.defense_assignments
|
|
assert all(d.available and d.activated_for is None for d in sim.drones)
|
|
|
|
|
|
def test_late_climb_packet_cannot_override_assignment():
|
|
sim = launch_fixture()
|
|
detect_once(sim)
|
|
spare = sim.drones[-1]
|
|
sim.pending_drone_commands.append({
|
|
"kind": "attacker_climb", "drone_id": spare.entity_id,
|
|
"threat_id": 'p', "altitude_m": 900.,
|
|
"hold_ned_m": spare.position_ned_m.copy(),
|
|
"computed_time_s": sim.time_s, "delivery_time_s": sim.time_s + 1.})
|
|
spare.activated_for = 'p'
|
|
spare.target_position_ned_m = np.array([123., 456., -80.])
|
|
deliver_commands(sim)
|
|
assert np.array_equal(spare.target_position_ned_m, [123., 456., -80.])
|
|
assert spare.activated_for == 'p'
|
|
|
|
|
|
def test_traces_persist_longer_than_old_six_second_window_and_samples_persist():
|
|
sim = launch_fixture()
|
|
sim.radar_detection_points['p'] = [[1000., 0., -20.]]
|
|
for _ in range(1000):
|
|
sim.step()
|
|
snapshot = sim.snapshot()
|
|
path = next(e['trajectory_ned_m'] for e in snapshot['entities'] if e['id'] == 'p')
|
|
assert path[0] == [1000., 0., -20.]
|
|
assert len(path) >= 50
|
|
assert snapshot['radar_detection_points']['p'] == [[1000., 0., -20.]]
|
|
|
|
|
|
def test_intention_preview_does_not_advance_controller_or_truth():
|
|
sim = launch_fixture()
|
|
seed_stable_inbound(sim)
|
|
sim._activate_defenders(sim.radar_tracks)
|
|
deliver_commands(sim)
|
|
d = next(d for d in sim.drones if d.flight_reference is not None)
|
|
position, target = d.position_ned_m.copy(), d.target_position_ned_m.copy()
|
|
travel, elapsed = d.flight_reference['travel_m'], d.flight_reference['elapsed_s']
|
|
path = np.array(d.intended_trajectory())
|
|
assert len(path) == 61 and np.isfinite(path).all()
|
|
# The preview runs toward the frontrun station and stays on that segment.
|
|
anchor = np.asarray(d.flight_reference['anchor'])
|
|
to_anchor = anchor[:2] - path[0, :2]
|
|
moved = path[-1, :2] - path[0, :2]
|
|
assert float(np.dot(moved, to_anchor)) > 0
|
|
cross = abs(float(to_anchor[0] * moved[1] - to_anchor[1] * moved[0]))
|
|
assert cross / max(float(np.linalg.norm(to_anchor)), 1.0) < 40
|
|
assert np.array_equal(position, d.position_ned_m)
|
|
assert np.array_equal(target, d.target_position_ned_m)
|
|
assert (travel, elapsed) == (d.flight_reference['travel_m'], d.flight_reference['elapsed_s'])
|
|
|
|
|
|
def test_a_closer_drone_takes_the_climb_and_the_first_returns_home():
|
|
sim = launch_fixture()
|
|
sim.scenario_config = {"drone_radius_km": 9.0}
|
|
far, near = sim.drones[0], sim.drones[1]
|
|
far.position_ned_m[:] = [0., 9000., 0.]
|
|
near.position_ned_m[:] = [9000., 0., 0.]
|
|
sim.remote_drone_estimates[far.entity_id] = far.position_ned_m.copy()
|
|
sim.remote_drone_estimates[near.entity_id] = near.position_ned_m.copy()
|
|
forecast = {"position": np.array([25000., 0., -1500.]),
|
|
"velocity": np.array([-140., 0., 12.]), "quality": "tentative"}
|
|
sim._early_climbs['p'] = sim._open_climb('p', far)
|
|
sim._relieve_climber('p', forecast)
|
|
assert sim._early_climbs['p']['drone_id'] == near.entity_id
|
|
assert far.available and far.activated_for is None and far.flight_reference is None
|
|
assert far.target_position_ned_m[:2] == pytest.approx([0., 9000.])
|
|
assert far.target_position_ned_m[2] == pytest.approx(0.)
|
|
assert [e['type'] for e in sim.events if e['type'] == 'drone_recalled'] == ['drone_recalled']
|
|
sim._launched_drones['p'] = ['a', 'b', 'c']
|
|
assert sim._closer_relief('p', forecast, near.entity_id) is None
|