feat(planning): consolidate recorded-route localization and spatial scene
Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""Run one bounded causal experiment against an immutable saved reference.
|
||||
|
||||
Use the repository virtual environment. This CLI never connects to hardware or
|
||||
the application ingress. Inputs and reports belong in private runtime storage.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import platform
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.device_plugins.xgrids_k1.planning_replay import iter_planning_events
|
||||
from k1link.missions.causal_replay import digest, replay
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--reference-run", required=True, type=Path)
|
||||
parser.add_argument("--query-raw", required=True, type=Path)
|
||||
parser.add_argument("--query-planning", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument(
|
||||
"--mode", choices=["baseline", "tracking", "acquisition"], default="baseline"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
started = time.monotonic()
|
||||
repository = Path(__file__).resolve().parents[1]
|
||||
code_paths = [Path(__file__).resolve()] + [
|
||||
repository / name
|
||||
for name in (
|
||||
"src/k1link/missions/entry_acquisition.py",
|
||||
"src/k1link/missions/entry_acquisition_worker.py",
|
||||
"src/k1link/missions/causal_replay.py",
|
||||
"src/k1link/missions/causal_tracking.py",
|
||||
"src/k1link/missions/live_buffer.py",
|
||||
"src/k1link/missions/registration.py",
|
||||
"src/k1link/missions/registration_worker.py",
|
||||
"src/k1link/device_plugins/xgrids_k1/planning_replay.py",
|
||||
"src/k1link/device_plugins/xgrids_k1/planning_live.py",
|
||||
)
|
||||
]
|
||||
code_hashes = {str(p.relative_to(repository)): digest(p) for p in code_paths}
|
||||
original = json.loads((args.reference_run / "report.json").read_text())
|
||||
query = json.loads(args.query_planning.read_text())
|
||||
if query["session_id"] == original["reference"]["session_id"]:
|
||||
raise ValueError("Independent replay requires a different query recording.")
|
||||
files = {
|
||||
args.reference_run / "report.json": digest(args.reference_run / "report.json"),
|
||||
args.reference_run / "clouds.npz": original["artifacts"]["clouds.npz"],
|
||||
args.query_raw: query["source_digests"]["raw-transport-primary"],
|
||||
args.query_raw.with_name("mqtt.metadata.jsonl"): query["source_digests"][
|
||||
"raw-transport-index"
|
||||
],
|
||||
args.query_planning: digest(args.query_planning),
|
||||
}
|
||||
for path, expected in files.items():
|
||||
if digest(path) != expected:
|
||||
raise ValueError(f"Source digest mismatch: {path.name}")
|
||||
with np.load(args.reference_run / "clouds.npz", allow_pickle=False) as archive:
|
||||
# Deliberately do not load the fitted query, its path, or the final transform.
|
||||
reference = archive["reference"]
|
||||
reference_path = archive["reference_path"]
|
||||
prep = time.monotonic() - started
|
||||
report = replay(
|
||||
iter_planning_events(args.query_raw, query["session_id"]),
|
||||
reference,
|
||||
reference_path,
|
||||
args.output,
|
||||
mode=args.mode,
|
||||
)
|
||||
for path, expected in files.items():
|
||||
if digest(path) != expected:
|
||||
report.update(state="invalid", source_integrity_verified=False)
|
||||
(args.output / "report.json").write_text(json.dumps(report, allow_nan=False))
|
||||
raise ValueError("Source changed during replay.")
|
||||
report.update(
|
||||
reference_run_id=original["id"],
|
||||
reference=original["reference"],
|
||||
query={k: query[k] for k in ["session_id", "generation", "source_digests", "label"]},
|
||||
input_digests={str(path): value for path, value in files.items()},
|
||||
source_integrity_verified=True,
|
||||
implementation_sha256=code_hashes,
|
||||
reference_preparation_s=prep,
|
||||
runtime=dict(
|
||||
system=platform.system(), machine=platform.machine(), python=platform.python_version()
|
||||
),
|
||||
)
|
||||
(args.output / "report.json").write_text(json.dumps(report, allow_nan=False))
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
k: report[k]
|
||||
for k in [
|
||||
"mode",
|
||||
"state",
|
||||
"elapsed_s",
|
||||
"distance_m",
|
||||
"first_heading_s",
|
||||
"first_candidate_s",
|
||||
"first_tracking_s",
|
||||
"source_integrity_verified",
|
||||
]
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
[
|
||||
dict(
|
||||
step=s["step"],
|
||||
at=s["requested_s"],
|
||||
distance=s["distance_m"],
|
||||
seed=s["seed"],
|
||||
status=s["result"]["status"],
|
||||
temporal=s["temporal"],
|
||||
overlap=s["result"].get("overlap"),
|
||||
rmse=s["result"].get("inlier_rmse_m"),
|
||||
fit_s=s["result"].get("registration_seconds"),
|
||||
state=s["tracking_state"],
|
||||
)
|
||||
for s in report["steps"]
|
||||
]
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user