feat(simulation): add Worker AI polygon runtime and terrain navigation

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:40:45 +03:00
parent a7c64e009d
commit f01bd39037
88 changed files with 9918 additions and 108 deletions
+398
View File
@@ -0,0 +1,398 @@
"""Synthetic protocol checks; these do not qualify rendering or AI model quality."""
import base64
import hashlib
import io
import json
import numpy as np
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from PIL import Image
from pydantic import ValidationError
from test_observatory_recorded_jobs import _definitions
from k1link.observatory.recorded_jobs import ObservatoryRecordedJobQueue
from k1link.simulation.ai_polygon.contracts import (
Decision,
RunApplied,
RunCreate,
RunSample,
WorkerHello,
WorkerWorldCreate,
WorldCreate,
WorldSettings,
)
from k1link.simulation.ai_polygon.policy import RoadPolicy
from k1link.simulation.ai_polygon.runs import RunStore
from k1link.simulation.ai_polygon.worlds import WorldStore, inspect_gaussian_ply
from k1link.web.ai_polygon_api import build_ai_polygon_router
def ply(value=0.0):
names = [
"x",
"y",
"z",
"opacity",
"f_dc_0",
"f_dc_1",
"f_dc_2",
"scale_0",
"scale_1",
"scale_2",
"rot_0",
"rot_1",
"rot_2",
"rot_3",
]
header = "ply\nformat binary_little_endian 1.0\nelement vertex 1\n"
header += "".join(f"property float {name}\n" for name in names) + "end_header\n"
return header.encode() + np.full(14, value, dtype="<f4").tobytes()
def make_world(worlds):
data = ply()
world = worlds.create(
WorldCreate(
name="Synthetic protocol fixture",
filename="fixture.PLY",
byte_length=len(data),
author="test",
license="CC0",
)
)
worlds.append(world["world_id"], 0, data)
worlds.complete(world["world_id"])
return worlds.configure(world["world_id"], WorldSettings(prepared=True))
@pytest.fixture
def runs(tmp_path):
queue = ObservatoryRecordedJobQueue(tmp_path, definitions=_definitions())
worlds = WorldStore(tmp_path)
store = RunStore(worlds, queue)
hello = WorkerHello(
worker_id="worker-006",
instance_id="a" * 32,
runtime="isaac-sim-6.1",
model_ids=["ddrnet-goose-pytorch-reference", "rf_detr_large"],
profile_sha256="b" * 64,
runtime_sources={key: "c" * 64 for key in ("worker", "scene", "models", "robot")},
)
store.register(hello)
return store, hello, make_world(worlds)
def sample(sequence=0, **changes):
stream = io.BytesIO()
Image.new("RGB", (800, 600)).save(stream, "JPEG")
row = dict(
sequence=sequence,
simulation_time_ns=sequence * 100_000_000,
inference_ms=5,
pose_xy=(0, 0),
decision=Decision(
speed_mps=0, yaw_rate_rps=0, reason="no-road", road_fraction=0, obstacle_count=0
),
image_jpeg_base64=base64.b64encode(stream.getvalue()).decode(),
)
return RunSample(**{**row, **changes})
def test_resume_and_atomic_publication_recovery(tmp_path):
worlds = WorldStore(tmp_path)
data = ply()
row = worlds.create(
WorldCreate(
name=" Test ", filename="scan.ply", byte_length=len(data), author="Test", license="CC0"
)
)
key = row["world_id"]
worlds.append(key, 0, data[:20])
assert worlds.prefix_hashes(key)["chunks"] == [
{"byte_length": 20, "sha256": hashlib.sha256(data[:20]).hexdigest()}
]
with pytest.raises(RuntimeError):
worlds.append(key, 0, data[:20])
with pytest.raises(RuntimeError):
worlds.complete(key)
worlds.append(key, 20, data[20:])
directory = worlds.directory(key)
(directory / "source.part").replace(directory / "source.ply")
recovered = WorldStore(tmp_path).complete(key)
assert recovered["status"] == "available"
assert recovered["sha256"] == hashlib.sha256(data).hexdigest()
assert recovered["name"] == "Test"
assert worlds.complete(key) == recovered
@pytest.mark.parametrize(
"data", [b"not ply", ply()[:-1], ply() + b"xxxx", ply(float("nan")), ply(float("inf"))]
)
def test_invalid_ply_rejected(tmp_path, data):
path = tmp_path / "bad.ply"
path.write_bytes(data)
with pytest.raises(ValueError):
inspect_gaussian_ply(path)
@pytest.mark.parametrize(
"settings",
[{"spawn_xy": [float("nan"), 0]}, {"rotation_degrees": [0, 0, 900]}, {"max_speed_mps": 2}],
)
def test_nonfinite_or_out_of_bounds_settings_rejected(settings):
with pytest.raises(ValidationError):
WorldSettings(**settings)
def test_start_snapshots_and_idempotency(runs):
store, hello, world = runs
request = RunCreate(world_id=world["world_id"], max_steps=2)
row = store.start(request, "request-001")
assert store.start(request, "request-001") == row
store.worlds.configure(world["world_id"], WorldSettings(max_speed_mps=0.5, prepared=True))
assert store.get(row["run_id"])["world"]["settings"]["max_speed_mps"] == 0.3
with pytest.raises(RuntimeError):
store.start(RunCreate(world_id=world["world_id"], max_steps=3), "request-001")
with pytest.raises(RuntimeError):
store.start(request, "request-002")
assert store.poll(hello.instance_id, None)["action"] == "load"
def test_sequence_clock_pause_step_stop_and_resource_release(runs):
store, hello, world = runs
key = store.start(RunCreate(world_id=world["world_id"], max_steps=2), "request-001")["run_id"]
with pytest.raises(RuntimeError):
store.control(key, "step")
store.poll(hello.instance_id, key)
store.sample(key, hello.instance_id, sample())
with pytest.raises(RuntimeError):
store.sample(key, hello.instance_id, sample())
store.applied(
key,
hello.instance_id,
RunApplied(sequence=0, simulation_time_ns=100_000_000, physics_steps=6, pose_xy=(0, 0)),
)
with pytest.raises(ValueError):
store.sample(key, hello.instance_id, sample(1, simulation_time_ns=999))
store.control(key, "pause")
assert store.get(key)["state"] == "running"
assert store.poll(hello.instance_id, key)["run"]["state"] == "paused"
store.control(key, "step")
assert store.poll(hello.instance_id, key)["action"] == "step"
assert store.poll(hello.instance_id, key)["action"] == "pause"
store.control(key, "stop")
with pytest.raises(RuntimeError):
store.sample(key, hello.instance_id, sample(1))
store.finish(key, hello.instance_id, "stopped", "")
store.queue.reserve_simulation("airun-" + "b" * 32)
def test_expired_worker_stops_run_but_does_not_claim_gpu_released(runs):
store, hello, world = runs
key = store.start(RunCreate(world_id=world["world_id"]), "request-001")["run_id"]
store.seen -= 21
assert not store.status()["available"]
assert store.get(key)["state"] == "failed"
with pytest.raises(RuntimeError):
store.sample(key, hello.instance_id, sample())
with pytest.raises(RuntimeError):
store.queue.reserve_simulation("airun-" + "b" * 32)
store.register(hello)
assert store.finish(key, hello.instance_id, "failed", "")["state"] == "failed"
store.queue.reserve_simulation("airun-" + "b" * 32)
def test_restart_fences_worker_and_preserves_journal(runs):
store, hello, world = runs
key = store.start(RunCreate(world_id=world["world_id"], max_steps=2), "request-001")["run_id"]
store.poll(hello.instance_id, key)
store.sample(key, hello.instance_id, sample())
restarted = RunStore(store.worlds, store.queue)
assert restarted.get(key)["state"] == "failed"
with pytest.raises(RuntimeError):
restarted.sample(key, hello.instance_id, sample(1))
saved = json.loads((store.directory(key) / "decisions.jsonl").read_text())
assert (
saved["image_sha256"]
== hashlib.sha256((store.directory(key) / "frames/000000.jpg").read_bytes()).hexdigest()
)
assert "image_jpeg_base64" not in saved
def test_completion_requires_expected_steps(runs):
store, hello, world = runs
key = store.start(RunCreate(world_id=world["world_id"], max_steps=1), "request-001")["run_id"]
with pytest.raises(RuntimeError):
store.finish(key, hello.instance_id, "completed", "")
store.poll(hello.instance_id, key)
store.sample(key, hello.instance_id, sample())
with pytest.raises(RuntimeError):
store.finish(key, hello.instance_id, "completed", "")
store.applied(
key,
hello.instance_id,
RunApplied(sequence=0, simulation_time_ns=100_000_000, physics_steps=6, pose_xy=(0, 0)),
)
assert store.finish(key, hello.instance_id, "completed", "")["state"] == "completed"
def test_api_auth_and_bounded_upload(tmp_path):
app = FastAPI()
app.include_router(build_ai_polygon_router(tmp_path))
with TestClient(app) as client:
root = "/api/v1/ai-polygon"
assert client.get(root + "/catalog").json()["runtime"]["available"] is False
assert client.post(root + "/worker/poll", json={"instance_id": "a" * 32}).status_code == 401
assert client.post(root + "/worlds", json={"filename": "../bad.ply"}).status_code == 422
world = make_world(WorldStore(tmp_path))
response = client.get(root + f"/worlds/{world['world_id']}/source.ply")
assert response.status_code == 200 and response.content == ply()
assert (
client.post(
root + "/runs",
headers={"Idempotency-Key": "test-run-001"},
json={"world_id": world["world_id"]},
).status_code
== 409
)
assert not any((tmp_path / "ai-polygon/runs").iterdir())
def test_camera_policy_brakes_and_waits_before_resume():
policy = RoadPolicy(0.3)
road = np.ones((512, 512), dtype=bool)
assert policy.decide(road, []).speed_mps == 0
policy.decide(road, [])
assert policy.decide(road, []).speed_mps == 0.3
obstacle = policy.decide(road, [(0.4, 0.2, 0.6, 0.8)])
assert obstacle.reason == "obstacle" and obstacle.speed_mps == 0
assert policy.decide(road, []).speed_mps == 0
assert policy.decide(np.zeros_like(road), []).reason == "no-road"
with pytest.raises(ValueError):
policy.decide(road, [(float("nan"), 0, 1, 1)])
def worker_asset():
return WorkerWorldCreate(
name="Synthetic paired scene",
filename="fixture.ply",
byte_length=len(ply()),
author="test",
license="CC0",
sha256="a" * 64,
collider_sha256="b" * 64,
splat_count=1,
settings=WorldSettings(prepared=True),
)
def test_worker_asset_is_manifest_only_and_retry_preserves_settings(tmp_path):
worlds = WorldStore(tmp_path)
request = worker_asset()
row = worlds.register_worker_asset(request, "worker-006")
assert {p.name for p in worlds.directory(row["world_id"]).iterdir()} == {"world.json"}
changed = worlds.configure(row["world_id"], WorldSettings(spawn_xy=(3, 4), prepared=True))
assert worlds.register_worker_asset(request, "worker-006") == changed
with pytest.raises(RuntimeError):
worlds.register_worker_asset(
request.model_copy(update={"collider_sha256": "c" * 64}), "worker-006"
)
with pytest.raises(RuntimeError):
worlds.prefix_hashes(row["world_id"])
def test_worker_asset_api_requires_current_authenticated_instance(tmp_path):
app = FastAPI()
app.include_router(build_ai_polygon_router(tmp_path))
base = "/api/v1/ai-polygon"
store = RunStore(WorldStore(tmp_path))
hello = WorkerHello(
worker_id="worker-006",
instance_id="a" * 32,
runtime="isaac-sim-6.1",
model_ids=["test"],
runtime_sources={k: "c" * 64 for k in ("worker", "scene", "models", "robot")},
profile_sha256="b" * 64,
)
auth = {"Authorization": "Bearer " + store.token, "Worker-Instance": hello.instance_id}
body = worker_asset().model_dump(mode="json")
with TestClient(app) as client:
assert (
client.post(
base + "/worker/worlds", json=body, headers={"Worker-Instance": hello.instance_id}
).status_code
== 401
)
assert client.post(base + "/worker/worlds", json=body, headers=auth).status_code == 409
assert (
client.post(
base + "/worker/register", json=hello.model_dump(), headers=auth
).status_code
== 200
)
response = client.post(base + "/worker/worlds", json=body, headers=auth)
assert response.status_code == 201
row = response.json()
assert row["storage"] == {"kind": "worker", "worker_id": "worker-006"}
assert client.get(base + f"/worlds/{row['world_id']}/source.ply").status_code == 409
assert (
client.post(
base + "/worker/worlds", json=body, headers={**auth, "Worker-Instance": "d" * 32}
).status_code
== 409
)
def test_camera_policy_turns_towards_connected_road():
policy = RoadPolicy(0.3)
road = np.zeros((512, 512), dtype=bool)
road[:, 100:260] = True
policy.decide(road, [])
policy.decide(road, [])
decision = policy.decide(road, [])
assert decision.yaw_rate_rps > 0 and 0 < decision.speed_mps < 0.3
def test_progress_does_not_claim_camera_ready(runs):
store, hello, world = runs
run = store.start(RunCreate(world_id=world["world_id"]), "progress-case-001")
store.progress(run["run_id"], hello.instance_id, "scene")
assert store.get(run["run_id"])["phase"] == "scene"
assert store.get(run["run_id"])["state"] == "starting"
store.control(run["run_id"], "stop")
assert store.progress(run["run_id"], hello.instance_id, "models")["control"] == "stop"
assert store.get(run["run_id"])["state"] == "stopping"
def test_worker_stop_race_accepts_already_exited_process(monkeypatch):
import importlib.util
from pathlib import Path
runtime = Path(__file__).resolve().parents[1] / "simulation/ai-polygon"
monkeypatch.syspath_prepend(str(runtime))
spec = importlib.util.spec_from_file_location("polygon_worker_stop_test", runtime / "worker.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
calls = []
class Child:
pid = 123
def poll(self):
return None
def wait(self, timeout):
calls.append(("wait", timeout))
return 0
def taskkill(*args, **kwargs):
# Windows reports a non-zero taskkill when the process exits in the race.
assert kwargs["check"] is False
calls.append(("taskkill", 255))
monkeypatch.setattr(module.subprocess, "run", taskkill)
module.terminate_episode(Child())
assert calls == [("taskkill", 255), ("wait", 30)]
+141
View File
@@ -0,0 +1,141 @@
import numpy as np
import pytest
from k1link.simulation.ai_polygon.contracts import WorldSettings
from k1link.simulation.ai_polygon.mission_policy import WaypointMission, inclination
def pose(x=0, y=0):
return [x, y, 0.37, 0, 0, 0, 1]
def goal(target, prior, excluded):
return [2, 0.7 * len(excluded), 0]
def test_chassis_jitter_cannot_hide_stall_and_retries_are_bounded():
mission = WaypointMission([[8, 0]])
states = []
for t in np.arange(0, 34, 0.2):
_, intent = mission.update(pose(0.01 * np.sin(t * 10)), float(t), goal)
states.append(intent["state"])
assert "replanning" in states
assert states[-1] == "stuck"
assert mission.update(pose(1), 35, goal)[1]["state"] == "stuck"
def test_route_cursor_and_completion_survive_pause_without_restarting_task():
mission = WaypointMission([[1, 0], [3, 0]])
mission.update(pose(), 0, goal)
assert mission.update(pose(1), 6, goal)[1]["waypoint"] == 1
mission.resume()
assert mission.update(pose(1), 6, goal)[1]["waypoint"] == 1
assert mission.update(pose(3), 18, goal)[1]["state"] == "goal-reached"
mission.resume()
assert mission.update(pose(3), 18, goal)[1]["state"] == "goal-reached"
def test_overturned_and_excessive_tilt_are_latched_before_goal_success():
mission = WaypointMission([[0, 0]])
overturned = [0, 0, 0.37, 1, 0, 0, 0]
assert inclination(overturned) == pytest.approx(180)
assert mission.update(overturned, 0, goal)[1]["state"] == "unstable"
mission.resume()
assert mission.update(pose(), 1, goal)[1]["state"] == "unstable"
def test_missing_surface_never_becomes_a_drive_permission_or_unbounded_wait():
mission = WaypointMission([[8, 0]])
for t in range(34):
target, intent = mission.update(pose(), t, lambda *_: None)
assert target is None
assert intent["state"] == "stuck"
def test_route_contract_bounds_and_finiteness():
from pydantic import ValidationError
for route in ([[float("nan"), 0]], [[0, 0]] * 33, [[0, 10001]]):
with pytest.raises(ValidationError):
WorldSettings(route_xy=route)
def test_recovery_uses_observed_goal_and_does_not_count_retreat_as_progress():
mission = WaypointMission([[8, 0]])
def retreat(prior):
return prior or [-0.65, 0, 0]
mission.update(pose(), 0, goal, retreat)
target, intent = mission.update(pose(), 8, goal, retreat)
assert target == [-0.65, 0, 0] and intent["state"] == "reversing"
assert mission.update(pose(-0.2), 10, goal, retreat)[1]["state"] == "reversing"
assert mission.update(pose(-0.4), 12, goal, retreat)[1]["state"] == "replanning"
mission.update(pose(), 16, goal, retreat)
assert mission.attempts == 1
# Moving back to where we started cannot create a fresh three-attempt budget.
assert mission.update(pose(), 20, goal, retreat)[1]["recovery_attempt"] == 2
def test_recovery_stops_immediately_on_lost_support_and_times_out_without_motion():
mission = WaypointMission([[8, 0]])
def retreat(prior):
return prior or [-0.65, 0, 0]
mission.update(pose(), 0, goal, retreat)
mission.update(pose(), 8, goal, retreat)
assert mission.update(pose(), 8.2, goal, lambda _: None)[0] is None
assert mission.recovery_goal is None
for seconds in (17, 23, 32, 38, 47):
_, intent = mission.update(pose(), seconds, goal, retreat)
assert intent["state"] == "stuck"
def test_wrong_direction_never_replenishes_route_attempts():
mission = WaypointMission([[8, 0]])
for seconds in range(34):
_, intent = mission.update(pose(-seconds * 0.1), seconds, goal)
assert intent["state"] == "stuck"
def test_signed_motion_contract_accepts_reverse_but_stays_bounded():
from pydantic import ValidationError
from k1link.simulation.ai_polygon.contracts import Decision
assert (
Decision(
speed_mps=-0.1, yaw_rate_rps=0, reason="replanning", road_fraction=0, obstacle_count=0
).speed_mps
< 0
)
with pytest.raises(ValidationError):
Decision(speed_mps=-1.1, yaw_rate_rps=0, reason="road", road_fraction=1, obstacle_count=0)
def test_slow_regulated_progress_is_not_mistaken_for_stall():
mission = WaypointMission([[8, 0]])
for seconds in range(60):
_, intent = mission.update(pose(seconds * 0.02), seconds, goal)
assert intent["state"] == "following"
assert intent["recovery_attempt"] == 0
def test_rejected_observation_stops_but_does_not_forget_revalidated_goal():
mission = WaypointMission([[2, 0]])
observed = [2, 0, 0]
assert mission.update(pose(), 0, lambda *_: observed)[0] == observed
assert mission.update(pose(1), 1, lambda *_: None)[0] is None
assert mission.goal == observed
mission.resume()
calls = []
def revalidate(_target, prior, _excluded):
calls.append(prior)
return prior
assert mission.update(pose(1.3), 1.2, revalidate)[0] == observed
assert calls == [observed]
# Memory alone never authorizes a command when the new frame is rejected.
assert mission.update(pose(1.3), 1.4, lambda *_: None)[0] is None
+40
View File
@@ -0,0 +1,40 @@
"""The actuator may soften acceleration but must never prolong a safety command."""
import importlib.util
from pathlib import Path
import pytest
spec = importlib.util.spec_from_file_location(
"motion_control", Path(__file__).parents[1] / "simulation/ai-polygon/motion_control.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
def test_slow_perception_keeps_continuous_physics_command():
drive = module.DriveEnvelope()
rows = [drive.step(0.15, 0, 1 / 60)[0] for _ in range(180)]
assert rows[0] == pytest.approx(0.2 / 60)
assert all(0 <= b - a <= 0.2 / 60 + 1e-9 for a, b in zip(rows, rows[1:], strict=False))
assert rows[44:] == pytest.approx([0.15] * 136)
def test_braking_and_deadman_bypass_ramp():
drive = module.DriveEnvelope()
for _ in range(60):
drive.step(0.15, 0, 1 / 60)
assert drive.step(0.03, 0, 1 / 60) == (0.03, 0)
assert drive.step(0.15, 0.2, 1 / 60, stop=True) == (0, 0)
assert drive.wheels == (0, 0)
assert drive.step(0.15, 0, 1 / 60)[0] == pytest.approx(0.2 / 60)
def test_curvature_and_direction_change():
drive = module.DriveEnvelope()
for _ in range(60):
speed, yaw = drive.step(0.15, 0.2, 1 / 60)
assert yaw / speed == pytest.approx(0.2 / 0.15)
speed, yaw = drive.step(-0.15, -0.2, 1 / 60)
assert speed < 0 and yaw < 0
assert max(map(abs, drive.wheels)) <= 0.2 / 60 + 1e-9
+390
View File
@@ -0,0 +1,390 @@
import importlib.util
import subprocess
import sys
from pathlib import Path
import numpy as np
import pytest
from k1link.observatory.modular_composition import CompositionError
from k1link.simulation.ai_polygon.composition import compose, registry
from k1link.simulation.ai_polygon.terrain_contract import terrain_matches
ROOT = Path(__file__).resolve().parents[1] / "simulation/ai-polygon"
def module(name):
spec = importlib.util.spec_from_file_location(name, ROOT / (name + ".py"))
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
def test_simulation_composition_has_causal_dependencies_and_separate_authority():
graph = compose(ROOT)
assert graph.as_dict()["execution"]["mode"] == "worker-local-simulation"
motion = graph.nodes[-1]
assert motion.module.group == "motion"
assert dict(motion.inputs)["segmentation.surface"] == "simulation-segformer-ade"
assert dict(motion.inputs)["detection.boxes"] == "simulation-rf-detr"
assert motion.module.state_policy == "causal-reset-at-source-start"
selection = graph.selection_document()
selection["selections"] = [r for r in selection["selections"] if r["group"] != "segmentation"]
with pytest.raises(CompositionError, match="segmentation.surface"):
compose(ROOT, selection)
def test_surface_providers_are_interchangeable_in_the_shared_constructor():
selection = compose(ROOT).selection_document()
reference = next(m for m in registry(ROOT).modules if m.module_id == "simulation-ddrnet-goose")
for row in selection["selections"]:
if row["group"] == "segmentation":
row.update(module_id=reference.module_id, module_sha256=reference.sha256)
graph = compose(ROOT, selection)
assert dict(graph.nodes[-1].inputs)["segmentation.surface"] == reference.module_id
def test_composition_rejects_stale_module_identity():
selection = compose(ROOT).selection_document()
selection["selections"][0]["module_sha256"] = "0" * 64
with pytest.raises(CompositionError, match="not installed"):
compose(ROOT, selection)
def test_shared_constructor_import_needs_no_core_or_third_party_runtime():
# -S removes site-packages, as in the minimal Windows coordinator. Loading
# a contract must not load the POSIX-only artifact gateway through __init__.
source = str(ROOT.parents[1] / "src")
result = subprocess.run(
[
sys.executable,
"-S",
"-c",
f"import sys; sys.path.insert(0, {source!r}); "
"from k1link.observatory.modular_composition import ModuleRegistry; "
"from k1link.simulation.ai_polygon.composition import compose; "
"assert 'k1link.artifact_gateway' not in sys.modules",
],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
def test_semantic_goal_uses_range_and_correct_square_camera_crop():
nav = module("navigation_client")
points = np.array(
[[x, y, 0] for x in np.linspace(1.5, 3, 20) for y in np.linspace(-0.3, 0.3, 9)],
dtype=np.float32,
)
pose = [0, 0, 0.27, 0, 0, 0, 1]
calibration = {
"origin": [0.38, 0, 0.8],
"rotation": np.eye(3).reshape(-1).tolist(),
"intrinsics": [800 * 24 / 36, 800 * 24 / 36, 400, 300],
}
leaves = np.ones((512, 512), dtype=bool)
goal = nav.visual_goal(leaves, points, pose, calibration)
assert goal is not None and 1.5 < goal[0] < 3 and abs(goal[1]) < 0.3
assert nav.visual_goal(np.zeros_like(leaves), points, pose, calibration) is None
assert nav.visual_goal(leaves, points + [0, 0, 2], pose, calibration) is None
# A previously valid goal cannot authorize motion through newly unknown RGB.
assert nav.visual_goal(np.zeros_like(leaves), points, pose, calibration, goal) is None
# An explicit-route waypoint entering the camera blind strip is retained,
# but losing all current visual surface support still forbids movement.
close = [0.65, 0, 0]
assert nav.visual_goal(leaves, points, pose, calibration, close, target=[0.65, 0]) == close
assert (
nav.visual_goal(np.zeros_like(leaves), points, pose, calibration, close, target=[0.65, 0])
is None
)
def test_ground_placement_uses_actual_triangle_intersection():
terrain = module("terrain")
vertices = np.array([[0, 0, 0], [1, 0, 0.2], [0, 1, 0]], dtype=np.float32)
faces = np.array([[0, 1, 2]], dtype=np.int32)
assert terrain.ground_intersections(vertices, faces, 0.25, 0.25)[0] == pytest.approx(0.05)
assert len(terrain.ground_intersections(vertices, faces, 0.9, 0.9)) == 0
def test_observed_route_goal_keeps_task_position_and_cannot_run_away_from_it():
choose = module("navigation_client").visual_goal
points = np.array(
[[x, y, 0] for x in np.arange(1.2, 3.1, 0.05) for y in np.arange(-0.5, 0.51, 0.05)]
)
pose = [0, 0, 0.37, 0, 0, 0, 1]
calibration = {
"origin": [0.38, 0, 0.8],
"rotation": np.eye(3).reshape(-1).tolist(),
"intrinsics": [800 * 24 / 36, 800 * 24 / 36, 400, 300],
"body_contact_height_m": 0.37,
}
surface = np.ones((512, 512), dtype=bool)
target = [2.03, 0.27]
assert choose(surface, points, pose, calibration, target=target) == pytest.approx([*target, 0])
# The close, already observed waypoint can enter the camera blind strip.
advanced = [1.5, 0, 0.37, 0, 0, 0, 1]
camera = {**calibration, "origin": [1.88, 0, 0.8]}
prior = [*target, 0]
assert choose(surface, points + [1.5, 0, 0], advanced, camera, prior, target) == prior
# The actual camera loses nearby ground beyond the old hardcoded 0.8 m.
advanced = [1.1, 0, 0.37, 0, 0, 0, 1]
camera = {**calibration, "origin": [1.48, 0, 0.8]}
assert choose(surface, points + [1.1, 0, 0], advanced, camera, prior, target) == prior
assert choose(np.zeros_like(surface), points, pose, calibration, target=target) is None
# Clear road ahead is not permission to drive away from a missed waypoint.
assert choose(surface, points, pose, calibration, target=[-1, 0]) is None
# A distant task may still use an observed local goal towards it.
far = choose(surface, points, pose, calibration, target=[8, 0])
assert far is not None and 1.2 <= far[0] <= 3.1
def test_collision_identity_follows_geometry_and_tile_coverage_not_camera_or_start():
settings = dict(
meters_per_unit=1,
rotation_degrees=[-90, 0, 180],
spawn_xy=[0, 0],
ground_z=0,
camera_height_m=0.8,
max_speed_mps=0.15,
)
terrain = dict(source_sha256="a" * 64, generator_sha256="b" * 64, settings=settings)
world = dict(sha256="a" * 64, settings={**settings, "spawn_xy": [1, 1], "camera_height_m": 1})
assert terrain_matches(terrain, world, "b" * 64)
assert not terrain_matches(terrain, world, "c" * 64)
assert not terrain_matches(terrain, {**world, "sha256": "d" * 64})
for change in ({"spawn_xy": [20, 0]}, {"meters_per_unit": 2}, {"rotation_degrees": [0, 0, 0]}):
assert not terrain_matches(terrain, {**world, "settings": {**world["settings"], **change}})
def test_paired_full_scene_does_not_inherit_generated_tile_bounds():
settings = dict(meters_per_unit=1, rotation_degrees=[90, 0, 0], spawn_xy=[0, 0], ground_z=5)
terrain = dict(
generator="paired-source",
source_sha256="a" * 64,
collider_sha256="b" * 64,
settings=settings,
)
world = dict(
sha256="a" * 64,
collider_sha256="b" * 64,
settings={**settings, "spawn_xy": [210, 30], "ground_z": 1.5},
)
assert terrain_matches(terrain, world)
assert not terrain_matches(terrain, {**world, "collider_sha256": "c" * 64})
assert not terrain_matches(
terrain, {**world, "settings": {**world["settings"], "meters_per_unit": 2}}
)
assert not terrain_matches({**terrain, "generator": "generated-tile"}, world)
def test_square_footprint_fits_straight_corridor_and_rejects_corner_sweep():
check = module("navigation/footprint").swept_footprint_clear
pose = [0, 0, 0.27, 0, 0, 0, 1]
path = [[0, 0, 0], [0.5, 0, 0], [1, 0, 0]]
walls = np.array([[x, y, 0.5, 0.5] for x in np.arange(-1, 2, 0.1) for y in [-0.65, 0.65]])
assert check(path, walls, pose)
walls[:, 1] *= 0.45 / 0.65
assert not check(path, walls, pose)
# A diagonal turn sweeps a square corner into this obstacle, even though
# the chassis at its initial and final straight poses does not contain it.
assert not check([[0, 0, 0], [0.5, 0.5, 0]], [[0.7, 0, 0.5, 0.5]], pose)
def test_command_monitor_covers_deadman_braking_distance_and_rotation():
check = module("navigation/footprint").command_footprint_clear
pose = [0, 0, 0.27, 0, 0, 0, 1]
assert check(0.15, 0, [[1.5, 0, 0.5, 0.5]], pose)
assert not check(0.15, 0, [[0.7, 0, 0.5, 0.5]], pose)
assert not check(0, 0.8, [[0.7, 0, 0.5, 0.5]], pose)
def test_smooth_slope_is_distinct_from_a_step_or_vertical_terrain():
costs = module("navigation/terrain_costs").supported_slope_costs
xy = np.array([[x, y] for x in np.arange(-1, 1.01, 0.1) for y in np.arange(-1, 1.01, 0.1)])
slope = np.column_stack((xy, xy[:, 0] * np.tan(np.radians(20)), np.full(len(xy), 0.15)))
normalized, corrected = costs(slope)
assert corrected > len(slope) * 0.9
assert normalized[len(slope) // 2, 3] == 0
assert np.array_equal(normalized[:, :3], slope[:, :3])
# A 15 cm ledge across the initial footprint cannot become a traversable ramp.
step = slope.copy()
step[:, 2] = np.where(step[:, 0] >= 0, 0.15, 0)
assert costs(step)[1] == 0
cliff = slope.copy()
cliff[:, 2] = np.where(cliff[:, 0] >= 0, -0.4, 0)
assert costs(cliff)[1] == 0
steep = slope.copy()
steep[:, 2] = steep[:, 0] * np.tan(np.radians(35))
assert costs(steep)[1] == 0
assert costs(slope[np.abs(slope[:, 1]) < 0.01])[1] == 0 # Unobserved lateral support.
def test_underbody_support_does_not_clear_future_terrain_walls_or_drops():
correct = module("navigation/terrain_costs").underbody_support_costs
terrain = np.array(
[
[-0.375, -0.28, 0.066, 0.103], # Low return already under the chassis.
[0.46, 0, 0.066, 0.103], # Inset excludes the leading edge.
[0.75, 0, 0.066, 0.103], # Never change future terrain from body pose.
[0, 0, 0.12, 0.12], # A real step within the footprint remains blocked.
[0, 0, 0.5, 0.5],
[0, 0, -0.4, 0.4],
]
)
original = terrain.copy()
result, count = correct(terrain, [0, 0, 0.37, 0, 0, 0, 1])
assert count == 1 and result[0, 3] == pytest.approx(0.066)
assert np.array_equal(result[1:], original[1:])
assert np.array_equal(terrain, original) # Never erase the causal raw map.
# Rotate both observations and the measured chassis; the result must agree.
yaw = np.pi / 2
rotated = terrain.copy()
rotated[:, :2] = terrain[:, :2] @ np.array([[0, 1], [-1, 0]]) + [3, 4]
pose = [3, 4, 0.37, 0, 0, np.sin(yaw / 2), np.cos(yaw / 2)]
assert np.allclose(correct(rotated, pose)[0][:, 3], result[:, 3])
assert correct(terrain, [0, 0, 0.37, 0, np.sin(np.pi / 12), 0, np.cos(np.pi / 12)])[1] == 0
def test_retreat_requires_observed_full_width_support_and_no_drop_or_step():
choose = module("navigation_client").recovery_goal
points = np.array(
[[x, y, 0.0] for x in np.arange(-1.5, -0.39, 0.05) for y in np.arange(-0.85, 0.86, 0.05)]
)
pose = [0, 0, 0.37, 0, 0, 0, 1]
calibration = {"body_contact_height_m": 0.37}
assert choose(points, pose, calibration) == pytest.approx([-0.65, 0, 0])
assert choose(points[points[:, 1] > -0.2], pose, calibration) is None
assert choose(points[points[:, 0] < -0.9], pose, calibration) is None
for height in (-0.4, 0.15):
discontinuous = points.copy()
discontinuous[points[:, 0] < -0.9, 2] = height
assert choose(discontinuous, pose, calibration) is None
slope = points.copy()
slope[:, 2] = slope[:, 0] * np.tan(np.radians(10))
assert choose(slope, pose, calibration) is not None
assert choose(points, pose, calibration, [-0.65, 0.3, 0]) is None
def test_reverse_monitor_checks_behind_the_body():
check = module("navigation/footprint").command_footprint_clear
pose = [0, 0, 0.37, 0, 0, 0, 1]
assert check(-0.1, 0, [[-1.5, 0, 0.5, 0.5]], pose)
assert not check(-0.1, 0, [[-0.65, 0, 0.5, 0.5]], pose)
def test_legacy_coordinate_migration_is_an_exact_rigid_rotation():
import json
import struct
migrate = module("navigation/migrate_terrain_coordinates")
positions = [[1.0, -2.0, -3.0], [2.0, -2.0, -3.0], [1.0, -1.0, -3.0]]
binary = struct.pack("<9f3I", *(v for p in positions for v in p), 0, 1, 2)
document = {
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0}, "indices": 1}]}],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"type": "VEC3",
"count": 3,
"min": [1, -2, -3],
"max": [2, -1, -3],
},
{"bufferView": 1, "componentType": 5125, "type": "SCALAR", "count": 3},
],
"bufferViews": [{"byteOffset": 0, "byteLength": 36}, {"byteOffset": 36, "byteLength": 12}],
}
raw = json.dumps(document).encode()
raw += b" " * ((-len(raw)) % 4)
glb = (
struct.pack("<III", 0x46546C67, 2, 28 + len(raw) + len(binary))
+ struct.pack("<II", len(raw), 0x4E4F534A)
+ raw
+ struct.pack("<II", len(binary), 0x004E4942)
+ binary
)
corrected = migrate.rotate_glb(glb)
length = struct.unpack_from("<I", corrected, 12)[0]
result = np.array(struct.unpack_from("<9f", corrected, 28 + length)).reshape(-1, 3)
assert np.array_equal(result, np.array(positions) * [-1, 1, -1])
assert np.array_equal(
result[:, [0, 2, 1]] * [1, -1, 1], [[-1, -3, -2], [-2, -3, -2], [-1, -3, -1]]
)
assert struct.unpack_from("<3I", corrected, 28 + length + 36) == (0, 1, 2)
def test_voxel_quantized_grade_does_not_become_a_wall_but_ledge_remains():
costs = module("navigation/terrain_costs").supported_slope_costs
xy = np.array([[x, y] for x in np.arange(-1, 1.01, 0.06) for y in np.arange(-1, 1.01, 0.06)])
height = np.round(xy[:, 0] * np.tan(np.radians(20)) / 0.06) * 0.06
surface = np.column_stack((xy, height, np.full(len(xy), 0.15)))
assert costs(surface)[1] > len(surface) * 0.8
for discontinuity in (0.12, 0.15, -0.4):
ledge = surface.copy()
ledge[:, 2] = np.where(xy[:, 0] >= 0, discontinuity, 0)
corrected, _ = costs(ledge)
near_edge = np.abs(xy[:, 0]) < 0.12
assert np.all(corrected[near_edge, 3] > 0.1)
stone = surface.copy()
stone[:, 2] = 0
stone[(abs(xy[:, 0]) < 0.12) & (abs(xy[:, 1]) < 0.12), 2] = 0.15
assert costs(stone)[1] == 0
def test_grade_fit_cannot_bridge_an_unobserved_gap():
costs = module("navigation/terrain_costs").supported_slope_costs
points = np.array(
[
[x, y, 0 if x < 0 else 0.15, 0.15]
for x in [-0.3, -0.2, 0.2, 0.3]
for y in np.arange(-0.3, 0.31, 0.1)
]
)
assert costs(points)[1] == 0
def test_existing_reserve_overlap_only_allows_departure_not_approach_or_body_overlap():
check = module("navigation/footprint").command_footprint_clear
pose = [0, 0, 0.37, 0, 0, 0, 1]
behind = [[-0.53, 0, 0.5, 0.5]]
assert check(0.15, 0, behind, pose)
assert not check(-0.1, 0, behind, pose)
assert not check(0, 0.35, behind, pose)
assert not check(0.15, 0, [[-0.49, 0, 0.5, 0.5]], pose)
assert not check(0.15, 0, [[0.53, 0, 0.5, 0.5]], pose)
# A longer admitted camera age must also enlarge the collision envelope.
assert not check(0.15, 0, [[0.80, 0, 0.5, 0.5]], pose)
def test_terrain_fit_cache_invalidates_when_a_new_obstacle_is_observed():
costs = module("navigation/terrain_costs")
normalize = costs.TerrainCostNormalizer()
xy = np.array([[x, y] for x in np.arange(-1, 1.01, 0.1) for y in np.arange(-1, 1.01, 0.1)])
grade = np.column_stack((xy, xy[:, 0] * np.tan(np.radians(20)), np.full(len(xy), 0.15)))
clear, count = normalize(grade)
assert count > len(grade) * 0.9
assert np.array_equal(normalize(grade)[0], clear)
changed = np.vstack((grade, [0.02, 0.02, 0.3, 0.3]))
cached, count = normalize(changed)
fresh, expected_count = costs.supported_slope_costs(changed)
assert np.array_equal(cached, fresh) and count == expected_count
assert cached[len(grade) // 2, 3] > 0.1
assert np.array_equal(normalize(grade)[0], clear)
def test_velocity_regulation_keeps_the_selected_arc_and_its_braking_clearance():
monitor = module("navigation/footprint")
pose = [0, 0, 0.37, 0, 0, 0, 1]
hazard = [[0.834, -0.08, 0.15, 0.15]]
speed, yaw, scale = monitor.regulate_command(0.15, -0.245, hazard, pose)
assert 0.2 <= scale < 1
assert speed / yaw == pytest.approx(0.15 / -0.245)
assert monitor.command_footprint_clear(speed, yaw, hazard, pose)
assert monitor.regulate_command(0.15, 0, [[0.56, 0, 0.2, 0.2]], pose) == (0, 0, 0)
assert monitor.regulate_command(0.15, 0, [[0.49, 0, 0.2, 0.2]], pose) == (0, 0, 0)
reverse, _, scale = monitor.regulate_command(-0.1, 0, [[-0.68, 0, 0.2, 0.2]], pose)
assert 0.2 <= scale < 1 and reverse < 0
assert monitor.command_footprint_clear(reverse, 0, [[-0.68, 0, 0.2, 0.2]], pose)
+303
View File
@@ -0,0 +1,303 @@
"""Lifecycle and clock-isolation checks; GPU/stream performance is qualified on Worker."""
import importlib.util
import threading
import time
from pathlib import Path
from types import SimpleNamespace
import numpy as np
import pytest
from pydantic import ValidationError
from test_ai_polygon import make_world, sample
from test_observatory_recorded_jobs import _definitions
from k1link.observatory.recorded_jobs import ObservatoryRecordedJobQueue
from k1link.simulation.ai_polygon.contracts import (
RealtimeSnapshot,
RunCreate,
StreamEndpoint,
WorkerHello,
)
from k1link.simulation.ai_polygon.runs import RunStore
from k1link.simulation.ai_polygon.worlds import WorldStore
@pytest.fixture
def realtime(tmp_path):
queue = ObservatoryRecordedJobQueue(tmp_path, definitions=_definitions())
store = RunStore(WorldStore(tmp_path), queue)
hello = WorkerHello(
worker_id="worker-006",
instance_id="a" * 32,
runtime="isaac-sim-6.1",
model_ids=["reference"],
profile_sha256="b" * 64,
runtime_sources={key: "c" * 64 for key in ("worker", "scene", "models", "robot")},
execution_modes=["realtime"],
stream=StreamEndpoint(server="100.80.1.2"),
)
store.register(hello)
world = make_world(store.worlds)
row = store.start(
RunCreate(world_id=world["world_id"], clock="realtime", start_paused=True),
"test-realtime-01",
)
return store, hello, row
def snapshot(**changes):
fields = dict(
sequence=0,
control_sequence=0,
state="ready",
phase="running",
simulation_time_ns=0,
wall_elapsed_seconds=1,
physics_steps=0,
render_frames=30,
sensor_frames=0,
inference_count=0,
dropped_frames=0,
rtf=0,
render_fps=30,
sensor_fps=0,
ai_hz=0,
pose_xy=(0, 0),
pose_yaw=0,
speed_mps=0,
applied_speed_mps=0,
applied_yaw_rate_rps=0,
stop_reason="paused",
ai_ready=False,
stream_ready=True,
camera="follow",
)
return RealtimeSnapshot(**{**fields, **changes})
def test_worker_owns_acknowledgement_and_no_frame_archive_on_core(realtime):
store, hello, row = realtime
key = row["run_id"]
assert not (store.directory(key) / "frames").exists()
assert store.poll(hello.instance_id, key)["run"]["state"] == "starting"
store.snapshot(key, hello.instance_id, snapshot())
command = store.control(key, "play")
assert command["state"] == "ready"
assert store.control(key, "play")["control_sequence"] == command["control_sequence"]
assert store.poll(hello.instance_id, key)["run"]["state"] == "ready"
store.snapshot(
key,
hello.instance_id,
snapshot(sequence=1, control_sequence=1, state="running", simulation_time_ns=66666667),
)
assert store.get(key)["state"] == "running"
with pytest.raises(RuntimeError):
store.sample(key, hello.instance_id, sample())
with pytest.raises(RuntimeError):
store.control(key, "step")
def test_disconnect_restart_reconcile_never_releases_gpu_or_replays(realtime):
store, hello, row = realtime
key = row["run_id"]
store.snapshot(key, hello.instance_id, snapshot())
store.seen -= 21
assert store.status()["active_run"]["state"] == "disconnected"
with pytest.raises(RuntimeError):
store.queue.reserve_simulation("airun-" + "d" * 32)
recovered = RunStore(store.worlds, store.queue)
with pytest.raises(RuntimeError):
recovered.register(hello.model_copy(update={"instance_id": "d" * 32}))
recovered.register(hello)
assert recovered.poll(hello.instance_id, key)["action"] == "pause"
recovered.snapshot(key, hello.instance_id, snapshot(sequence=3))
assert recovered.get(key)["state"] == "ready"
recovered.control(key, "stop")
recovered.snapshot(key, hello.instance_id, snapshot(sequence=4))
assert recovered.get(key)["state"] == "stopping"
recovered.finish(key, hello.instance_id, "stopped", "")
recovered.queue.reserve_simulation("airun-" + "d" * 32)
def test_snapshot_is_idempotent_and_rejects_unissued_ack(realtime):
store, hello, row = realtime
key = row["run_id"]
first = snapshot(sequence=5, simulation_time_ns=1000)
assert store.snapshot(key, hello.instance_id, first) == store.snapshot(
key, hello.instance_id, first
)
with pytest.raises(ValueError):
store.snapshot(key, hello.instance_id, snapshot(sequence=6, control_sequence=2))
with pytest.raises(ValueError):
store.snapshot(key, hello.instance_id, snapshot(sequence=6, simulation_time_ns=999))
with pytest.raises(ValidationError):
StreamEndpoint(server="8.8.8.8")
def test_slow_ai_is_bounded_and_pause_fences_inflight_result(tmp_path):
path = Path(__file__).parents[1] / "simulation/ai-polygon/realtime_ai.py"
spec = importlib.util.spec_from_file_location("polygon_realtime_ai", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
entered, release = threading.Event(), threading.Event()
seen = []
class Model:
def ready(self):
pass
def close(self):
pass
def infer(self, rgb):
seen.append(int(rgb[0, 0, 0]))
entered.set()
assert release.wait(2)
return None, []
decision = {
"speed_mps": 0.3,
"yaw_rate_rps": 0,
"reason": "road",
"road_fraction": 0.7,
"obstacle_count": 0,
}
ai = module.LatestInference(
Model,
SimpleNamespace(
reset=lambda: None, decide=lambda *_: SimpleNamespace(model_dump=lambda: decision)
),
tmp_path,
)
ai.start()
try:
ai.enable(True)
ai.submit(np.zeros((2, 2, 3), dtype=np.uint8), 0, time.monotonic(), 0)
assert entered.wait(2)
for frame_id in range(1, 11):
ai.submit(
np.full((2, 2, 3), frame_id, dtype=np.uint8),
frame_id,
time.monotonic(),
frame_id * 1000,
)
assert ai.dropped == 9
assert ai.command(time.monotonic())[0] == 0
ai.enable(False)
release.set()
ai.close()
assert seen == [0]
assert ai.result is None
finally:
release.set()
ai.close()
def test_expired_source_stops_even_if_inference_just_completed(tmp_path):
path = Path(__file__).parents[1] / "simulation/ai-polygon/realtime_ai.py"
spec = importlib.util.spec_from_file_location("polygon_command_deadline", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
ai = module.LatestInference(None, None, tmp_path)
ai.enabled = True
ai.result = {
"captured_at": 0.0,
"completed_at": 1.0,
"decision": {"speed_mps": 0.3, "yaw_rate_rps": 0},
}
assert ai.command(1.1)[:3] == (0.0, 0.0, "stale-camera")
ai.result["captured_at"] = 1.0
assert ai.command(1.1)[:3] == (0.3, 0, "none")
ai.result["decision"]["speed_mps"] = -0.1
assert ai.command(1.1)[:3] == (-0.1, 0, "none")
assert ai.command(1.6)[:2] == (0, 0)
# The worker's 0.8 s camera budget still rejects old images, even when
# a result has just arrived, and never extends the command watchdog.
ai.result.update(captured_at=1.0, completed_at=1.7)
assert ai.command(1.79, frame_deadline=0.8)[:3] == (-0.1, 0, "none")
assert ai.command(1.81, frame_deadline=0.8)[:3] == (0.0, 0.0, "stale-camera")
ai.result.update(captured_at=1.7, completed_at=1.0)
assert ai.command(1.79, frame_deadline=0.8)[:2] == (0.0, 0.0)
def test_reverse_telemetry_keeps_the_run_speed_limit(realtime):
store, hello, row = realtime
store.snapshot(row["run_id"], hello.instance_id, snapshot(applied_speed_mps=-0.1))
assert store.get(row["run_id"])["telemetry"]["applied_speed_mps"] == -0.1
with pytest.raises(ValueError, match="скорость"):
store.snapshot(
row["run_id"], hello.instance_id, snapshot(sequence=1, applied_speed_mps=-0.4)
)
def test_resume_requires_fresh_clear_observations(tmp_path):
from k1link.simulation.ai_polygon.policy import RoadPolicy
path = Path(__file__).parents[1] / "simulation/ai-polygon/realtime_ai.py"
spec = importlib.util.spec_from_file_location("polygon_resume", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class Model:
def ready(self):
pass
def close(self):
pass
def infer(self, rgb):
return np.ones((512, 512), dtype=bool), []
ai = module.LatestInference(Model, RoadPolicy(0.3), tmp_path)
ai.start()
def frame(number):
ai.submit(np.zeros((2, 2, 3), dtype=np.uint8), number, time.monotonic(), number)
deadline = time.monotonic() + 2
while ai.count < number and time.monotonic() < deadline:
time.sleep(0.001)
assert ai.count == number
return ai.command(time.monotonic())[0]
try:
ai.enable(True)
assert frame(1) == frame(2) == 0
assert frame(3) > 0
ai.enable(False)
ai.enable(True)
assert frame(4) == frame(5) == 0
assert frame(6) > 0
finally:
ai.close()
def test_windows_snapshot_sharing_retry_is_bounded(tmp_path, monkeypatch):
path = Path(__file__).parents[1] / "simulation/ai-polygon/local_state.py"
spec = importlib.util.spec_from_file_location("polygon_ipc", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
original = module.os.replace
attempts = []
def transient(source, target):
attempts.append(1)
if len(attempts) <= 2:
raise PermissionError("Windows sharing violation")
original(source, target)
monkeypatch.setattr(module.os, "replace", transient)
monkeypatch.setattr(module.time, "sleep", lambda _: None)
target = tmp_path / "snapshot.json"
module.write_json(target, {"sequence": 3})
assert module.read_json(target) == {"sequence": 3}
assert len(attempts) == 3
attempts.clear()
def permanent():
attempts.append(1)
raise PermissionError("Not a transient lock")
with pytest.raises(PermissionError):
module.sharing_retry(permanent)
assert len(attempts) == 8
+42
View File
@@ -0,0 +1,42 @@
"""Prevent a reconstructed trunk/face from being admitted between ground rays."""
import importlib.util
from pathlib import Path
import numpy as np
SPEC = importlib.util.spec_from_file_location(
"spawn_clearance", Path(__file__).parents[1] / "simulation/ai-polygon/spawn_clearance.py"
)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
def count(points, xy=(0, 0), heading=0, plane=(0, 0, 0)):
return MODULE.obstructing_triangles(np.array(points), np.array([[0, 1, 2]]), xy, heading, plane)
def test_narrow_trunk_between_support_rays_and_crossing_triangle():
assert count([[0.21, 0.21, 0], [0.23, 0.21, 0], [0.21, 0.21, 2]]) == 1
# All vertices outside the prism, but the face crosses through its centre.
assert count([[-2, 0, 0.3], [2, 0, 0.3], [0, 2, 0.3]]) == 1
def test_ground_step_and_overhead_geometry_do_not_block_start():
for z in (0, 0.05, 0.10, 1.5):
assert count([[-2, -2, z], [2, -2, z], [0, 2, z]]) == 0
assert count([[-2, -2, 0.12], [2, -2, 0.12], [0, 2, 0.12]]) == 1
def test_triangle_bounding_box_alone_does_not_reject_empty_corner():
assert count([[0.4, 2, 0.5], [2, 0.4, 0.5], [2, 2, 0.5]]) == 0
def test_clearance_tracks_translation_heading_and_support_slope():
points = np.array([[0.6, 0, 0.3], [0.7, 0, 0.3], [0.6, 0.03, 0.8]])
assert count(points) == 0
assert count(points, heading=45) == 1
points[:, 2] += points[:, 0] * 0.2 + 4
points[:, :2] += [10, -3]
assert count(points, (10, -3), 45, (0.2, 0, 4)) == 1
assert count([[8, -5, 3.6], [12, -5, 4.4], [10, -1, 4]], (10, -3), 45, (0.2, 0, 4)) == 0
+74
View File
@@ -0,0 +1,74 @@
"""Paired assets remain on Worker; hashes and manifests cross the operator link."""
import hashlib
import importlib
import json
import struct
from pathlib import Path
import numpy as np
import pytest
from test_ai_polygon import ply, worker_asset
def triangle_glb():
binary = np.array([[0, 0, 0], [1, 0, 0], [0, 0, 1]], dtype="<f4").tobytes()
binary += np.array([0, 1, 2], dtype="<u4").tobytes()
doc = {
"buffers": [{"byteLength": 48}],
"bufferViews": [
{"buffer": 0, "byteLength": 36},
{"buffer": 0, "byteOffset": 36, "byteLength": 12},
],
"accessors": [
{"bufferView": 0, "componentType": 5126, "type": "VEC3", "count": 3},
{"bufferView": 1, "componentType": 5125, "type": "SCALAR", "count": 3},
],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0}, "indices": 1}]}],
}
encoded = json.dumps(doc).encode()
encoded += b" " * (-len(encoded) % 4)
return (
struct.pack("<III", 0x46546C67, 2, 28 + len(encoded) + len(binary))
+ struct.pack("<II", len(encoded), 0x4E4F534A)
+ encoded
+ struct.pack("<II", len(binary), 0x004E4942)
+ binary
)
def test_paired_import_checks_actual_files_and_never_fetches_from_operator(tmp_path, monkeypatch):
monkeypatch.syspath_prepend(str(Path(__file__).parents[1] / "simulation/ai-polygon"))
importer = importlib.import_module("register_paired_scene")
preparer = importlib.import_module("prepare_terrain")
client_module = importlib.import_module("core_client")
source, collider = tmp_path / "source.ply", tmp_path / "mesh.glb"
source.write_bytes(ply())
collider.write_bytes(triangle_glb())
descriptor = worker_asset().model_dump(mode="json")
descriptor.update(
sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
collider_sha256=hashlib.sha256(collider.read_bytes()).hexdigest(),
)
root = tmp_path / "worker"
request = importer.stage(root, source, collider, descriptor)
world = {**request.model_dump(mode="json"), "storage": {"kind": "worker"}}
manifest_path = preparer.prepare_terrain(root, tmp_path, world)
assert json.loads(manifest_path.read_text())["collider_sha256"] == descriptor["collider_sha256"]
assert importer.stage(root, source, collider, descriptor) == request
token = tmp_path / "token"
token.write_text("test" * 12)
client = client_module.CoreClient("http://127.0.0.1:18080", token, "a" * 32)
monkeypatch.setattr(
client_module.http.client,
"HTTPConnection",
lambda *args, **kwargs: pytest.fail("Operator must not supply this asset"),
)
cached = root / "state/worlds" / (request.sha256 + ".ply")
client.download(world, cached)
with pytest.raises(RuntimeError, match="missing or changed"):
client.download(world, tmp_path / "missing.ply")
with pytest.raises(ValueError, match="match"):
preparer.prepare_terrain(root, tmp_path, {**world, "collider_sha256": "f" * 64})
with pytest.raises(ValueError, match="identity"):
importer.stage(root, source, collider, {**descriptor, "collider_sha256": "f" * 64})
+44
View File
@@ -1604,3 +1604,47 @@ def test_queue_detects_mutated_immutable_identity(tmp_path: Path) -> None:
with pytest.raises(ObservatoryRecordedQueueIntegrityError, match="identity"):
queue.get(job.job_id)
def test_simulation_reservation_serializes_recorded_claims_and_survives_restart(tmp_path):
queue = _queue(tmp_path)
job, _ = queue.submit(_intent(), enqueue=True)
owner = "airun-" + "a" * 32
queue.reserve_simulation(owner)
queue.reserve_simulation(owner)
restarted = _queue(tmp_path)
assert restarted.claim_next(claimant_id="worker-006", claim_request_id="sim-blocked") is None
assert restarted.get(job.job_id).state == "queued"
with pytest.raises(ObservatoryRecordedQueueConflictError):
restarted.release_simulation("airun-" + "b" * 32)
restarted.release_simulation(owner)
claim = restarted.claim_next(claimant_id="worker-006", claim_request_id="sim-released")
assert claim.job.job_id == job.job_id
with pytest.raises(ObservatoryRecordedQueueBusyError):
queue.reserve_simulation(owner)
def test_simulation_and_recorded_claim_cannot_win_together(tmp_path):
queue = _queue(tmp_path)
queue.submit(_intent(), enqueue=True)
second = _queue(tmp_path)
barrier = Barrier(2)
def simulate():
barrier.wait()
try:
queue.reserve_simulation("airun-" + "a" * 32)
return True
except ObservatoryRecordedQueueBusyError:
return False
def recorded():
barrier.wait()
return (
second.claim_next(claimant_id="worker-006", claim_request_id="racing-claim") is not None
)
with ThreadPoolExecutor(max_workers=2) as pool:
simulation = pool.submit(simulate)
inference = pool.submit(recorded)
assert simulation.result() != inference.result()