"""Bounded synthetic acceptance of the installed CMU image on Worker only. This tests navigation independently of perception and physics. It does not claim Gaussian-world or physical-rover acceptance. Owns one temporary container. """ import argparse import http.client import json import math import subprocess import time from datetime import UTC, datetime from pathlib import Path from uuid import uuid4 def main(): parser = argparse.ArgumentParser() parser.add_argument("--adapter", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() profile = json.loads((args.adapter / "models.worker-006.json").read_text()) name = "ndc-mission-core-ai-module-navigation-check-" + uuid4().hex[:8] identity = subprocess.check_output( [ "docker", "run", "-d", "--name", name, "--cpus", "3", "--memory", "2g", "--label", "com.nodedc.stack=ai-polygon-qualification", "--label", "com.nodedc.product=mission-core", "--label", "com.nodedc.role=qualification", "--label", "com.nodedc.managed-by=ai-polygon-qualification", "-p", "127.0.0.1:18193:8010", "--mount", f"type=bind,source={args.adapter},target=/adapter,readonly", profile["navigation"]["image"], ], text=True, ).strip() connection = http.client.HTTPConnection("127.0.0.1", 18193, timeout=3) def request(path, body=None): connection.request( "GET" if body is None else "POST", path, body=None if body is None else json.dumps(body) ) response = connection.getresponse() value = json.loads(response.read()) if response.status != 200: raise RuntimeError(str(value)) return value def ready(): deadline = time.monotonic() + 15 while time.monotonic() < deadline: try: request("/ready") return except (OSError, RuntimeError, http.client.HTTPException): time.sleep(0.2) raise TimeoutError("CMU qualification container did not become ready") report = { "image": profile["navigation"]["image"], "cases": [], "utc": datetime.now(UTC).isoformat(), "monotonic": time.monotonic(), } try: ready() floor = [[x / 10, y / 10, 0.0] for x in range(-20, 41) for y in range(-20, 21)] wall = [[1.5, y / 10, z / 10] for y in range(-20, 21) for z in range(1, 16)] enclosure = [ [side * 0.9, y / 10, z / 10] for side in (-1, 1) for y in range(-9, 10) for z in range(1, 16) ] enclosure += [ [x / 10, side * 0.9, z / 10] for side in (-1, 1) for x in range(-9, 10) for z in range(1, 16) ] def corridor(half_width): return floor + [ [x / 10, side * half_width, z / 10] for x in range(-15, 51) for side in (-1, 1) for z in range(1, 16) ] for name, points in ( ("clear-flat-ground", floor), ("rear-reserve-forward-escape", floor + [[-0.53, 0, z / 10] for z in range(1, 10)]), ("front-reserve-stop", floor + [[0.53, 0, z / 10] for z in range(1, 10)]), ("body-overlap-stop", floor + [[-0.49, 0, z / 10] for z in range(1, 10)]), ("reverse-clear-ground", floor), ( "reverse-blocked-by-rear-wall", floor + [[-0.7, y / 10, z / 10] for y in range(-10, 11) for z in range(1, 12)], ), ("turn-away-from-wall", floor + wall), ( "close-wall-stop", floor + [[0.65, y / 10, z / 10] for y in range(-20, 21) for z in range(1, 16)], ), ("closed-enclosure-stop", floor + enclosure), ("metre-rover-in-1.3m-corridor", corridor(0.65)), ("metre-rover-rejects-0.9m-corridor", corridor(0.45)), ( "rear-corner-selects-clear-primitive", floor + [[-0.62, -0.39, z / 10] for z in range(1, 15)], ), ): request("/reset", {}) ready() samples = [] for _ in range(20): samples.append( request( "/plan", { "points": points, "pose": [0, 0, 0.27, 0, 0, 0, 1], "goal": [-0.65, 0, 0] if name.startswith("reverse-") else ([2, -2, 0] if name.startswith("rear-corner") else [3, 0, 0]), "allow_reverse": name.startswith("reverse-"), "max_speed_mps": 0.15, }, ) ) time.sleep(0.2) if name in ( "clear-flat-ground", "metre-rover-in-1.3m-corridor", "rear-reserve-forward-escape", ): passed = any(v["speed_mps"] > 0.05 and v["status"] == "path" for v in samples) elif name == "reverse-clear-ground": passed = any(v["speed_mps"] < -0.05 and v["status"] == "path" for v in samples[-5:]) elif name == "turn-away-from-wall": valid = [v for v in samples[-5:] if v["status"] == "path"] passed = len(valid) >= 3 and all( (v["speed_mps"] > 0.02 or abs(v["yaw_rate_rps"]) > 0.1) and max(p[0] for p in v["path"]) < 0.75 for v in valid ) elif name.startswith("rear-corner"): passed = any(v["speed_mps"] > 0.05 for v in samples[-5:]) and all( v.get("diagnostic", {}).get("failure") != "footprint" for v in samples[-5:] ) else: passed = all(v["speed_mps"] == 0 and v["status"] == "blocked" for v in samples[-5:]) report["cases"].append({"name": name, "passed": passed, "samples": samples}) # A vanished return is occlusion, not proof of free space. Observe a # near wall, then only distant ground for longer than the decay timer. request("/reset", {}) ready() close_wall = [[0.7, y / 10, z / 10] for y in range(-10, 11) for z in range(1, 10)] samples = [] for i in range(30): samples.append( request( "/plan", { "points": floor + close_wall if i < 5 else [p for p in floor if p[0] > 1.2], "pose": [0, 0, 0.37, 0, 0, 0, 1], "goal": [3, 0, 0], "max_speed_mps": 0.15, }, ) ) time.sleep(0.2) report["cases"].append( { "name": "occluded-near-obstacle-retained", "samples": samples, "passed": all( x["speed_mps"] == 0 and x["status"] == "blocked" for x in samples[-5:] ), } ) # Navigation must admit the same continuous grades as the measured # physical profile, while retaining a discontinuous 15 cm ledge. for angle, quantized in [(a, False) for a in (5, 10, 15, 20, 25)] + [ (10, True), (20, True), ]: request("/reset", {}) ready() radians = math.radians(angle) slope = math.tan(radians) samples = [] for _ in range(12): samples.append( request( "/plan", { "points": [ [x, y, round(x * slope / 0.06) * 0.06 if quantized else x * slope] for x, y, _ in floor ], "pose": [ 0, 0, 0.37, 0, -math.sin(radians / 2), 0, math.cos(radians / 2), ], "goal": [3, 0, 3 * slope], "max_speed_mps": 0.15, }, ) ) time.sleep(0.2) report["cases"].append( { "name": f"supported-{'voxel-' if quantized else ''}ramp-{angle}-degrees", "samples": samples, "passed": any(s["speed_mps"] > 0.05 for s in samples[-5:]), } ) for height in (0.12, 0.15, -0.4): request("/reset", {}) ready() points = [[x, y, height if x >= 0.7 else 0] for x, y, _ in floor] points += [ [x / 10, side * 0.65, z / 10] for x in range(-15, 41) for side in (-1, 1) for z in range(1, 16) ] samples = [] for _ in range(12): samples.append( request( "/plan", { "points": points, "pose": [0, 0, 0.37, 0, 0, 0, 1], "goal": [3, 0, 0], "max_speed_mps": 0.15, }, ) ) time.sleep(0.2) report["cases"].append( { "name": f"discontinuous-height-{height:+.2f}m-stop", "samples": samples, "passed": all( s["speed_mps"] == 0 and s["status"] == "blocked" for s in samples[-5:] ), } ) report["passed"] = all(case["passed"] for case in report["cases"]) finally: report["container_log_tail"] = subprocess.run( ["docker", "logs", "--tail", "30", identity], capture_output=True, text=True ).stderr args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2), encoding="utf-8") connection.close() subprocess.run(["docker", "rm", "-f", identity], check=True, capture_output=True) print( json.dumps( { "passed": report["passed"], "cases": [ {"name": row["name"], "passed": row["passed"]} for row in report["cases"] ], } ) ) if __name__ == "__main__": main()