Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b01bb7775 | ||
|
|
b688347778 | ||
|
|
252cbc8495 | ||
|
|
048716b3b3 | ||
|
|
343ddeac2d |
@@ -97,7 +97,7 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
||||
{!collisionAvailable ? (
|
||||
<footer className="simulation-viewport__notice">
|
||||
<StatusBadge tone="warning">Collision недоступен</StatusBadge>
|
||||
<span>Worker 006 сейчас работает в CPU-режиме; визуальный слой настоящий, collision GLB не подменяется.</span>
|
||||
<span>Для этой сборки collision GLB не был запрошен; визуальный слой настоящий и не подменяется.</span>
|
||||
</footer>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -69,4 +69,5 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
|
||||
assert.match(runtime, /dispose\(\)/);
|
||||
assert.doesNotMatch(`${runtime}\n${viewport}`, /iframe|<GSplat/);
|
||||
assert.match(viewport, /Collision недоступен/);
|
||||
assert.match(viewport, /Для этой сборки collision GLB не был запрошен/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"schema_version": "missioncore.m48-additive-low-step-occupancy-profile/v1",
|
||||
"profile_id": "m48r3-ravnoves00-additive-low-step/v1",
|
||||
"provider_id": "ravnoves00-additive-low-step-geometry/v1",
|
||||
"base_geometry": {
|
||||
"profile_id": "m4-ravnoves00-e29-e32-geometry/v1",
|
||||
"sha256": "cc666c9389a5e221957faddec89584709b66918d14abaf646f1832e001421999"
|
||||
},
|
||||
"source": {
|
||||
"source_id": "RAVNOVES00",
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"frame_count": 4489,
|
||||
"point_count": 9207270,
|
||||
"local_surface_model_id": "k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55",
|
||||
"local_surface_sha256": "f57eb2485b6cef47f2a97a2d9ff1aa9fd9265fe1eb69cd5852d12f39e13b8bc6",
|
||||
"m48r2_result_id": "m48-static-occupancy-qualification-568024554cff011332ff19ca4739f70555a6232c0607dec2a71be6db408ea69a",
|
||||
"m48r2_cases_sha256": "249f58bf62973bfbcef8cd2f780013830fd4bf61ec342365a541963af96fa2ff"
|
||||
},
|
||||
"componentization": {
|
||||
"voxel_size_m": 0.45,
|
||||
"neighbor_radius_cells": 1,
|
||||
"minimum_points": 5,
|
||||
"minimum_voxels": 1,
|
||||
"local_radius_m": 10.0,
|
||||
"maximum_candidate_points_per_frame": 768,
|
||||
"maximum_cells_per_component": 256,
|
||||
"maximum_components_per_frame": 128,
|
||||
"exclude_baseline_occupied_points": true,
|
||||
"exclude_claimed_source_points": true
|
||||
},
|
||||
"separation_expectations": [
|
||||
{
|
||||
"anchor_id": "anchor-5e6e2a81e0667bdd9faf2a9e",
|
||||
"sequence": 1856,
|
||||
"expected_minimum_components": 2,
|
||||
"interpretation": "two thin posts remain independent occupied components"
|
||||
},
|
||||
{
|
||||
"anchor_id": "anchor-924a4623077fe5df18816b47",
|
||||
"sequence": 1856,
|
||||
"expected_minimum_components": 2,
|
||||
"interpretation": "two concrete hemispheres remain independent occupied components"
|
||||
}
|
||||
],
|
||||
"acceptance": {
|
||||
"expected_frames": 4489,
|
||||
"requested_source_rate_hz": 12.0,
|
||||
"minimum_effective_world_state_fps": 11.5,
|
||||
"maximum_world_state_completion_p95_ms": 60.0,
|
||||
"maximum_geometry_stage_p95_ms": 9.0,
|
||||
"maximum_geometry_stage_p99_ms": 16.0,
|
||||
"maximum_fps_regression_fraction_vs_native_baseline": 0.05,
|
||||
"maximum_world_state_p95_delta_ms_vs_native_baseline": 15.0,
|
||||
"maximum_additive_component_mean_growth_fraction": 1.0,
|
||||
"maximum_additive_cell_mean_growth_fraction": 1.0,
|
||||
"maximum_capacity_drop_count": 0,
|
||||
"minimum_critical_near_recall": 1.0,
|
||||
"minimum_canonical_engineering_recall": 1.0,
|
||||
"maximum_false_free_count": 0
|
||||
},
|
||||
"policy": {
|
||||
"absence_of_points_means_free": false,
|
||||
"absence_of_camera_detection_means_free": false,
|
||||
"additive_only": true,
|
||||
"semantic_class_used": false,
|
||||
"ray_clearing_used": false,
|
||||
"planner_authoritative_free_space_claimed": false
|
||||
},
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": false,
|
||||
"commands_enabled": false,
|
||||
"actuation_allowed": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"schema_version": "missioncore.reference-perception-graph-config/v2",
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"source_profile_id": "m4-ravnoves00-recorded-realtime/v1",
|
||||
"providers": [
|
||||
{
|
||||
"role": "source",
|
||||
"provider_id": "ravnoves00-recorded-source/v1",
|
||||
"version": "1.0.0",
|
||||
"revision": "m4-ravnoves00-recorded-realtime/v1",
|
||||
"sha256": "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
|
||||
},
|
||||
{
|
||||
"role": "detector",
|
||||
"provider_id": "triton-rf-detr-large-coco-native-kb4-risk-fp16-shadow/v0",
|
||||
"version": "0.1.0",
|
||||
"revision": "rf-detr-large-coco-native-kb4-uint8-trt11-fp16-risk-shadow/v0",
|
||||
"sha256": "dbf4da5dbad6c3c22b1280b46ffcad81719bd183c81c263a4859847d829019b6"
|
||||
},
|
||||
{
|
||||
"role": "geometry",
|
||||
"provider_id": "ravnoves00-additive-low-step-geometry/v1",
|
||||
"version": "0.1.0",
|
||||
"revision": "m48r3-ravnoves00-additive-low-step/v1",
|
||||
"sha256": "0d46dcb28542902849ee31de4c4594ef90407834d9e5551474fc4191cadbf1a9"
|
||||
},
|
||||
{
|
||||
"role": "temporal",
|
||||
"provider_id": "bounded-spatial-temporal-layer/v1",
|
||||
"version": "1.0.0",
|
||||
"revision": "m4-bounded-temporal-motion/v1",
|
||||
"sha256": "7130eaee24a95c7d888bf7598010e03e129e1c3ac5b34bcd8401015ff4244b39"
|
||||
},
|
||||
{
|
||||
"role": "motion",
|
||||
"provider_id": "class-independent-motion-estimator/v1",
|
||||
"version": "1.0.0",
|
||||
"revision": "m4-bounded-temporal-motion/v1",
|
||||
"sha256": "7130eaee24a95c7d888bf7598010e03e129e1c3ac5b34bcd8401015ff4244b39"
|
||||
},
|
||||
{
|
||||
"role": "rolling",
|
||||
"provider_id": "rolling-local-obstacle-map/v1",
|
||||
"version": "1.0.0",
|
||||
"revision": "ravnoves00-rolling-local-obstacle-map/v1",
|
||||
"sha256": "f7e3315eaf6ffaf3aee1e04913933812092cf82bbcc9984c1a6fa2d9250e6784"
|
||||
},
|
||||
{
|
||||
"role": "threat",
|
||||
"provider_id": "dual-evidence-replay-threat/v3",
|
||||
"version": "3.0.0",
|
||||
"revision": "m4-ravnoves00-virtual-corridor/v3",
|
||||
"sha256": "8c3a5aa837da1f028f5998fb504a1381f9b2b68de6420a32160410b6dc0887c7"
|
||||
}
|
||||
],
|
||||
"queues": [
|
||||
{
|
||||
"stage_id": "detector",
|
||||
"capacity": 2,
|
||||
"deadline_ns": 1000000000,
|
||||
"terminal_timeout_ns": 90000000000
|
||||
},
|
||||
{
|
||||
"stage_id": "geometry",
|
||||
"capacity": 2,
|
||||
"deadline_ns": 1500000000,
|
||||
"terminal_timeout_ns": 90000000000
|
||||
},
|
||||
{
|
||||
"stage_id": "temporal",
|
||||
"capacity": 2,
|
||||
"deadline_ns": 1750000000,
|
||||
"terminal_timeout_ns": 90000000000
|
||||
},
|
||||
{
|
||||
"stage_id": "rolling",
|
||||
"capacity": 2,
|
||||
"deadline_ns": 2000000000,
|
||||
"terminal_timeout_ns": 90000000000
|
||||
},
|
||||
{
|
||||
"stage_id": "threat",
|
||||
"capacity": 2,
|
||||
"deadline_ns": 2250000000,
|
||||
"terminal_timeout_ns": 90000000000
|
||||
}
|
||||
],
|
||||
"authority": {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": false,
|
||||
"commands_enabled": false,
|
||||
"actuation_allowed": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -383,6 +383,7 @@ def main() -> int:
|
||||
):
|
||||
parser.add_argument(f"--{name}", type=Path, required=True)
|
||||
parser.add_argument("--triton-origin", default="http://127.0.0.1:8000")
|
||||
parser.add_argument("--additive-low-step-profile", type=Path)
|
||||
parser.add_argument("--loops", type=int, default=1)
|
||||
parser.add_argument("--maximum-frames", type=int)
|
||||
parser.add_argument("--source-rate-hz", type=float)
|
||||
@@ -477,6 +478,7 @@ def main() -> int:
|
||||
decode_timing_observer=timing_store.observe_decode,
|
||||
source_pacing_observer=timing_store.observe_pacing,
|
||||
detector_timing_observer=timing_store.observe_detector,
|
||||
additive_low_step_profile=arguments.additive_low_step_profile,
|
||||
maximum_frames=arguments.maximum_frames,
|
||||
source_rate_hz=arguments.source_rate_hz,
|
||||
) as runtime:
|
||||
@@ -685,7 +687,11 @@ def main() -> int:
|
||||
"worker_id": "worker-006",
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"detector_provider_id": detector_provider_id,
|
||||
"inputs": _input_digests(paths, arguments.detector_profile),
|
||||
"inputs": _input_digests(
|
||||
paths,
|
||||
arguments.detector_profile,
|
||||
arguments.additive_low_step_profile,
|
||||
),
|
||||
"runtime_artifact_sha256": arguments.runtime_artifact_sha256,
|
||||
"runner_sha256": arguments.runner_sha256,
|
||||
},
|
||||
@@ -1105,12 +1111,15 @@ def _gc_telemetry_summary(events: list[dict[str, object]]) -> dict[str, object]:
|
||||
def _input_digests(
|
||||
paths: ReferenceGraphRuntimePaths,
|
||||
detector_profile: Path,
|
||||
additive_low_step_profile: Path | None = None,
|
||||
) -> dict[str, str]:
|
||||
values = {
|
||||
field.name: _sha256(getattr(paths, field.name))
|
||||
for field in fields(ReferenceGraphRuntimePaths)
|
||||
}
|
||||
values["detector_profile"] = _sha256(detector_profile)
|
||||
if additive_low_step_profile is not None:
|
||||
values["additive_low_step_profile"] = _sha256(additive_low_step_profile)
|
||||
return dict(sorted(values.items()))
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ param(
|
||||
[double]$MinimumEffectiveWorldStateFps = 9.5,
|
||||
[ValidateRange(1.0, 10000.0)]
|
||||
[double]$MaximumWorldStateCompletionP95Ms = 125.0,
|
||||
[switch]$AdditiveLowStep,
|
||||
[string]$OutputRoot = (
|
||||
"D:\NDC_MISSIONCORE\runtime\results\m48n-native-reference-graph-shadow"
|
||||
)
|
||||
@@ -109,6 +110,15 @@ $expectedConfigs = [ordered]@{
|
||||
"8c3a5aa837da1f028f5998fb504a1381f9b2b68de6420a32160410b6dc0887c7"
|
||||
)
|
||||
}
|
||||
if ($AdditiveLowStep) {
|
||||
$expectedConfigs.Remove("m48n-rf-detr-native-reference-graph-shadow-v0.json")
|
||||
$expectedConfigs["m48r3-native-low-step-reference-graph-shadow-v1.json"] = (
|
||||
"5f5832a0a0c1879374b166071efed02de14aeda815046da5711d6f4053eb9af4"
|
||||
)
|
||||
$expectedConfigs["m48r3-additive-low-step-occupancy-v1.json"] = (
|
||||
"0d46dcb28542902849ee31de4c4594ef90407834d9e5551474fc4191cadbf1a9"
|
||||
)
|
||||
}
|
||||
foreach ($entry in $expectedConfigs.GetEnumerator()) {
|
||||
$null = Assert-File (Join-Path $release $entry.Key) $entry.Value (
|
||||
"M48N config {0}" -f $entry.Key
|
||||
@@ -203,8 +213,16 @@ if (-not $canonicalTriton.State.Running -or $canonicalTriton.State.Health.Status
|
||||
throw "Canonical Triton must remain healthy during M48N shadow"
|
||||
}
|
||||
$canonicalTritonId = [string]$canonicalTriton.Id
|
||||
$tritonName = "ndc-mission-core-m48n-native-reference-graph-triton"
|
||||
$graphName = "ndc-mission-core-m48n-native-reference-graph"
|
||||
$tritonName = if ($AdditiveLowStep) {
|
||||
"ndc-mission-core-m48r3-low-step-triton"
|
||||
} else {
|
||||
"ndc-mission-core-m48n-native-reference-graph-triton"
|
||||
}
|
||||
$graphName = if ($AdditiveLowStep) {
|
||||
"ndc-mission-core-m48r3-low-step-reference-graph"
|
||||
} else {
|
||||
"ndc-mission-core-m48n-native-reference-graph"
|
||||
}
|
||||
foreach ($name in @($tritonName, $graphName)) {
|
||||
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$name$") {
|
||||
throw "M48N candidate container $name already exists"
|
||||
@@ -263,6 +281,19 @@ try {
|
||||
throw "M48N Triton published a host port"
|
||||
}
|
||||
|
||||
$graphConfigName = if ($AdditiveLowStep) {
|
||||
"m48r3-native-low-step-reference-graph-shadow-v1.json"
|
||||
} else {
|
||||
"m48n-rf-detr-native-reference-graph-shadow-v0.json"
|
||||
}
|
||||
$additiveArguments = if ($AdditiveLowStep) {
|
||||
@(
|
||||
"--additive-low-step-profile",
|
||||
"/release/m48r3-additive-low-step-occupancy-v1.json"
|
||||
)
|
||||
} else {
|
||||
@()
|
||||
}
|
||||
$arguments = @(
|
||||
"run", "--name", $graphName,
|
||||
"--label", "com.nodedc.product=mission-core",
|
||||
@@ -294,7 +325,7 @@ try {
|
||||
"--entrypoint", "python3",
|
||||
$image,
|
||||
"/release/run_m48s_reference_graph_shadow_worker.py",
|
||||
"--graph-config", "/release/m48n-rf-detr-native-reference-graph-shadow-v0.json",
|
||||
"--graph-config", ("/release/{0}" -f $graphConfigName),
|
||||
"--baseline-profile", "/release/m4-recorded-realtime-baseline-v1.json",
|
||||
"--detector-profile", "/release/rf-detr-large-native-kb4-risk-shadow-v0.json",
|
||||
"--geometry-profile", "/release/m4-geometry-association-v1.json",
|
||||
@@ -331,7 +362,7 @@ try {
|
||||
"--output", "/output/result.json",
|
||||
"--progress", "/output/progress.jsonl",
|
||||
"--frame-ledger", "/output/frames.jsonl"
|
||||
)
|
||||
) + $additiveArguments
|
||||
& docker @arguments
|
||||
Assert-LastExitCode "M48N native complete reference graph shadow"
|
||||
foreach ($name in @("result.json", "frames.jsonl", "progress.jsonl")) {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the deterministic M4.8R3 occupied-only Worker 006 shadow release."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl"
|
||||
POWERSHELL_RUNNER = (
|
||||
REPOSITORY_ROOT
|
||||
/ "experiments/perception/worker/Invoke-M48NNativeReferenceGraph.ps1"
|
||||
)
|
||||
PYTHON_RUNNER = (
|
||||
REPOSITORY_ROOT
|
||||
/ "experiments/perception/run_m48s_reference_graph_shadow_worker.py"
|
||||
)
|
||||
NATIVE_CONFIG = (
|
||||
REPOSITORY_ROOT / "experiments/perception/worker/rf_detr_large_native_kb4_config.pbtxt"
|
||||
)
|
||||
CONFIG_PATHS = (
|
||||
Path("config/perception/m48r3-native-low-step-reference-graph-shadow-v1.json"),
|
||||
Path("config/perception/m48r3-additive-low-step-occupancy-v1.json"),
|
||||
Path("config/perception/m4-recorded-realtime-baseline-v1.json"),
|
||||
Path("config/perception/rf-detr-large-native-kb4-risk-shadow-v0.json"),
|
||||
Path("config/perception/m4-geometry-association-v1.json"),
|
||||
Path("config/perception/m4-temporal-motion-v1.json"),
|
||||
Path("config/perception/m4-rolling-local-map-v1.json"),
|
||||
Path("config/perception/m4-replay-threat-v3.json"),
|
||||
)
|
||||
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
||||
|
||||
|
||||
class ArtifactBuildError(RuntimeError):
|
||||
"""The M4.8R3 release cannot be built from the declared source."""
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def git_revision(*, require_clean: bool) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
revision = result.stdout.strip()
|
||||
if re.fullmatch(r"[a-f0-9]{40}", revision) is None:
|
||||
raise ArtifactBuildError("Git revision is not a full SHA-1")
|
||||
if require_clean:
|
||||
status = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if status.stdout.strip():
|
||||
raise ArtifactBuildError("Worker shadow artifact requires a clean worktree")
|
||||
return revision
|
||||
|
||||
|
||||
def build_wheel(output: Path) -> Path:
|
||||
environment = os.environ.copy()
|
||||
environment["SOURCE_DATE_EPOCH"] = "0"
|
||||
result = subprocess.run(
|
||||
["uv", "build", "--wheel", "--out-dir", str(output)],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
env=environment,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout).strip()
|
||||
raise ArtifactBuildError(f"wheel build failed: {detail}")
|
||||
wheel = output / WHEEL_NAME
|
||||
if not wheel.is_file() or wheel.is_symlink():
|
||||
raise ArtifactBuildError("expected Worker wheel was not built")
|
||||
return wheel
|
||||
|
||||
|
||||
def _tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
|
||||
info = tarfile.TarInfo(arcname)
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.uname = "root"
|
||||
info.gname = "root"
|
||||
info.mtime = 0
|
||||
if path.is_dir():
|
||||
info.type = tarfile.DIRTYPE
|
||||
info.mode = 0o755
|
||||
else:
|
||||
info.type = tarfile.REGTYPE
|
||||
info.mode = 0o644
|
||||
info.size = path.stat().st_size
|
||||
return info
|
||||
|
||||
|
||||
def write_canonical_archive(stage: Path, target: Path) -> None:
|
||||
members = [stage / "manifest.env", stage / "files.txt", stage / "payload"]
|
||||
members.extend(sorted((stage / "payload").rglob("*")))
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with (
|
||||
target.open("wb") as raw,
|
||||
gzip.GzipFile(filename="", mode="wb", fileobj=raw, compresslevel=9, mtime=0) as gz,
|
||||
tarfile.open(fileobj=gz, mode="w", format=tarfile.PAX_FORMAT) as archive,
|
||||
):
|
||||
for path in members:
|
||||
info = _tar_info(path, path.relative_to(stage).as_posix())
|
||||
if path.is_file():
|
||||
with path.open("rb") as stream:
|
||||
archive.addfile(info, stream)
|
||||
else:
|
||||
archive.addfile(info, io.BytesIO())
|
||||
|
||||
|
||||
def build_artifact(
|
||||
patch_id: str,
|
||||
output_directory: Path,
|
||||
*,
|
||||
revision: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
if PATCH_ID.fullmatch(patch_id) is None:
|
||||
raise ArtifactBuildError("patch id is invalid")
|
||||
selected_revision = revision or git_revision(require_clean=True)
|
||||
if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None:
|
||||
raise ArtifactBuildError("artifact revision is invalid")
|
||||
sources = (
|
||||
POWERSHELL_RUNNER,
|
||||
PYTHON_RUNNER,
|
||||
NATIVE_CONFIG,
|
||||
*(REPOSITORY_ROOT / relative for relative in CONFIG_PATHS),
|
||||
)
|
||||
if any(path.is_symlink() or not path.is_file() for path in sources):
|
||||
raise ArtifactBuildError("Worker release input is not a regular file")
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-m48r3-worker-") as directory:
|
||||
stage = Path(directory)
|
||||
payload = stage / "payload"
|
||||
payload.mkdir()
|
||||
wheel = build_wheel(stage / "wheel")
|
||||
copied: list[Path] = []
|
||||
for source in sources:
|
||||
destination = payload / source.name
|
||||
destination.write_bytes(source.read_bytes())
|
||||
copied.append(destination)
|
||||
wheel_destination = payload / WHEEL_NAME
|
||||
wheel_destination.write_bytes(wheel.read_bytes())
|
||||
copied.append(wheel_destination)
|
||||
release = {
|
||||
"schema_version": "missioncore.m48r3-worker-shadow-release/v1",
|
||||
"patch_id": patch_id,
|
||||
"transition": "m48r3-additive-low-step-isolated-shadow/v1",
|
||||
"code_revision": selected_revision,
|
||||
"worker_id": "worker-006",
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"detector_provider_id": (
|
||||
"triton-rf-detr-large-coco-native-kb4-risk-fp16-shadow/v0"
|
||||
),
|
||||
"geometry_provider_id": "ravnoves00-additive-low-step-geometry/v1",
|
||||
"expected_frames": 4489,
|
||||
"requested_source_rate_hz": 12.0,
|
||||
"candidate_accepted": False,
|
||||
"production_accepted": False,
|
||||
"durable_worker_action": "none",
|
||||
"canonical_triton_action": "none",
|
||||
"files": {
|
||||
path.name: {"sha256": sha256_file(path), "bytes": path.stat().st_size}
|
||||
for path in sorted(copied)
|
||||
},
|
||||
}
|
||||
release_path = payload / "release.json"
|
||||
release_path.write_text(
|
||||
json.dumps(release, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
"utf-8",
|
||||
)
|
||||
payload_files = sorted((*release["files"], release_path.name))
|
||||
(stage / "manifest.env").write_text(
|
||||
f"id={patch_id}\ncomponent=mission-core-worker\ntype=shadow-release\n",
|
||||
"utf-8",
|
||||
)
|
||||
(stage / "files.txt").write_text("\n".join(payload_files) + "\n", "utf-8")
|
||||
target = output_directory.resolve() / f"nodedc-{patch_id}.tgz"
|
||||
write_canonical_archive(stage, target)
|
||||
return {
|
||||
"ok": True,
|
||||
"patch_id": patch_id,
|
||||
"artifact": str(target),
|
||||
"sha256": sha256_file(target),
|
||||
"code_revision": selected_revision,
|
||||
"wheel_sha256": release["files"][WHEEL_NAME]["sha256"],
|
||||
"payload_files": payload_files,
|
||||
"transition": release["transition"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("patch_id")
|
||||
parser.add_argument(
|
||||
"--output-directory",
|
||||
type=Path,
|
||||
default=REPOSITORY_ROOT / ".runtime/worker-artifacts",
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
result = build_artifact(arguments.patch_id, arguments.output_directory)
|
||||
except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc:
|
||||
parser.error(str(exc))
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,643 @@
|
||||
"""Occupied-only low-step geometry shadow for M4.8R3.
|
||||
|
||||
The provider composes the accepted geometry association provider and may only
|
||||
add class-free current LiDAR observations. It never removes baseline evidence,
|
||||
publishes free space, changes camera semantics, or performs inference.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from .contracts import (
|
||||
EvidenceBasis,
|
||||
EvidenceCurrentness,
|
||||
MetricGeometry,
|
||||
ObjectProposal2D,
|
||||
ObstacleObservation,
|
||||
validate_exclusive_point_ownership,
|
||||
)
|
||||
from .geometry import (
|
||||
GeometryProviderSnapshot,
|
||||
Ravnoves00GeometryAssociationProvider,
|
||||
RecordedGeometryStore,
|
||||
)
|
||||
from .geometry_math import POINT_OCCUPIED
|
||||
from .providers import SourcePacket
|
||||
|
||||
M48_LOW_STEP_PROFILE_SCHEMA: Final = (
|
||||
"missioncore.m48-additive-low-step-occupancy-profile/v1"
|
||||
)
|
||||
M48_LOW_STEP_PROVIDER_ID: Final = "ravnoves00-additive-low-step-geometry/v1"
|
||||
|
||||
IntArray = npt.NDArray[np.int64]
|
||||
|
||||
|
||||
class M48LowStepOccupancyError(RuntimeError):
|
||||
"""The additive profile, source evidence, or bounded component set is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LowStepComponentProfile:
|
||||
voxel_size_m: float
|
||||
neighbor_radius_cells: int
|
||||
minimum_points: int
|
||||
minimum_voxels: int
|
||||
local_radius_m: float
|
||||
maximum_candidate_points_per_frame: int
|
||||
maximum_cells_per_component: int
|
||||
maximum_components_per_frame: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
not math.isfinite(self.voxel_size_m)
|
||||
or not 0.05 <= self.voxel_size_m <= 2.0
|
||||
or self.neighbor_radius_cells != 1
|
||||
or not 1 <= self.minimum_points <= 256
|
||||
or not 1 <= self.minimum_voxels <= 128
|
||||
or not math.isfinite(self.local_radius_m)
|
||||
or not 1.0 <= self.local_radius_m <= 100.0
|
||||
or not 1 <= self.maximum_candidate_points_per_frame <= 4096
|
||||
or not 1 <= self.maximum_cells_per_component <= 2048
|
||||
or not 1 <= self.maximum_components_per_frame <= 512
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step component bounds are invalid")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LowStepSeparationExpectation:
|
||||
anchor_id: str
|
||||
sequence: int
|
||||
expected_minimum_components: int
|
||||
interpretation: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48LowStepOccupancyProfile:
|
||||
profile_id: str
|
||||
provider_id: str
|
||||
source_id: str
|
||||
session_id: str
|
||||
frame_count: int
|
||||
point_count: int
|
||||
local_surface_model_id: str
|
||||
local_surface_sha256: str
|
||||
base_geometry_profile_id: str
|
||||
base_geometry_profile_sha256: str
|
||||
component: LowStepComponentProfile
|
||||
separation_expectations: tuple[LowStepSeparationExpectation, ...]
|
||||
profile_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48LowStepOccupancySnapshot:
|
||||
base: GeometryProviderSnapshot
|
||||
input_frames: int
|
||||
completed_frames: int
|
||||
failed_frames: int
|
||||
frames_with_additions: int
|
||||
candidate_point_count: int
|
||||
additive_observation_count: int
|
||||
additive_voxel_count: int
|
||||
peak_candidate_points_per_frame: int
|
||||
peak_additive_observations_per_frame: int
|
||||
peak_voxels_per_component: int
|
||||
additive_core_duration_ns: int
|
||||
|
||||
|
||||
class M48AdditiveLowStepGeometryProvider:
|
||||
"""Compose baseline geometry with bounded, spatially separate step components."""
|
||||
|
||||
provider_id: str = M48_LOW_STEP_PROVIDER_ID
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
store: RecordedGeometryStore,
|
||||
profile: M48LowStepOccupancyProfile,
|
||||
) -> None:
|
||||
if profile.provider_id != self.provider_id:
|
||||
raise M48LowStepOccupancyError("low-step provider identity changed")
|
||||
geometry = store.profile
|
||||
if (
|
||||
geometry.source_id != profile.source_id
|
||||
or geometry.session_id != profile.session_id
|
||||
or geometry.frame_count != profile.frame_count
|
||||
or geometry.point_count != profile.point_count
|
||||
or geometry.local_surface_model_id != profile.local_surface_model_id
|
||||
or geometry.local_surface_sha256 != profile.local_surface_sha256
|
||||
or geometry.profile_id != profile.base_geometry_profile_id
|
||||
or geometry.profile_sha256 != profile.base_geometry_profile_sha256
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step source binding changed")
|
||||
self.store = store
|
||||
self.profile = profile
|
||||
self.base = Ravnoves00GeometryAssociationProvider(store=store)
|
||||
self._lock = Lock()
|
||||
self._input_frames = 0
|
||||
self._completed_frames = 0
|
||||
self._failed_frames = 0
|
||||
self._frames_with_additions = 0
|
||||
self._candidate_points = 0
|
||||
self._additive_observation_count = 0
|
||||
self._additive_voxels = 0
|
||||
self._peak_candidate_points = 0
|
||||
self._peak_additive_observations = 0
|
||||
self._peak_component_voxels = 0
|
||||
self._additive_core_duration_ns = 0
|
||||
|
||||
def associate(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
proposals: tuple[ObjectProposal2D, ...],
|
||||
) -> tuple[ObstacleObservation, ...]:
|
||||
with self._lock:
|
||||
self._input_frames += 1
|
||||
baseline = self.base.associate(packet, proposals)
|
||||
started = time.perf_counter_ns()
|
||||
try:
|
||||
additive, candidate_points, voxel_count, peak_component_voxels = (
|
||||
self._build_additive_observations(packet, baseline)
|
||||
)
|
||||
result = (*baseline, *additive)
|
||||
validate_exclusive_point_ownership(result)
|
||||
except Exception:
|
||||
with self._lock:
|
||||
self._failed_frames += 1
|
||||
self._additive_core_duration_ns += max(
|
||||
0, time.perf_counter_ns() - started
|
||||
)
|
||||
raise
|
||||
with self._lock:
|
||||
self._completed_frames += 1
|
||||
self._frames_with_additions += bool(additive)
|
||||
self._candidate_points += candidate_points
|
||||
self._additive_observation_count += len(additive)
|
||||
self._additive_voxels += voxel_count
|
||||
self._peak_candidate_points = max(
|
||||
self._peak_candidate_points, candidate_points
|
||||
)
|
||||
self._peak_additive_observations = max(
|
||||
self._peak_additive_observations, len(additive)
|
||||
)
|
||||
self._peak_component_voxels = max(
|
||||
self._peak_component_voxels, peak_component_voxels
|
||||
)
|
||||
self._additive_core_duration_ns += max(
|
||||
0, time.perf_counter_ns() - started
|
||||
)
|
||||
return tuple(result)
|
||||
|
||||
def _build_additive_observations(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
baseline: tuple[ObstacleObservation, ...],
|
||||
) -> tuple[tuple[ObstacleObservation, ...], int, int, int]:
|
||||
frame = self.store.frame(packet)
|
||||
if frame is None or not frame.surface_valid:
|
||||
return (), 0, 0, 0
|
||||
step = self.store.point_step_candidates_for_frame(frame.frame_index)
|
||||
if step is None or step.shape != (frame.source_point_count,):
|
||||
raise M48LowStepOccupancyError("low-step point index space changed")
|
||||
claimed = {
|
||||
point_id for observation in baseline for point_id in observation.source_point_ids
|
||||
}
|
||||
candidate = np.flatnonzero(
|
||||
(step > 0) & (frame.point_class != POINT_OCCUPIED)
|
||||
).astype(np.int64)
|
||||
if claimed and candidate.size:
|
||||
candidate = candidate[
|
||||
np.fromiter(
|
||||
(int(value) not in claimed for value in candidate),
|
||||
dtype=np.bool_,
|
||||
count=int(candidate.size),
|
||||
)
|
||||
]
|
||||
if candidate.size:
|
||||
ranges = np.linalg.norm(
|
||||
frame.points_map[candidate] - frame.sensor_position_map,
|
||||
axis=1,
|
||||
)
|
||||
candidate = candidate[ranges <= self.profile.component.local_radius_m]
|
||||
candidate_count = int(candidate.size)
|
||||
if candidate_count > self.profile.component.maximum_candidate_points_per_frame:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step candidate point capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
components = _voxel_components(
|
||||
frame.points_map,
|
||||
candidate,
|
||||
self.profile.component,
|
||||
)
|
||||
qualified = tuple(
|
||||
item
|
||||
for item in components
|
||||
if item[0].size >= self.profile.component.minimum_points
|
||||
and item[1] >= self.profile.component.minimum_voxels
|
||||
)
|
||||
qualified = tuple(
|
||||
sorted(
|
||||
qualified,
|
||||
key=lambda item: (
|
||||
float(
|
||||
np.min(
|
||||
np.linalg.norm(
|
||||
frame.points_map[item[0]]
|
||||
- frame.sensor_position_map,
|
||||
axis=1,
|
||||
)
|
||||
)
|
||||
),
|
||||
int(item[0][0]),
|
||||
),
|
||||
)
|
||||
)
|
||||
if len(qualified) > self.profile.component.maximum_components_per_frame:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step component capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
observations: list[ObstacleObservation] = []
|
||||
voxel_count = 0
|
||||
peak_voxels = 0
|
||||
for component_index, (indices, cells) in enumerate(qualified):
|
||||
if cells > self.profile.component.maximum_cells_per_component:
|
||||
raise M48LowStepOccupancyError(
|
||||
"low-step component cell capacity exceeded; dropping is forbidden"
|
||||
)
|
||||
point_ids = tuple(sorted(int(value) for value in indices))
|
||||
points = frame.points_map[indices]
|
||||
centroid = np.median(points, axis=0)
|
||||
covariance = points.var(axis=0)
|
||||
nearest = float(
|
||||
np.min(np.linalg.norm(points - frame.sensor_position_map, axis=1))
|
||||
)
|
||||
observations.append(
|
||||
ObstacleObservation(
|
||||
observation_id=(
|
||||
f"{packet.envelope.frame_id}:low-step:{component_index}"
|
||||
),
|
||||
occupancy_key=(
|
||||
f"{packet.envelope.frame_id}:low-step:{component_index}"
|
||||
),
|
||||
source_id=packet.envelope.source_id,
|
||||
frame_id=packet.envelope.frame_id,
|
||||
evidence_time_ns=packet.envelope.timestamps.source_ns,
|
||||
basis=EvidenceBasis.LIDAR,
|
||||
currentness=EvidenceCurrentness.CURRENT,
|
||||
occupied_support=True,
|
||||
source_point_ids=point_ids,
|
||||
metric_geometry=MetricGeometry(
|
||||
coordinate_frame=self.store.profile.coordinate_frame,
|
||||
centroid_xyz_m=(
|
||||
float(centroid[0]),
|
||||
float(centroid[1]),
|
||||
float(centroid[2]),
|
||||
),
|
||||
range_m=nearest,
|
||||
covariance_diagonal_m2=(
|
||||
float(covariance[0]),
|
||||
float(covariance[1]),
|
||||
float(covariance[2]),
|
||||
),
|
||||
),
|
||||
proposal_ids=(),
|
||||
semantic_hint=None,
|
||||
reason_codes=(
|
||||
"additive-low-step-current-component",
|
||||
"occupied-only-never-free",
|
||||
),
|
||||
)
|
||||
)
|
||||
voxel_count += cells
|
||||
peak_voxels = max(peak_voxels, cells)
|
||||
return tuple(observations), candidate_count, voxel_count, peak_voxels
|
||||
|
||||
def snapshot(self) -> M48LowStepOccupancySnapshot:
|
||||
with self._lock:
|
||||
return M48LowStepOccupancySnapshot(
|
||||
base=self.base.snapshot(),
|
||||
input_frames=self._input_frames,
|
||||
completed_frames=self._completed_frames,
|
||||
failed_frames=self._failed_frames,
|
||||
frames_with_additions=self._frames_with_additions,
|
||||
candidate_point_count=self._candidate_points,
|
||||
additive_observation_count=self._additive_observation_count,
|
||||
additive_voxel_count=self._additive_voxels,
|
||||
peak_candidate_points_per_frame=self._peak_candidate_points,
|
||||
peak_additive_observations_per_frame=(
|
||||
self._peak_additive_observations
|
||||
),
|
||||
peak_voxels_per_component=self._peak_component_voxels,
|
||||
additive_core_duration_ns=self._additive_core_duration_ns,
|
||||
)
|
||||
|
||||
|
||||
def load_m48_low_step_occupancy_profile(
|
||||
path: Path,
|
||||
) -> M48LowStepOccupancyProfile:
|
||||
resolved = path.resolve(strict=True)
|
||||
if resolved.is_symlink() or not resolved.is_file():
|
||||
raise M48LowStepOccupancyError("low-step profile must be a regular file")
|
||||
raw = resolved.read_bytes()
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise M48LowStepOccupancyError("low-step profile JSON is invalid") from exc
|
||||
document = _object(value, "low-step profile")
|
||||
_exact_keys(
|
||||
document,
|
||||
{
|
||||
"schema_version",
|
||||
"profile_id",
|
||||
"provider_id",
|
||||
"base_geometry",
|
||||
"source",
|
||||
"componentization",
|
||||
"separation_expectations",
|
||||
"acceptance",
|
||||
"policy",
|
||||
"authority",
|
||||
},
|
||||
"low-step profile",
|
||||
)
|
||||
if (
|
||||
document["schema_version"] != M48_LOW_STEP_PROFILE_SCHEMA
|
||||
or document["provider_id"] != M48_LOW_STEP_PROVIDER_ID
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step profile identity changed")
|
||||
base = _object(document["base_geometry"], "base geometry")
|
||||
source = _object(document["source"], "low-step source")
|
||||
component = _object(document["componentization"], "componentization")
|
||||
acceptance = _object(document["acceptance"], "low-step acceptance")
|
||||
_exact_keys(base, {"profile_id", "sha256"}, "base geometry")
|
||||
_exact_keys(
|
||||
source,
|
||||
{
|
||||
"source_id",
|
||||
"session_id",
|
||||
"frame_count",
|
||||
"point_count",
|
||||
"local_surface_model_id",
|
||||
"local_surface_sha256",
|
||||
"m48r2_result_id",
|
||||
"m48r2_cases_sha256",
|
||||
},
|
||||
"low-step source",
|
||||
)
|
||||
_exact_keys(
|
||||
component,
|
||||
{
|
||||
"voxel_size_m",
|
||||
"neighbor_radius_cells",
|
||||
"minimum_points",
|
||||
"minimum_voxels",
|
||||
"local_radius_m",
|
||||
"maximum_candidate_points_per_frame",
|
||||
"maximum_cells_per_component",
|
||||
"maximum_components_per_frame",
|
||||
"exclude_baseline_occupied_points",
|
||||
"exclude_claimed_source_points",
|
||||
},
|
||||
"componentization",
|
||||
)
|
||||
_exact_keys(
|
||||
acceptance,
|
||||
{
|
||||
"expected_frames",
|
||||
"requested_source_rate_hz",
|
||||
"minimum_effective_world_state_fps",
|
||||
"maximum_world_state_completion_p95_ms",
|
||||
"maximum_geometry_stage_p95_ms",
|
||||
"maximum_geometry_stage_p99_ms",
|
||||
"maximum_fps_regression_fraction_vs_native_baseline",
|
||||
"maximum_world_state_p95_delta_ms_vs_native_baseline",
|
||||
"maximum_additive_component_mean_growth_fraction",
|
||||
"maximum_additive_cell_mean_growth_fraction",
|
||||
"maximum_capacity_drop_count",
|
||||
"minimum_critical_near_recall",
|
||||
"minimum_canonical_engineering_recall",
|
||||
"maximum_false_free_count",
|
||||
},
|
||||
"low-step acceptance",
|
||||
)
|
||||
if (
|
||||
component["exclude_baseline_occupied_points"] is not True
|
||||
or component["exclude_claimed_source_points"] is not True
|
||||
):
|
||||
raise M48LowStepOccupancyError("low-step point ownership policy changed")
|
||||
for key in (
|
||||
"expected_frames",
|
||||
"maximum_capacity_drop_count",
|
||||
"maximum_false_free_count",
|
||||
):
|
||||
item = acceptance.get(key)
|
||||
if not isinstance(item, int) or isinstance(item, bool) or item < 0:
|
||||
raise M48LowStepOccupancyError("low-step acceptance bounds are invalid")
|
||||
for key in (
|
||||
"requested_source_rate_hz",
|
||||
"minimum_effective_world_state_fps",
|
||||
"maximum_world_state_completion_p95_ms",
|
||||
"maximum_geometry_stage_p95_ms",
|
||||
"maximum_geometry_stage_p99_ms",
|
||||
"maximum_fps_regression_fraction_vs_native_baseline",
|
||||
"maximum_world_state_p95_delta_ms_vs_native_baseline",
|
||||
"maximum_additive_component_mean_growth_fraction",
|
||||
"maximum_additive_cell_mean_growth_fraction",
|
||||
"minimum_critical_near_recall",
|
||||
"minimum_canonical_engineering_recall",
|
||||
):
|
||||
if _number(acceptance, key) < 0.0:
|
||||
raise M48LowStepOccupancyError("low-step acceptance bounds are invalid")
|
||||
if (
|
||||
not _string(source, "m48r2_result_id").startswith(
|
||||
"m48-static-occupancy-qualification-"
|
||||
)
|
||||
or len(_string(source, "m48r2_result_id"))
|
||||
!= len("m48-static-occupancy-qualification-") + 64
|
||||
):
|
||||
raise M48LowStepOccupancyError("M4.8R2 result binding is invalid")
|
||||
_digest(source, "m48r2_cases_sha256")
|
||||
expected_policy = {
|
||||
"absence_of_points_means_free": False,
|
||||
"absence_of_camera_detection_means_free": False,
|
||||
"additive_only": True,
|
||||
"semantic_class_used": False,
|
||||
"ray_clearing_used": False,
|
||||
"planner_authoritative_free_space_claimed": False,
|
||||
}
|
||||
expected_authority = {
|
||||
"mode": "replay-simulated",
|
||||
"physical_live": False,
|
||||
"commands_enabled": False,
|
||||
"actuation_allowed": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
if document["policy"] != expected_policy or document["authority"] != expected_authority:
|
||||
raise M48LowStepOccupancyError("low-step policy or authority changed")
|
||||
expectations_value = document["separation_expectations"]
|
||||
if not isinstance(expectations_value, list) or not expectations_value:
|
||||
raise M48LowStepOccupancyError("low-step separation expectations are missing")
|
||||
expectations: list[LowStepSeparationExpectation] = []
|
||||
for value in expectations_value:
|
||||
item = _object(value, "separation expectation")
|
||||
_exact_keys(
|
||||
item,
|
||||
{"anchor_id", "sequence", "expected_minimum_components", "interpretation"},
|
||||
"separation expectation",
|
||||
)
|
||||
expectations.append(
|
||||
LowStepSeparationExpectation(
|
||||
anchor_id=_string(item, "anchor_id"),
|
||||
sequence=_positive_integer(item, "sequence"),
|
||||
expected_minimum_components=_positive_integer(
|
||||
item, "expected_minimum_components"
|
||||
),
|
||||
interpretation=_string(item, "interpretation"),
|
||||
)
|
||||
)
|
||||
return M48LowStepOccupancyProfile(
|
||||
profile_id=_string(document, "profile_id"),
|
||||
provider_id=_string(document, "provider_id"),
|
||||
source_id=_string(source, "source_id"),
|
||||
session_id=_string(source, "session_id"),
|
||||
frame_count=_positive_integer(source, "frame_count"),
|
||||
point_count=_positive_integer(source, "point_count"),
|
||||
local_surface_model_id=_string(source, "local_surface_model_id"),
|
||||
local_surface_sha256=_digest(source, "local_surface_sha256"),
|
||||
base_geometry_profile_id=_string(base, "profile_id"),
|
||||
base_geometry_profile_sha256=_digest(base, "sha256"),
|
||||
component=LowStepComponentProfile(
|
||||
voxel_size_m=_number(component, "voxel_size_m"),
|
||||
neighbor_radius_cells=_positive_integer(
|
||||
component, "neighbor_radius_cells"
|
||||
),
|
||||
minimum_points=_positive_integer(component, "minimum_points"),
|
||||
minimum_voxels=_positive_integer(component, "minimum_voxels"),
|
||||
local_radius_m=_number(component, "local_radius_m"),
|
||||
maximum_candidate_points_per_frame=_positive_integer(
|
||||
component, "maximum_candidate_points_per_frame"
|
||||
),
|
||||
maximum_cells_per_component=_positive_integer(
|
||||
component, "maximum_cells_per_component"
|
||||
),
|
||||
maximum_components_per_frame=_positive_integer(
|
||||
component, "maximum_components_per_frame"
|
||||
),
|
||||
),
|
||||
separation_expectations=tuple(expectations),
|
||||
profile_sha256=hashlib.sha256(raw).hexdigest(),
|
||||
)
|
||||
|
||||
|
||||
def _voxel_components(
|
||||
points_map: npt.NDArray[np.float64],
|
||||
source_indices: IntArray,
|
||||
profile: LowStepComponentProfile,
|
||||
) -> tuple[tuple[IntArray, int], ...]:
|
||||
if source_indices.size == 0:
|
||||
return ()
|
||||
cells = np.floor(
|
||||
points_map[source_indices] / profile.voxel_size_m
|
||||
).astype(np.int64)
|
||||
cell_points: dict[tuple[int, int, int], list[int]] = {}
|
||||
for local_index, row in enumerate(cells):
|
||||
key = (int(row[0]), int(row[1]), int(row[2]))
|
||||
cell_points.setdefault(key, []).append(int(source_indices[local_index]))
|
||||
remaining = set(cell_points)
|
||||
radius = profile.neighbor_radius_cells
|
||||
neighbors = tuple(
|
||||
(dx, dy, dz)
|
||||
for dx in range(-radius, radius + 1)
|
||||
for dy in range(-radius, radius + 1)
|
||||
for dz in range(-radius, radius + 1)
|
||||
if dx or dy or dz
|
||||
)
|
||||
components: list[tuple[IntArray, int]] = []
|
||||
while remaining:
|
||||
seed = min(remaining)
|
||||
remaining.remove(seed)
|
||||
connected = [seed]
|
||||
queue = deque((seed,))
|
||||
while queue:
|
||||
cell = queue.popleft()
|
||||
for delta in neighbors:
|
||||
neighbor = (
|
||||
cell[0] + delta[0],
|
||||
cell[1] + delta[1],
|
||||
cell[2] + delta[2],
|
||||
)
|
||||
if neighbor not in remaining:
|
||||
continue
|
||||
remaining.remove(neighbor)
|
||||
connected.append(neighbor)
|
||||
queue.append(neighbor)
|
||||
indices = np.asarray(
|
||||
[point for cell in sorted(connected) for point in cell_points[cell]],
|
||||
dtype=np.int64,
|
||||
)
|
||||
components.append((indices, len(connected)))
|
||||
components.sort(key=lambda item: int(item[0][0]))
|
||||
return tuple(components)
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise M48LowStepOccupancyError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(value: dict[str, object], expected: set[str], label: str) -> None:
|
||||
if set(value) != expected:
|
||||
raise M48LowStepOccupancyError(f"{label} fields changed")
|
||||
|
||||
|
||||
def _string(value: dict[str, object], key: str) -> str:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, str) or not item.strip():
|
||||
raise M48LowStepOccupancyError(f"{key} must be a non-empty string")
|
||||
return item
|
||||
|
||||
|
||||
def _positive_integer(value: dict[str, object], key: str) -> int:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, int) or isinstance(item, bool) or item < 1:
|
||||
raise M48LowStepOccupancyError(f"{key} must be a positive integer")
|
||||
return item
|
||||
|
||||
|
||||
def _number(value: dict[str, object], key: str) -> float:
|
||||
item = value.get(key)
|
||||
if not isinstance(item, (int, float)) or isinstance(item, bool):
|
||||
raise M48LowStepOccupancyError(f"{key} must be numeric")
|
||||
result = float(item)
|
||||
if not math.isfinite(result):
|
||||
raise M48LowStepOccupancyError(f"{key} must be finite")
|
||||
return result
|
||||
|
||||
|
||||
def _digest(value: dict[str, object], key: str) -> str:
|
||||
item = _string(value, key)
|
||||
if len(item) != 64 or any(character not in "0123456789abcdef" for character in item):
|
||||
raise M48LowStepOccupancyError(f"{key} must be a SHA-256 digest")
|
||||
return item
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48AdditiveLowStepGeometryProvider",
|
||||
"M48LowStepOccupancyError",
|
||||
"M48LowStepOccupancyProfile",
|
||||
"M48LowStepOccupancySnapshot",
|
||||
"M48_LOW_STEP_PROVIDER_ID",
|
||||
"load_m48_low_step_occupancy_profile",
|
||||
]
|
||||
@@ -26,6 +26,10 @@ from .geometry import (
|
||||
)
|
||||
from .graph import DeliveryEvidenceObserver, ReferencePerceptionGraphV2
|
||||
from .graph_contracts import DeliveredFrame, GraphRunMode
|
||||
from .m48_low_step_occupancy import (
|
||||
M48AdditiveLowStepGeometryProvider,
|
||||
load_m48_low_step_occupancy_profile,
|
||||
)
|
||||
from .motion import ClassIndependentMotionEstimator
|
||||
from .providers import (
|
||||
DetectorProvider,
|
||||
@@ -122,6 +126,7 @@ def build_m48s_reference_graph_runtime(
|
||||
decode_timing_observer: DecodeTimingObserver | None = None,
|
||||
source_pacing_observer: SourcePacingObserver | None = None,
|
||||
detector_timing_observer: DetectorTimingObserver | None = None,
|
||||
additive_low_step_profile: Path | None = None,
|
||||
maximum_frames: int | None = None,
|
||||
source_rate_hz: float | None = None,
|
||||
source_prefetch_capacity_frames: int = 64,
|
||||
@@ -133,7 +138,9 @@ def build_m48s_reference_graph_runtime(
|
||||
pinned_files = {
|
||||
ProviderRole.SOURCE: paths.baseline_profile,
|
||||
ProviderRole.DETECTOR: detector_profile,
|
||||
ProviderRole.GEOMETRY: paths.geometry_profile,
|
||||
ProviderRole.GEOMETRY: (
|
||||
additive_low_step_profile or paths.geometry_profile
|
||||
),
|
||||
ProviderRole.TEMPORAL: paths.temporal_motion_profile,
|
||||
ProviderRole.MOTION: paths.temporal_motion_profile,
|
||||
ProviderRole.ROLLING: paths.rolling_map_profile,
|
||||
@@ -144,6 +151,11 @@ def build_m48s_reference_graph_runtime(
|
||||
|
||||
load_m4_baseline(paths.baseline_profile)
|
||||
geometry_profile = load_geometry_profile(paths.geometry_profile)
|
||||
low_step_profile = (
|
||||
None
|
||||
if additive_low_step_profile is None
|
||||
else load_m48_low_step_occupancy_profile(additive_low_step_profile)
|
||||
)
|
||||
temporal_motion_profile = load_temporal_motion_profile(paths.temporal_motion_profile)
|
||||
rolling_map_profile = load_rolling_map_profile(paths.rolling_map_profile)
|
||||
threat_profile = load_replay_threat_profile(paths.threat_profile)
|
||||
@@ -204,11 +216,19 @@ def build_m48s_reference_graph_runtime(
|
||||
store,
|
||||
profile=threat_profile.body_frame,
|
||||
)
|
||||
geometry = (
|
||||
Ravnoves00GeometryAssociationProvider(store=store)
|
||||
if low_step_profile is None
|
||||
else M48AdditiveLowStepGeometryProvider(
|
||||
store=store,
|
||||
profile=low_step_profile,
|
||||
)
|
||||
)
|
||||
graph = ReferencePerceptionGraphV2(
|
||||
config=config,
|
||||
source=source,
|
||||
detector=detector,
|
||||
geometry=Ravnoves00GeometryAssociationProvider(store=store),
|
||||
geometry=geometry,
|
||||
temporal=BoundedSpatialTemporalProvider(
|
||||
point_resolver=store,
|
||||
profile=temporal_motion_profile,
|
||||
|
||||
@@ -38,13 +38,13 @@ from .threat_timeline import (
|
||||
)
|
||||
|
||||
FRAME_EVIDENCE_SCHEMA: Final = "missioncore.m48s-reference-graph-frame-evidence/v0"
|
||||
M48R3_FRAME_EVIDENCE_SCHEMA: Final = (
|
||||
"missioncore.m48s-reference-graph-frame-evidence/v1"
|
||||
)
|
||||
EXPECTED_FRAME_COUNT: Final = 4_489
|
||||
CAMERA_ACCUMULATION_WINDOW_SECONDS: Final = 2.0
|
||||
CAMERA_ACCUMULATION_POINT_LIMIT: Final = 20_000
|
||||
CAMERA_POINT_OVERLAY_SCHEMA: Final = "missioncore.m48s-camera-point-overlay/v1"
|
||||
_FRAME_EVIDENCE_SCHEMA_MARKER: Final = (
|
||||
b'"schema_version":"missioncore.m48s-reference-graph-frame-evidence/v0"'
|
||||
)
|
||||
_SOURCE_ENVELOPE_MARKER: Final = b'"source_envelope":'
|
||||
_JSON_DECODER: Final = json.JSONDecoder()
|
||||
|
||||
@@ -61,16 +61,37 @@ class _LedgerIndex:
|
||||
class M48sReplayTimeline:
|
||||
"""Read source-indexed chunks while preserving latest-wins world-state gaps."""
|
||||
|
||||
def __init__(self, *, repository_root: Path, result_root: Path, result_id: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
repository_root: Path,
|
||||
result_root: Path,
|
||||
result_id: str,
|
||||
frames_name: str = "reference-graph-replay-frames.jsonl",
|
||||
worker_result_name: str = "reference-graph-replay-worker-result.json",
|
||||
frame_evidence_schema: str = FRAME_EVIDENCE_SCHEMA,
|
||||
camera_endpoint_root: str = (
|
||||
"/api/v1/laboratory/m48s/fixed-class-detector"
|
||||
),
|
||||
) -> None:
|
||||
self.repository_root = repository_root.resolve(strict=True)
|
||||
self.result_root = result_root.resolve(strict=True)
|
||||
self.result_id = result_id
|
||||
self.frames_path = (
|
||||
self.result_root / "reference-graph-replay-frames.jsonl"
|
||||
).resolve(strict=True)
|
||||
self.worker_path = (
|
||||
self.result_root / "reference-graph-replay-worker-result.json"
|
||||
).resolve(strict=True)
|
||||
if (
|
||||
Path(frames_name).name != frames_name
|
||||
or Path(worker_result_name).name != worker_result_name
|
||||
or frame_evidence_schema not in {
|
||||
FRAME_EVIDENCE_SCHEMA,
|
||||
M48R3_FRAME_EVIDENCE_SCHEMA,
|
||||
}
|
||||
or not camera_endpoint_root.startswith("/api/v1/laboratory/")
|
||||
or camera_endpoint_root.endswith("/")
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S timeline binding is invalid")
|
||||
self.frame_evidence_schema = frame_evidence_schema
|
||||
self.camera_endpoint_root = camera_endpoint_root
|
||||
self.frames_path = (self.result_root / frames_name).resolve(strict=True)
|
||||
self.worker_path = (self.result_root / worker_result_name).resolve(strict=True)
|
||||
if (
|
||||
self.frames_path.parent != self.result_root
|
||||
or self.worker_path.parent != self.result_root
|
||||
@@ -96,7 +117,12 @@ class M48sReplayTimeline:
|
||||
raise M48sReplayTimelineError("M4.8S source clock is not monotonic")
|
||||
worker = _object(json.loads(self.worker_path.read_text("utf-8")), "worker result")
|
||||
self.outcomes = _terminal_outcomes(worker)
|
||||
self.index = _index_ledger(self.frames_path, self.source_times_ns, self.outcomes)
|
||||
self.index = _index_ledger(
|
||||
self.frames_path,
|
||||
self.source_times_ns,
|
||||
self.outcomes,
|
||||
frame_evidence_schema=self.frame_evidence_schema,
|
||||
)
|
||||
self._cache_lock = Lock()
|
||||
self._chunk_json_cache: OrderedDict[tuple[int, int], bytes] = OrderedDict()
|
||||
self._camera_point_json_cache: OrderedDict[int, bytes] = OrderedDict()
|
||||
@@ -432,8 +458,8 @@ class M48sReplayTimeline:
|
||||
"camera_proposals": camera_proposals,
|
||||
"decision_counts": _decision_counts(assessments),
|
||||
"camera_url": (
|
||||
"/api/v1/laboratory/m48s/fixed-class-detector/"
|
||||
f"{self.result_id}/timeline/frames/{sequence}/camera"
|
||||
f"{self.camera_endpoint_root}/{self.result_id}"
|
||||
f"/timeline/frames/{sequence}/camera"
|
||||
),
|
||||
"ground_truth": False,
|
||||
"authority": "replay-simulated",
|
||||
@@ -447,7 +473,10 @@ class M48sReplayTimeline:
|
||||
stream.seek(offset)
|
||||
line = stream.readline()
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict) or value.get("schema_version") != FRAME_EVIDENCE_SCHEMA:
|
||||
if (
|
||||
not isinstance(value, dict)
|
||||
or value.get("schema_version") != self.frame_evidence_schema
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S frame row is invalid")
|
||||
return value
|
||||
|
||||
@@ -456,6 +485,8 @@ def _index_ledger(
|
||||
path: Path,
|
||||
source_times_ns: tuple[int, ...],
|
||||
outcomes: dict[int, str],
|
||||
*,
|
||||
frame_evidence_schema: str,
|
||||
) -> _LedgerIndex:
|
||||
offsets: dict[int, int] = {}
|
||||
with path.open("rb") as stream:
|
||||
@@ -464,7 +495,10 @@ def _index_ledger(
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
break
|
||||
envelope = _ledger_source_envelope(line)
|
||||
envelope = _ledger_source_envelope(
|
||||
line,
|
||||
frame_evidence_schema=frame_evidence_schema,
|
||||
)
|
||||
timestamps = _object(envelope.get("timestamps"), "source timestamps")
|
||||
sequence = envelope.get("sequence")
|
||||
if (
|
||||
@@ -483,7 +517,11 @@ def _index_ledger(
|
||||
return _LedgerIndex(offsets)
|
||||
|
||||
|
||||
def _ledger_source_envelope(line: bytes) -> dict[str, object]:
|
||||
def _ledger_source_envelope(
|
||||
line: bytes,
|
||||
*,
|
||||
frame_evidence_schema: str,
|
||||
) -> dict[str, object]:
|
||||
"""Validate a ledger row while decoding only its small trailing envelope.
|
||||
|
||||
The full row can exceed 100 KiB because it contains the delivered world
|
||||
@@ -492,10 +530,10 @@ def _ledger_source_envelope(line: bytes) -> dict[str, object]:
|
||||
the GIL for many seconds.
|
||||
"""
|
||||
|
||||
if (
|
||||
line.count(_FRAME_EVIDENCE_SCHEMA_MARKER) != 1
|
||||
or line.count(_SOURCE_ENVELOPE_MARKER) != 1
|
||||
):
|
||||
schema_marker = (
|
||||
f'"schema_version":"{frame_evidence_schema}"'.encode("ascii")
|
||||
)
|
||||
if line.count(schema_marker) != 1 or line.count(_SOURCE_ENVELOPE_MARKER) != 1:
|
||||
raise M48sReplayTimelineError("M4.8S ledger schema changed")
|
||||
start = line.find(_SOURCE_ENVELOPE_MARKER) + len(_SOURCE_ENVELOPE_MARKER)
|
||||
try:
|
||||
@@ -569,6 +607,8 @@ __all__ = [
|
||||
"CAMERA_ACCUMULATION_POINT_LIMIT",
|
||||
"CAMERA_ACCUMULATION_WINDOW_SECONDS",
|
||||
"CAMERA_POINT_OVERLAY_SCHEMA",
|
||||
"FRAME_EVIDENCE_SCHEMA",
|
||||
"M48R3_FRAME_EVIDENCE_SCHEMA",
|
||||
"M48sReplayTimeline",
|
||||
"M48sReplayTimelineError",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.perception.contracts import (
|
||||
ClockBasis,
|
||||
ModalityOutcome,
|
||||
ModalityStatus,
|
||||
ObstacleObservation,
|
||||
SourceEnvelope,
|
||||
TimestampBundle,
|
||||
)
|
||||
from k1link.perception.geometry import GeometryFrame, RecordedGeometryStore
|
||||
from k1link.perception.geometry_math import Kb4ProjectionProfile, project_map_points_kb4
|
||||
from k1link.perception.m48_low_step_occupancy import (
|
||||
M48AdditiveLowStepGeometryProvider,
|
||||
load_m48_low_step_occupancy_profile,
|
||||
)
|
||||
from k1link.perception.providers import SourcePacket
|
||||
from k1link.perception.recorded_source import RecordedFrameReference
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILE_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48r3-additive-low-step-occupancy-v1.json"
|
||||
)
|
||||
R2_CASES_PATH = (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime/compute-experiments/m48/static-occupancy-qualification-results"
|
||||
/ (
|
||||
"m48-static-occupancy-qualification-"
|
||||
"568024554cff011332ff19ca4739f70555a6232c0607dec2a71be6db408ea69a"
|
||||
)
|
||||
/ "cases.jsonl"
|
||||
)
|
||||
|
||||
|
||||
class _Store:
|
||||
def __init__(self, frame: GeometryFrame, step: np.ndarray) -> None:
|
||||
self.profile = RecordedGeometryStore.from_repository(REPOSITORY_ROOT).profile
|
||||
self._frame = frame
|
||||
self._step = np.asarray(step, dtype=np.uint8)
|
||||
|
||||
def frame(self, _packet: SourcePacket) -> GeometryFrame:
|
||||
return self._frame
|
||||
|
||||
def point_step_candidates_for_frame(self, _frame_index: int) -> np.ndarray:
|
||||
return self._step
|
||||
|
||||
|
||||
def _status() -> ModalityStatus:
|
||||
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
|
||||
|
||||
|
||||
def _packet(sequence: int = 0) -> SourcePacket:
|
||||
profile = RecordedGeometryStore.from_repository(REPOSITORY_ROOT).profile
|
||||
reference = RecordedFrameReference(profile.source_pack_id, sequence)
|
||||
return SourcePacket(
|
||||
envelope=SourceEnvelope(
|
||||
source_id=profile.source_id,
|
||||
session_id=profile.session_id,
|
||||
frame_id=f"frame-{sequence:06d}",
|
||||
sequence=sequence,
|
||||
timestamps=TimestampBundle(
|
||||
utc_ns=sequence + 1,
|
||||
monotonic_ns=sequence + 2,
|
||||
source_ns=sequence + 3,
|
||||
clock_basis=ClockBasis.RECORDED_HOST,
|
||||
),
|
||||
source_age_ns=0,
|
||||
binding_reason="test-recorded-source",
|
||||
calibration_id="camera-1-kb4-test",
|
||||
representation_id="registered-map-increment-v1",
|
||||
image=_status(),
|
||||
registered_point_increment=_status(),
|
||||
pose=_status(),
|
||||
),
|
||||
image_payload="image",
|
||||
registered_point_increment_payload=reference,
|
||||
pose_payload=reference,
|
||||
)
|
||||
|
||||
|
||||
def _frame(points: np.ndarray) -> GeometryFrame:
|
||||
return GeometryFrame(
|
||||
frame_index=0,
|
||||
points_map=np.asarray(points, dtype=np.float64),
|
||||
point_class=np.ones(points.shape[0], dtype=np.uint8),
|
||||
sensor_position_map=np.zeros(3, dtype=np.float64),
|
||||
sensor_orientation_xyzw=np.asarray((0.0, 0.0, 0.0, 1.0), dtype=np.float64),
|
||||
projection=Kb4ProjectionProfile(
|
||||
width=100,
|
||||
height=100,
|
||||
intrinsic_fx_fy_cx_cy=(100.0, 100.0, 50.0, 50.0),
|
||||
distortion_kb4=(0.0, 0.0, 0.0, 0.0),
|
||||
t_camera_from_lidar=np.eye(4, dtype=np.float64),
|
||||
),
|
||||
surface_valid=True,
|
||||
)
|
||||
|
||||
|
||||
def test_wide_operator_region_cannot_bridge_two_spatial_components() -> None:
|
||||
points = np.asarray(
|
||||
(
|
||||
(0.00, 0.0, 5.00),
|
||||
(0.02, 0.0, 5.00),
|
||||
(0.04, 0.0, 5.00),
|
||||
(0.06, 0.0, 5.00),
|
||||
(0.08, 0.0, 5.00),
|
||||
(1.70, 0.0, 5.00),
|
||||
(1.72, 0.0, 5.00),
|
||||
(1.74, 0.0, 5.00),
|
||||
(1.76, 0.0, 5.00),
|
||||
(1.78, 0.0, 5.00),
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
store = _Store(_frame(points), np.ones(10, dtype=np.uint8))
|
||||
provider = M48AdditiveLowStepGeometryProvider( # type: ignore[arg-type]
|
||||
store=store,
|
||||
profile=load_m48_low_step_occupancy_profile(PROFILE_PATH),
|
||||
)
|
||||
|
||||
observations = provider.associate(_packet(), ())
|
||||
additive = tuple(
|
||||
item
|
||||
for item in observations
|
||||
if "additive-low-step-current-component" in item.reason_codes
|
||||
)
|
||||
|
||||
assert len(additive) == 2
|
||||
assert {item.source_point_ids for item in additive} == {
|
||||
(0, 1, 2, 3, 4),
|
||||
(5, 6, 7, 8, 9),
|
||||
}
|
||||
assert all(item.semantic_hint is None for item in additive)
|
||||
assert all("occupied-only-never-free" in item.reason_codes for item in additive)
|
||||
snapshot = provider.snapshot()
|
||||
assert snapshot.additive_observation_count == 2
|
||||
assert snapshot.additive_voxel_count == 2
|
||||
assert snapshot.failed_frames == 0
|
||||
|
||||
|
||||
def test_frame_1856_preserves_baseline_posts_and_splits_low_hemisphere_support() -> None:
|
||||
store = RecordedGeometryStore.from_repository(REPOSITORY_ROOT)
|
||||
provider = M48AdditiveLowStepGeometryProvider(
|
||||
store=store,
|
||||
profile=load_m48_low_step_occupancy_profile(PROFILE_PATH),
|
||||
)
|
||||
# Operator-anchor sequences are one-based display frame numbers, while the
|
||||
# recorded-source packet and timeline use zero-based source sequences.
|
||||
source_sequence = 1856 - 1
|
||||
frame = store.frame_for_index(source_sequence)
|
||||
assert frame is not None
|
||||
observations = provider.associate(_packet(source_sequence), ())
|
||||
projected = project_map_points_kb4(
|
||||
frame.points_map,
|
||||
position_map_xyz=frame.sensor_position_map,
|
||||
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
|
||||
profile=frame.projection,
|
||||
)
|
||||
source_rows = {
|
||||
int(source_index): row
|
||||
for row, source_index in enumerate(projected.source_indices)
|
||||
}
|
||||
cases = [
|
||||
json.loads(line)
|
||||
for line in R2_CASES_PATH.read_text("utf-8").splitlines()
|
||||
if line.strip() and json.loads(line)["sequence"] == 1856
|
||||
]
|
||||
by_anchor = {row["anchor_id"]: row for row in cases}
|
||||
posts = by_anchor["anchor-5e6e2a81e0667bdd9faf2a9e"]
|
||||
hemispheres = by_anchor["anchor-924a4623077fe5df18816b47"]
|
||||
|
||||
assert posts["accepted_graph"]["component_count"] >= 2
|
||||
assert _component_hits(
|
||||
observations,
|
||||
hemispheres["extent_xyxy"],
|
||||
projected.pixels_xy,
|
||||
source_rows,
|
||||
width=frame.projection.width,
|
||||
height=frame.projection.height,
|
||||
) >= 2
|
||||
|
||||
|
||||
def _component_hits(
|
||||
observations: tuple[ObstacleObservation, ...],
|
||||
extent: list[float],
|
||||
pixels: np.ndarray,
|
||||
source_rows: dict[int, int],
|
||||
*,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> int:
|
||||
x1, y1, x2, y2 = (
|
||||
extent[0] * width,
|
||||
extent[1] * height,
|
||||
extent[2] * width,
|
||||
extent[3] * height,
|
||||
)
|
||||
count = 0
|
||||
for value in observations:
|
||||
indices = value.source_point_ids
|
||||
if not indices:
|
||||
continue
|
||||
rows = [source_rows[index] for index in indices if index in source_rows]
|
||||
if any(
|
||||
x1 <= pixels[row, 0] <= x2 and y1 <= pixels[row, 1] <= y2
|
||||
for row in rows
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILDER_PATH = REPOSITORY_ROOT / "scripts/build_m48r3_worker_shadow_artifact.py"
|
||||
SPEC = importlib.util.spec_from_file_location("m48r3_worker_shadow_builder", BUILDER_PATH)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
BUILDER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BUILDER)
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _regular_files(archive: tarfile.TarFile) -> dict[str, bytes]:
|
||||
result: dict[str, bytes] = {}
|
||||
for member in archive.getmembers():
|
||||
if not member.isfile():
|
||||
continue
|
||||
stream = archive.extractfile(member)
|
||||
assert stream is not None
|
||||
result[member.name] = stream.read()
|
||||
return result
|
||||
|
||||
|
||||
def test_m48r3_worker_artifact_is_deterministic_and_bounded(tmp_path: Path) -> None:
|
||||
patch_id = "mission-core-m48r3-low-step-unit-001"
|
||||
revision = "a" * 40
|
||||
first = BUILDER.build_artifact(patch_id, tmp_path / "first", revision=revision)
|
||||
second = BUILDER.build_artifact(patch_id, tmp_path / "second", revision=revision)
|
||||
|
||||
first_bytes = Path(first["artifact"]).read_bytes()
|
||||
assert first_bytes == Path(second["artifact"]).read_bytes()
|
||||
assert first["sha256"] == _sha256(first_bytes)
|
||||
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||
members = archive.getmembers()
|
||||
regular = _regular_files(archive)
|
||||
assert all(not member.issym() and not member.islnk() for member in members)
|
||||
assert set(regular) == {
|
||||
"manifest.env",
|
||||
"files.txt",
|
||||
*(f"payload/{name}" for name in first["payload_files"]),
|
||||
}
|
||||
assert regular["files.txt"].decode().splitlines() == first["payload_files"]
|
||||
release = json.loads(regular["payload/release.json"])
|
||||
assert release["code_revision"] == revision
|
||||
assert release["geometry_provider_id"] == (
|
||||
"ravnoves00-additive-low-step-geometry/v1"
|
||||
)
|
||||
assert release["candidate_accepted"] is False
|
||||
assert release["production_accepted"] is False
|
||||
assert release["durable_worker_action"] == "none"
|
||||
assert release["canonical_triton_action"] == "none"
|
||||
serialized = json.dumps(release).lower()
|
||||
assert "private key" not in serialized
|
||||
assert "password=" not in serialized
|
||||
@@ -24,6 +24,10 @@ NATIVE_GRAPH_CONFIG = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48n-rf-detr-native-reference-graph-shadow-v0.json"
|
||||
)
|
||||
LOW_STEP_GRAPH_CONFIG = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48r3-native-low-step-reference-graph-shadow-v1.json"
|
||||
)
|
||||
|
||||
|
||||
def test_m48s_reference_graph_replaces_only_the_detector_pin() -> None:
|
||||
@@ -116,6 +120,42 @@ def test_m48n_native_reference_graph_pins_every_profile_digest() -> None:
|
||||
assert pins[role].sha256 == hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def test_m48r3_graph_replaces_only_the_geometry_pin() -> None:
|
||||
candidate = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(LOW_STEP_GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
native = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(NATIVE_GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
candidate_pins = {item.role: item for item in candidate.providers}
|
||||
native_pins = {item.role: item for item in native.providers}
|
||||
|
||||
assert candidate.graph_id == native.graph_id == "reference-perception-graph/v2"
|
||||
assert candidate.queues == native.queues
|
||||
assert candidate.authority == native.authority
|
||||
assert candidate_pins[ProviderRole.GEOMETRY].provider_id == (
|
||||
"ravnoves00-additive-low-step-geometry/v1"
|
||||
)
|
||||
assert all(
|
||||
candidate_pins[role] == native_pins[role]
|
||||
for role in ProviderRole
|
||||
if role is not ProviderRole.GEOMETRY
|
||||
)
|
||||
|
||||
|
||||
def test_m48r3_graph_pins_the_additive_profile_digest() -> None:
|
||||
config = ReferencePerceptionGraphConfigV2.from_dict(
|
||||
json.loads(LOW_STEP_GRAPH_CONFIG.read_text("utf-8"))
|
||||
)
|
||||
pin = {item.role: item for item in config.providers}[ProviderRole.GEOMETRY]
|
||||
payload = (
|
||||
REPOSITORY_ROOT
|
||||
/ "config/perception/m48r3-additive-low-step-occupancy-v1.json"
|
||||
).read_bytes()
|
||||
|
||||
assert pin.sha256 == hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def test_m48s_advisory_policy_is_bounded_distinct_and_commandless() -> None:
|
||||
matrix = advisory_policy_matrix()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user