Files
overwatch/simulation/tests/test_presets.py
thebadking 34d4406611
Some checks failed
AERIS-10 CI / Python Lint + Tests (push) Has been cancelled
AERIS-10 CI / MCU Firmware Tests (push) Has been cancelled
AERIS-10 CI / FPGA Regression (push) Has been cancelled
AERIS-10 CI / Cross-Layer Contract Tests (push) Has been cancelled
Initial commit.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 07:49:17 +00:00

114 lines
6.1 KiB
Python

import numpy as np
import pytest
from defense_sim.simulation.presets import PRESETS, sample_preset
from defense_sim.simulation.scenarios import basic_intercept
def test_random_selection_reproduces_and_covers_all_classes():
a, b = np.random.default_rng(19), np.random.default_rng(19)
samples = [sample_preset("random", a) for _ in range(100)]
assert samples == [sample_preset("random", b) for _ in range(100)]
assert {s["preset"] for s in samples} == set(PRESETS)
assert len({s["entry_speed_m_s"] for s in samples}) == 100
@pytest.mark.parametrize("name", PRESETS)
def test_preset_parameters_and_initial_velocity(name):
engine = basic_intercept(seed=7, projectile_preset=name, wind_speed_m_s=0)
profile = engine.scenario_config["projectile_profile"]
bounds = PRESETS[name]
for field in ("mass_kg", "area_m2", "drag_coefficient"):
low, high = getattr(bounds, field)
assert low <= profile[field] <= high
p = engine.projectiles[0]
# Mid-flight launch: the vehicle starts on the approach ring at its
# sampled entry altitude, moving toward the strike point (the run models
# the terminal phase, starting outside the outer detection ring).
approach_m = engine.scenario_config["approach_range_km"] * 1000.0
assert np.allclose(p.position_ned_m, [approach_m, 0.0, -profile["entry_altitude_m"]])
assert np.isclose(np.linalg.norm(p.velocity_ned_m_s), profile["entry_speed_m_s"])
# The velocity is the straight line from the start position to the strike
# point (the initial glide-slope aim; terminal guidance re-aims and
# flattens it into a descent as the flight develops -- the initial line
# can climb when the target is close and the entry altitude is high).
target = np.asarray(engine.scenario_config["target_position_ned_m"])
aim = np.array([target[0] - approach_m, target[1], profile["entry_altitude_m"]])
expected_dir = aim / np.linalg.norm(aim)
assert np.allclose(p.velocity_ned_m_s / profile["entry_speed_m_s"], expected_dir, atol=1e-9)
# The vehicle always closes horizontal distance to the strike point.
assert np.dot(p.velocity_ned_m_s[:2], target[:2] - p.position_ned_m[:2]) > 0
# The guided preset carries a terminal guidance law aimed at the target.
assert p.guidance is not None
assert np.allclose(p.guidance.target_ned_m, target)
# The drag term opposes motion through the air at launch.
acceleration = p.derivative(np.concatenate((p.position_ned_m, p.velocity_ned_m_s)), np.zeros(3))[3:]
drag = acceleration - np.array([0., 0., 9.80665])
assert np.dot(drag, p.velocity_ned_m_s) < 0
def test_scenario_ring_geometry():
engine = basic_intercept(seed=3, projectile_preset="shahed_238",
protected_radius_km=10, radar_radius_km=12,
drone_radius_km=28, radar_station_count=8)
protected = engine.city_circle.radius_m
radar_ring = engine.scenario_config["radar_radius_km"] * 1000.0
drone_ring = engine.scenario_config["drone_radius_km"] * 1000.0
assert protected == 10_000
assert radar_ring == 12_000
assert drone_ring == 28_000
# Defense drones loiter on the drone loiter ring.
for drone in engine.drones:
assert np.isclose(np.hypot(*drone.position_ned_m[:2]), drone_ring)
# Radar stations sit on the radar site ring.
for radar in engine.radars:
assert np.isclose(np.hypot(*radar.position_ned_m[:2]), radar_ring)
# The randomized strike target lands inside the protected circle.
target = np.asarray(engine.scenario_config["target_position_ned_m"])
assert np.hypot(*target[:2]) <= protected
# Snapshot exposes the ring radii for the renderer.
snapshot = engine.snapshot()
assert snapshot["environment"]["protected_radius_m"] == protected
assert snapshot["environment"]["radar_radius_m"] == radar_ring
assert snapshot["environment"]["drone_radius_m"] == drone_ring
assert snapshot["environment"]["defense_perimeter_radius_m"] == drone_ring
def test_terminal_guidance_holds_speed_and_reaches_target():
# A guided loitering munition keeps cruise speed against drag (thrust
# balance) and steers to the strike point: within the radar range it must
# be tracked, the defenders must activate, and the impact must land near
# the target -- it cannot stall and fall short.
engine = basic_intercept(seed=7, projectile_preset="shahed_238",
approach_range_km=12, protected_radius_km=8,
radar_radius_km=10, drone_radius_km=14,
wind_speed_m_s=0, defense_drone_count=4)
p = engine.projectiles[0]
entry_speed = engine.scenario_config["projectile_profile"]["entry_speed_m_s"]
target = np.asarray(engine.scenario_config["target_position_ned_m"])
assert p.guidance is not None and np.allclose(p.guidance.target_ned_m, target)
radar_crossed = False
for _ in range(20000): # up to 200 s
engine.step()
pos = p.position_ned_m
if not p.active:
break
if not radar_crossed and np.hypot(*pos[:2]) < 10_000 and pos[2] < 0:
radar_crossed = True
# speed must still be near cruise when the track becomes usable
assert np.isclose(np.linalg.norm(p.velocity_ned_m_s), entry_speed, rtol=0.15)
assert radar_crossed, "vehicle never reached the radar ring; guidance stalled"
assert any(e["type"] == "radar_track_update" for e in engine.events)
# This sparse layout is not guaranteed feasible: detection and a valid
# threatening flight do not imply the defenders can launch in time.
assert any(e["type"] in {"defense_assignment_infeasible", "defenders_assigned"} for e in engine.events)
impacts = [e for e in engine.events if e["type"] == "ground_impact"]
assert impacts, "vehicle never reached the ground"
impact = np.asarray(impacts[0]["position_ned_m"])
assert np.linalg.norm(impact[:2] - target[:2]) < 2_000, "impact far from strike target"
def test_unknown_preset_is_rejected():
with pytest.raises(ValueError, match="unknown projectile preset"):
basic_intercept(seed=1, projectile_preset="unknown")