feat(perception): add TRAVEL RAVNOVES compatibility probe

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 19:25:19 +03:00
parent b0738438d4
commit 28d5297237
8 changed files with 1071 additions and 0 deletions
@@ -0,0 +1,56 @@
{
"schema_version": "missioncore.m49-travel-ravnoves-compatibility/v1",
"profile_id": "m49-travel-ravnoves-compatibility/v1",
"candidate": {
"id": "travel",
"revision": "95dc2fbd66a343efd9060c45a5711b6307a950a4",
"worker_image_id": "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
"upstream_profile": "kitti-64x4500"
},
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"representation": "registered-map-increment-v1",
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
"intensity_available": false,
"ring_available": false,
"per_point_time_available": false,
"native_ray_model_available": false
},
"anchors": [171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856],
"input_profiles": [
{
"id": "current_increment",
"description": "Exact current registered map increment transformed into the current lidar frame.",
"history_seconds": 0.0,
"local_radius_m": null,
"travel_min_range_m": 1.0,
"travel_max_range_m": 80.0
},
{
"id": "causal_rolling_1s",
"description": "Current and past registered increments from one second, bounded by the accepted local-map radius.",
"history_seconds": 1.0,
"local_radius_m": 12.0,
"travel_min_range_m": 1.0,
"travel_max_range_m": 80.0
}
],
"order_variants": ["source", "reversed"],
"compatibility_acceptance": {
"ground_and_nonground_account_for_all_in_range_input_points": true,
"minimum_partition_multiset_jaccard": 0.99,
"minimum_labeled_multiset_jaccard": 0.99,
"all_outputs_must_be_finite": true
},
"invariants": {
"future_frames_allowed": false,
"missing_support_means_free": false,
"rolling_support_can_clear_space": false,
"zero_filled_intensity_has_semantic_authority": false,
"camera_resize_or_rectification_allowed": false,
"gpu_allowed": false,
"navigation_or_actuation_allowed": false
}
}
@@ -0,0 +1,205 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId,
[string]$SourcePackPath = "D:\NDC_MISSIONCORE\runtime\derived\e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b\lidar-pack.npz",
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m49-t3-travel-ravnoves"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$TravelImageTag = "ndc/mission-core-m49-t3-travel:20260826"
$TravelImageId = "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f"
$ParityImageTag = "ndc-mission-core-m48t-upstream-parity:1.9.4-cu130"
$ParityImageId = "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label, [bool]$Create) {
if ($Create -and -not (Test-Path -LiteralPath $Path)) {
$null = New-Item -ItemType Directory -Path $Path
}
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
if (
-not $item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
) {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Resolve-DFile([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
if (
$item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
) {
throw "$Label must be a real D: file"
}
return $item.FullName
}
function Convert-ToDockerPath([string]$Path) {
return ($Path -replace "\\", "/")
}
function Get-Container([string]$Name) {
$rows = @(((& docker inspect $Name) | ConvertFrom-Json))
Assert-LastExitCode "Docker inspection for $Name"
if ($rows.Count -ne 1) { throw "Container identity for $Name is not unique" }
return $rows[0]
}
function Assert-Image([string]$Tag, [string]$ExpectedId) {
$rows = @(((& docker image inspect $Tag) | ConvertFrom-Json))
Assert-LastExitCode "Docker image inspection for $Tag"
if ($rows.Count -ne 1 -or [string]$rows[0].Id -cne $ExpectedId) {
throw "Pinned image identity changed for $Tag"
}
}
function Remove-ExactContainer([string]$Name) {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$Name$") {
& docker rm --force $Name *> $null
}
}
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") {
throw "M49 T3 RAVNOVES probe is pinned to Worker 006"
}
$release = Resolve-DDirectory $ReleaseRoot "M49 T3 probe release" $false
$payload = Resolve-DDirectory (Join-Path $release "payload") "M49 T3 probe payload" $false
$sourcePack = Resolve-DFile $SourcePackPath "RAVNOVES00 source pack"
$output = Resolve-DDirectory $OutputRoot "M49 T3 probe output root" $true
$runOutputCandidate = Join-Path $output $RunId
if (Test-Path -LiteralPath $runOutputCandidate) { throw "M49 T3 probe output already exists" }
$null = New-Item -ItemType Directory -Path $runOutputCandidate
$runOutput = Resolve-DDirectory $runOutputCandidate "M49 T3 probe run output" $false
$releaseDocument = Get-Content -LiteralPath (Join-Path $payload "release.json") -Raw | ConvertFrom-Json
if (
$releaseDocument.schema_version -cne "missioncore.m49-t3-ravnoves-worker-release/v1" -or
$releaseDocument.worker_id -cne "worker-006" -or
$releaseDocument.candidate_id -cne "travel"
) {
throw "M49 T3 probe release contract changed"
}
foreach ($property in $releaseDocument.files.PSObject.Properties) {
$path = Join-Path $payload $property.Name
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash.ToLowerInvariant()
if ($actual -cne [string]$property.Value.sha256) {
throw "M49 T3 probe payload digest changed: $($property.Name)"
}
}
$sourcePackSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $sourcePack).Hash.ToLowerInvariant()
if ($sourcePackSha -cne "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944") {
throw "RAVNOVES00 source pack digest changed"
}
$os = Get-CimInstance Win32_OperatingSystem
$freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB
if ($freeMemoryGiB -lt 16.0) {
throw ("M49 T3 probe requires 16 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB)
}
$canonicalTritonBefore = Get-Container "ndc-mission-core-triton"
if (
-not $canonicalTritonBefore.State.Running -or
$canonicalTritonBefore.State.Health.Status -cne "healthy"
) {
throw "Canonical Mission Core Triton must remain healthy during M49 T3 probe"
}
Assert-Image $TravelImageTag $TravelImageId
Assert-Image $ParityImageTag $ParityImageId
$prepareName = "ndc-mission-core-m49-t3-prepare-$RunId"
$travelName = "ndc-mission-core-m49-t3-probe-$RunId"
$analyzeName = "ndc-mission-core-m49-t3-analyze-$RunId"
foreach ($name in @($prepareName, $travelName, $analyzeName)) {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$name$") {
throw "M49 T3 probe container name already exists: $name"
}
}
$started = [DateTimeOffset]::UtcNow
try {
& docker run --rm --name $prepareName --network none --cpus 8 --memory 16g `
--entrypoint python3 `
--volume ((Convert-ToDockerPath $sourcePack) + ":/source/lidar-pack.npz:ro") `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/probe") `
$ParityImageTag /release/prepare_ravnoves_probe.py `
--source-pack /source/lidar-pack.npz `
--config /release/m49-travel-ravnoves-compatibility-v1.json `
--output-root /probe/inputs
Assert-LastExitCode "M49 T3 RAVNOVES input preparation"
& docker run --rm --name $travelName --network none --cpus 16 --memory 24g `
--entrypoint /bin/bash `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/probe") `
$TravelImageTag /release/run_ravnoves_probe.sh
Assert-LastExitCode "M49 T3 RAVNOVES TRAVEL probe"
& docker run --rm --name $analyzeName --network none --cpus 8 --memory 16g `
--entrypoint python3 `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/probe") `
$ParityImageTag /release/analyze_ravnoves_probe.py `
--probe-root /probe `
--config /release/m49-travel-ravnoves-compatibility-v1.json
Assert-LastExitCode "M49 T3 RAVNOVES result analysis"
} finally {
foreach ($name in @($prepareName, $travelName, $analyzeName)) {
Remove-ExactContainer $name
}
}
$completed = [DateTimeOffset]::UtcNow
$resultPath = Join-Path $runOutput "result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "M49 T3 RAVNOVES result.json is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if ($result.status -cne "passed") { throw "M49 T3 RAVNOVES probe did not complete" }
$canonicalTritonAfter = Get-Container "ndc-mission-core-triton"
if (
-not $canonicalTritonAfter.State.Running -or
$canonicalTritonAfter.State.Health.Status -cne "healthy" -or
[string]$canonicalTritonAfter.Id -cne [string]$canonicalTritonBefore.Id
) {
throw "Canonical Mission Core Triton changed during M49 T3 probe"
}
$summary = [ordered]@{
schema_version = "missioncore.m49-t3-ravnoves-worker-summary/v1"
worker_id = "worker-006"
run_id = $RunId
code_revision = [string]$releaseDocument.code_revision
travel_image_id = $TravelImageId
parity_image_id = $ParityImageId
source_pack_sha256 = $sourcePackSha
started_utc = $started.ToString("o")
wall_seconds = [math]::Round(($completed - $started).TotalSeconds, 6)
free_memory_gib_before = [math]::Round($freeMemoryGiB, 6)
canonical_triton_id = [string]$canonicalTritonAfter.Id
canonical_triton_health = [string]$canonicalTritonAfter.State.Health.Status
candidate_run_count = [int]$result.candidate_run_count
direct_upstream_profile_compatible = [bool]$result.summary.direct_upstream_profile_compatible
ravnoves00_visual_quality_accepted = $false
realtime_accepted = $false
navigation_or_actuation_allowed = $false
}
$summary | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath (
Join-Path $runOutput "worker-summary.json"
) -Encoding utf8
$summary | ConvertTo-Json -Depth 3
@@ -0,0 +1,61 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId
)
$ErrorActionPreference = "Stop"
$taskName = "MissionCore-M49T3TravelRavnoves"
$release = (Resolve-Path -LiteralPath $ReleaseRoot).Path
$runner = Join-Path $release "payload\Invoke-M49T3TravelRavnovesProbe.ps1"
if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
throw "M49 T3 RAVNOVES runner is missing"
}
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existing -and $existing.State -eq "Running") {
throw "$taskName is already running"
}
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = @(
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", "`"$runner`"",
"-ReleaseRoot", "`"$release`"",
"-RunId", "`"$RunId`""
) -join " "
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction `
-Execute $powerShell `
-Argument $arguments `
-WorkingDirectory $release
$principal = New-ScheduledTaskPrincipal `
-UserId $userId `
-LogonType Interactive `
-RunLevel Limited
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit ([TimeSpan]::FromHours(2))
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Principal $principal `
-Trigger $trigger `
-Settings $settings `
-Description "One-shot CPU-only M49 T3 TRAVEL compatibility probe on RAVNOVES00." `
-Force | Out-Null
Start-ScheduledTask -TaskName $taskName
[pscustomobject]@{
task_name = $taskName
run_id = $RunId
release_root = $release
state = (Get-ScheduledTask -TaskName $taskName).State.ToString()
} | ConvertTo-Json -Compress
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Analyze TRAVEL partitions without assigning navigation or visual-quality authority."""
from __future__ import annotations
import argparse
import csv
import hashlib
import json
from collections import Counter
from pathlib import Path
import numpy as np
class ProbeAnalysisError(RuntimeError):
"""The candidate output does not satisfy the evidence contract."""
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 _load(path: Path, columns: int) -> np.ndarray:
values = np.fromfile(path, dtype=np.float32)
if values.size % columns:
raise ProbeAnalysisError(f"malformed float32 output: {path}")
result = values.reshape(-1, columns)
if not np.isfinite(result).all():
raise ProbeAnalysisError(f"non-finite output: {path}")
return result
def _point_multiset(points: np.ndarray) -> Counter[bytes]:
contiguous = np.ascontiguousarray(points[:, :3], dtype=np.float32)
return Counter(row.tobytes() for row in contiguous)
def multiset_jaccard(left: np.ndarray, right: np.ndarray) -> float:
left_counts = _point_multiset(left)
right_counts = _point_multiset(right)
keys = left_counts.keys() | right_counts.keys()
intersection = sum(min(left_counts[key], right_counts[key]) for key in keys)
union = sum(max(left_counts[key], right_counts[key]) for key in keys)
return 1.0 if union == 0 else intersection / union
def _timings(path: Path) -> dict[tuple[str, str, int], dict[str, float | int]]:
result: dict[tuple[str, str, int], dict[str, float | int]] = {}
with path.open(newline="", encoding="utf-8") as stream:
for row in csv.DictReader(stream, delimiter="\t"):
key = (row["profile"], row["variant"], int(row["slot"]))
result[key] = {
"wall_seconds": float(row["wall_seconds"]),
"max_rss_kib": int(row["max_rss_kib"]),
}
if len(result) != 40:
raise ProbeAnalysisError("timing row count changed")
return result
def analyze(probe_root: Path, config_path: Path) -> dict[str, object]:
inputs = probe_root / "inputs"
outputs = probe_root / "outputs"
manifest = json.loads((inputs / "input-manifest.json").read_text(encoding="utf-8"))
config = json.loads(config_path.read_text(encoding="utf-8"))
if (
manifest.get("schema_version") != "missioncore.m49-travel-ravnoves-probe-input/v1"
or manifest.get("config_sha256") != sha256_file(config_path)
or len(manifest.get("records", [])) != 20
):
raise ProbeAnalysisError("probe input manifest changed")
timings = _timings(probe_root / "travel-timing.tsv")
rows: list[dict[str, object]] = []
variants: dict[tuple[str, int, str], np.ndarray] = {}
for record in manifest["records"]:
profile = str(record["profile_id"])
slot = int(record["slot"])
for variant in ("source", "reversed"):
input_path = inputs / record["variants"][variant]["relative_path"]
if sha256_file(input_path) != record["variants"][variant]["sha256"]:
raise ProbeAnalysisError("probe input changed after preparation")
native = _load(input_path, 4)
xy_range = np.linalg.norm(native[:, :2].astype(np.float64), axis=1)
eligible_input_points = int(np.count_nonzero((xy_range > 1.0) & (xy_range < 80.0)))
base = outputs / profile / variant / str(slot)
ground = _load(base.with_name(base.name + "_ground.bin"), 4)
nonground = _load(base.with_name(base.name + "_nonground.bin"), 4)
labeled = _load(base.with_name(base.name + "_labeled.bin"), 5)
variants[(profile, slot, variant + "-ground")] = ground
variants[(profile, slot, variant + "-nonground")] = nonground
variants[(profile, slot, variant + "-labeled")] = labeled
cluster_ids = np.unique(labeled[:, 4].astype(np.int64))
cluster_ids = cluster_ids[cluster_ids > 0]
rows.append(
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": int(record["anchor_frame_index"]),
"variant": variant,
"input_points": int(native.shape[0]),
"eligible_input_points": eligible_input_points,
"ground_points": int(ground.shape[0]),
"nonground_points": int(nonground.shape[0]),
"labeled_points": int(labeled.shape[0]),
"cluster_count": int(cluster_ids.size),
"partition_accounted_for_all_eligible_input_points": bool(
ground.shape[0] + nonground.shape[0] == eligible_input_points
),
"nonground_labeled_fraction": (
1.0
if nonground.shape[0] == 0
else float(labeled.shape[0] / nonground.shape[0])
),
**timings[(profile, variant, slot)],
}
)
threshold = float(config["compatibility_acceptance"]["minimum_partition_multiset_jaccard"])
labeled_threshold = float(
config["compatibility_acceptance"]["minimum_labeled_multiset_jaccard"]
)
comparisons: list[dict[str, object]] = []
for profile in ("current_increment", "causal_rolling_1s"):
for slot, anchor in enumerate(config["anchors"]):
ground_jaccard = multiset_jaccard(
variants[(profile, slot, "source-ground")],
variants[(profile, slot, "reversed-ground")],
)
nonground_jaccard = multiset_jaccard(
variants[(profile, slot, "source-nonground")],
variants[(profile, slot, "reversed-nonground")],
)
labeled_jaccard = multiset_jaccard(
variants[(profile, slot, "source-labeled")],
variants[(profile, slot, "reversed-labeled")],
)
comparisons.append(
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": int(anchor),
"ground_multiset_jaccard": ground_jaccard,
"nonground_multiset_jaccard": nonground_jaccard,
"labeled_multiset_jaccard": labeled_jaccard,
"partition_order_accepted": bool(
ground_jaccard >= threshold and nonground_jaccard >= threshold
),
"aos_order_accepted": bool(labeled_jaccard >= labeled_threshold),
}
)
partition_complete = all(
bool(row["partition_accounted_for_all_eligible_input_points"]) for row in rows
)
partition_order = all(bool(row["partition_order_accepted"]) for row in comparisons)
aos_order = all(bool(row["aos_order_accepted"]) for row in comparisons)
wall = np.asarray([float(row["wall_seconds"]) for row in rows], dtype=np.float64)
rss = np.asarray([int(row["max_rss_kib"]) for row in rows], dtype=np.int64)
result = {
"schema_version": "missioncore.m49-travel-ravnoves-compatibility-result/v1",
"status": "passed",
"source_pack_sha256": manifest["source_pack_sha256"],
"config_sha256": manifest["config_sha256"],
"input_record_count": len(manifest["records"]),
"candidate_run_count": len(rows),
"rows": rows,
"order_comparisons": comparisons,
"summary": {
"partition_complete": partition_complete,
"partition_order_accepted": partition_order,
"aos_order_accepted": aos_order,
"direct_upstream_profile_compatible": bool(
partition_complete and partition_order and aos_order
),
"wall_seconds_p50": float(np.median(wall)),
"wall_seconds_p95": float(np.percentile(wall, 95)),
"max_rss_kib": int(rss.max()),
"minimum_ground_multiset_jaccard": min(
float(row["ground_multiset_jaccard"]) for row in comparisons
),
"minimum_nonground_multiset_jaccard": min(
float(row["nonground_multiset_jaccard"]) for row in comparisons
),
"minimum_labeled_multiset_jaccard": min(
float(row["labeled_multiset_jaccard"]) for row in comparisons
),
},
"authority": {
"ravnoves00_visual_quality_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
}
(probe_root / "result.json").write_text(
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--probe-root", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
arguments = parser.parse_args()
result = analyze(arguments.probe_root, arguments.config)
print(json.dumps(result["summary"], indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Prepare causal RAVNOVES00 inputs for the pinned TRAVEL compatibility probe."""
from __future__ import annotations
import argparse
import hashlib
import json
import math
from pathlib import Path
import numpy as np
SOURCE_PACK_SHA256 = "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
EXPECTED_ARRAYS = {
"cloud_offsets",
"cloud_points_map",
"frame_indices",
"pose_positions_map",
"pose_quaternions_map_from_lidar",
"sample_available",
"session_seconds",
}
class ProbePreparationError(RuntimeError):
"""The immutable source or requested compatibility profile is invalid."""
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 rotation_map_from_lidar(quaternion_xyzw: np.ndarray) -> np.ndarray:
quaternion = np.asarray(quaternion_xyzw, dtype=np.float64)
if quaternion.shape != (4,) or not np.isfinite(quaternion).all():
raise ProbePreparationError("pose quaternion is invalid")
norm = float(np.linalg.norm(quaternion))
if not math.isfinite(norm) or not 0.99 <= norm <= 1.01:
raise ProbePreparationError("pose quaternion is not normalized")
x, y, z, w = quaternion / norm
return np.asarray(
[
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
],
dtype=np.float64,
)
def points_in_current_lidar_frame(
points_map: np.ndarray,
position_map: np.ndarray,
quaternion_map_from_lidar: np.ndarray,
) -> np.ndarray:
points = np.asarray(points_map, dtype=np.float64)
position = np.asarray(position_map, dtype=np.float64)
if (
points.ndim != 2
or points.shape[1:] != (3,)
or position.shape != (3,)
or not np.isfinite(points).all()
or not np.isfinite(position).all()
):
raise ProbePreparationError("point or pose arrays are invalid")
rotation = rotation_map_from_lidar(quaternion_map_from_lidar)
result = (points - position) @ rotation
if not np.isfinite(result).all():
raise ProbePreparationError("sensor-frame points are non-finite")
return result
def xyzi(points_lidar: np.ndarray) -> np.ndarray:
result = np.zeros((points_lidar.shape[0], 4), dtype=np.float32)
result[:, :3] = points_lidar.astype(np.float32)
return result
def _validate_source(arrays: dict[str, np.ndarray]) -> None:
frames = 4489
if (
arrays["cloud_offsets"].shape != (frames + 1,)
or arrays["cloud_offsets"].dtype != np.int64
or arrays["cloud_points_map"].shape != (9_207_270, 3)
or arrays["cloud_points_map"].dtype != np.float32
or arrays["frame_indices"].shape != (frames,)
or arrays["frame_indices"].dtype != np.int64
or arrays["pose_positions_map"].shape != (frames, 3)
or arrays["pose_positions_map"].dtype != np.float64
or arrays["pose_quaternions_map_from_lidar"].shape != (frames, 4)
or arrays["pose_quaternions_map_from_lidar"].dtype != np.float64
or arrays["sample_available"].shape != (frames,)
or arrays["sample_available"].dtype != np.bool_
or arrays["session_seconds"].shape != (frames,)
or arrays["session_seconds"].dtype != np.float64
or int(arrays["cloud_offsets"][0]) != 0
or int(arrays["cloud_offsets"][-1]) != 9_207_270
or np.any(np.diff(arrays["cloud_offsets"]) < 0)
or np.any(np.diff(arrays["frame_indices"]) <= 0)
or np.any(np.diff(arrays["session_seconds"]) < 0)
):
raise ProbePreparationError("RAVNOVES00 lidar-pack array contract changed")
def _frame_points(arrays: dict[str, np.ndarray], frame_index: int) -> np.ndarray:
start = int(arrays["cloud_offsets"][frame_index])
stop = int(arrays["cloud_offsets"][frame_index + 1])
return arrays["cloud_points_map"][start:stop]
def _profile_points(
arrays: dict[str, np.ndarray],
*,
anchor: int,
profile: dict[str, object],
) -> tuple[np.ndarray, tuple[int, ...]]:
seconds = arrays["session_seconds"]
if profile["id"] == "current_increment":
contributors = (anchor,)
elif profile["id"] == "causal_rolling_1s":
history = float(profile["history_seconds"])
start = int(np.searchsorted(seconds, seconds[anchor] - history, side="left"))
contributors = tuple(
index for index in range(start, anchor + 1) if bool(arrays["sample_available"][index])
)
else:
raise ProbePreparationError("unknown input profile")
if not contributors or contributors[-1] != anchor:
raise ProbePreparationError("causal profile does not contain its anchor")
points_map = np.concatenate(
[_frame_points(arrays, frame_index) for frame_index in contributors], axis=0
)
local_radius = profile["local_radius_m"]
if local_radius is not None:
relative_xy = (
points_map[:, :2].astype(np.float64) - arrays["pose_positions_map"][anchor, :2]
)
points_map = points_map[np.linalg.norm(relative_xy, axis=1) <= float(local_radius)]
sensor_points = points_in_current_lidar_frame(
points_map,
arrays["pose_positions_map"][anchor],
arrays["pose_quaternions_map_from_lidar"][anchor],
)
if sensor_points.shape[0] == 0:
raise ProbePreparationError("compatibility profile produced an empty cloud")
return xyzi(sensor_points), contributors
def prepare(source_pack: Path, config_path: Path, output_root: Path) -> dict[str, object]:
if output_root.exists():
raise ProbePreparationError("probe input root already exists")
if sha256_file(source_pack) != SOURCE_PACK_SHA256:
raise ProbePreparationError("RAVNOVES00 lidar-pack digest changed")
config = json.loads(config_path.read_text(encoding="utf-8"))
if (
config.get("schema_version") != "missioncore.m49-travel-ravnoves-compatibility/v1"
or config.get("source", {}).get("source_pack_sha256") != SOURCE_PACK_SHA256
or config.get("order_variants") != ["source", "reversed"]
):
raise ProbePreparationError("compatibility config changed")
with np.load(source_pack, allow_pickle=False) as archive:
if not EXPECTED_ARRAYS.issubset(archive.files):
raise ProbePreparationError("RAVNOVES00 lidar-pack members changed")
arrays = {name: archive[name] for name in EXPECTED_ARRAYS}
_validate_source(arrays)
anchors = tuple(int(value) for value in config["anchors"])
if len(anchors) != 10 or len(set(anchors)) != 10:
raise ProbePreparationError("compatibility anchors changed")
records: list[dict[str, object]] = []
for profile in config["input_profiles"]:
profile_id = str(profile["id"])
for slot, anchor in enumerate(anchors):
if not bool(arrays["sample_available"][anchor]):
raise ProbePreparationError(f"anchor {anchor} has no lidar sample")
native, contributors = _profile_points(arrays, anchor=anchor, profile=profile)
record: dict[str, object] = {
"slot": slot,
"anchor_frame_index": anchor,
"anchor_source_frame_index": int(arrays["frame_indices"][anchor]),
"anchor_session_seconds": float(arrays["session_seconds"][anchor]),
"profile_id": profile_id,
"point_count": int(native.shape[0]),
"contributing_frame_indices": list(contributors),
"contributing_source_frame_indices": [
int(arrays["frame_indices"][value]) for value in contributors
],
"contributing_session_seconds": [
float(arrays["session_seconds"][value]) for value in contributors
],
"variants": {},
}
for variant, points in (("source", native), ("reversed", native[::-1])):
target = (
output_root / "profiles" / profile_id / variant / "velodyne" / f"{slot:06d}.bin"
)
target.parent.mkdir(parents=True, exist_ok=True)
contiguous = np.ascontiguousarray(points, dtype=np.float32)
target.write_bytes(contiguous.tobytes())
record["variants"][variant] = {
"relative_path": target.relative_to(output_root).as_posix(),
"bytes": target.stat().st_size,
"sha256": sha256_file(target),
}
records.append(record)
manifest = {
"schema_version": "missioncore.m49-travel-ravnoves-probe-input/v1",
"source_pack_sha256": SOURCE_PACK_SHA256,
"config_sha256": sha256_file(config_path),
"intensity_policy": "zero-filled-algorithm-compatibility-only",
"coordinate_frame": "current-lidar",
"future_frames_used": False,
"records": records,
}
manifest_path = output_root / "input-manifest.json"
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return manifest
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-pack", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
arguments = parser.parse_args()
manifest = prepare(arguments.source_pack, arguments.config, arguments.output_root)
print(json.dumps({"ok": True, "records": len(manifest["records"])}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
readonly INPUT_ROOT=/probe/inputs
readonly OUTPUT_ROOT=/probe/outputs
readonly LOG_ROOT=/probe/logs
readonly TIMING_PATH=/probe/travel-timing.tsv
readonly TRAVEL_BIN=/opt/travel/core-build/examples/run_travel_kitti
test -f "${INPUT_ROOT}/input-manifest.json"
test ! -e "${OUTPUT_ROOT}"
test ! -e "${LOG_ROOT}"
mkdir -p "${OUTPUT_ROOT}" "${LOG_ROOT}"
printf 'profile\tvariant\tslot\twall_seconds\tmax_rss_kib\n' > "${TIMING_PATH}"
for profile in current_increment causal_rolling_1s; do
for variant in source reversed; do
sequence="${INPUT_ROOT}/profiles/${profile}/${variant}"
output="${OUTPUT_ROOT}/${profile}/${variant}"
mkdir -p "${output}"
for slot in $(seq 0 9); do
log="${LOG_ROOT}/${profile}-${variant}-${slot}.log"
timing="${LOG_ROOT}/${profile}-${variant}-${slot}.time"
/usr/bin/time -f '%e\t%M' -o "${timing}" \
"${TRAVEL_BIN}" "${sequence}" "${slot}" "${output}" > "${log}" 2>&1
read -r wall rss < "${timing}"
printf '%s\t%s\t%s\t%s\t%s\n' \
"${profile}" "${variant}" "${slot}" "${wall}" "${rss}" >> "${TIMING_PATH}"
for suffix in ground nonground labeled; do
test -f "${output}/${slot}_${suffix}.bin"
done
done
done
done
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Build the deterministic M49 T3 TRAVEL RAVNOVES00 probe release."""
from __future__ import annotations
import argparse
import gzip
import hashlib
import io
import json
import re
import subprocess
import tarfile
import tempfile
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
SOURCES = (
Path("experiments/perception/worker/m49_t3_travel/prepare_ravnoves_probe.py"),
Path("experiments/perception/worker/m49_t3_travel/run_ravnoves_probe.sh"),
Path("experiments/perception/worker/m49_t3_travel/analyze_ravnoves_probe.py"),
Path("experiments/perception/worker/Invoke-M49T3TravelRavnovesProbe.ps1"),
Path("experiments/perception/worker/Invoke-M49T3TravelRavnovesProbeAsInteractiveUser.ps1"),
Path("config/perception/m49-travel-ravnoves-compatibility-v1.json"),
)
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
class ArtifactBuildError(RuntimeError):
"""The probe artifact 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() -> 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")
return revision
def tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
info = tarfile.TarInfo(arcname)
info.uid = info.gid = 0
info.uname = info.gname = "root"
info.mtime = 0
if path.is_dir():
info.type = tarfile.DIRTYPE
info.mode = 0o755
else:
info.type = tarfile.REGTYPE
info.mode = 0o755 if path.suffix in {".sh", ".ps1", ".py"} else 0o644
info.size = path.stat().st_size
return info
def write_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, mtime=0) as compressed,
tarfile.open(fileobj=compressed, 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(
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")
sources = tuple(REPOSITORY_ROOT / source for source in SOURCES)
if any(path.is_symlink() or not path.is_file() for path in sources):
raise ArtifactBuildError("release input is not a regular file")
selected_revision = revision or git_revision()
if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None:
raise ArtifactBuildError("artifact revision is invalid")
with tempfile.TemporaryDirectory(prefix="mission-core-m49-t3-ravnoves-") as directory:
stage = Path(directory)
payload = stage / "payload"
payload.mkdir()
files: dict[str, dict[str, object]] = {}
for source in sources:
destination = payload / source.name
destination.write_bytes(source.read_bytes())
files[destination.name] = {
"bytes": destination.stat().st_size,
"sha256": sha256_file(destination),
}
release = {
"schema_version": "missioncore.m49-t3-ravnoves-worker-release/v1",
"patch_id": patch_id,
"code_revision": selected_revision,
"worker_id": "worker-006",
"candidate_id": "travel",
"license": "GPL-3.0-or-later",
"source_pack_sha256": (
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
),
"images": {
"travel": "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
"parity": "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0",
},
"authority": {
"ravnoves00_visual_quality_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
"files": files,
}
release_path = payload / "release.json"
release_path.write_text(
json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
payload_names = sorted((*files, release_path.name))
(stage / "manifest.env").write_text(
f"id={patch_id}\ncomponent=mission-core-worker\ntype=qualification-release\n",
encoding="utf-8",
)
(stage / "files.txt").write_text("\n".join(payload_names) + "\n", encoding="utf-8")
target = output_directory.resolve() / f"nodedc-{patch_id}.tgz"
write_archive(stage, target)
return {
"ok": True,
"artifact": str(target),
"sha256": sha256_file(target),
"patch_id": patch_id,
"code_revision": selected_revision,
"payload_files": payload_names,
}
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(arguments.patch_id, arguments.output_directory)
except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc:
parser.error(str(exc))
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,88 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
import tarfile
from pathlib import Path
import numpy as np
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
PREPARE = _load(
"m49_t3_prepare",
REPOSITORY_ROOT / "experiments/perception/worker/m49_t3_travel/prepare_ravnoves_probe.py",
)
ANALYZE = _load(
"m49_t3_analyze",
REPOSITORY_ROOT / "experiments/perception/worker/m49_t3_travel/analyze_ravnoves_probe.py",
)
BUILDER = _load(
"m49_t3_ravnoves_builder",
REPOSITORY_ROOT / "scripts/build_m49_t3_travel_ravnoves_worker_artifact.py",
)
def _sha256(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def test_map_points_are_transformed_into_current_lidar_frame() -> None:
points = np.asarray([[2.0, 4.0, 6.0], [3.0, 6.0, 9.0]], dtype=np.float32)
result = PREPARE.points_in_current_lidar_frame(
points,
np.asarray([1.0, 2.0, 3.0], dtype=np.float64),
np.asarray([0.0, 0.0, 0.0, 1.0], dtype=np.float64),
)
assert np.array_equal(result, np.asarray([[1.0, 2.0, 3.0], [2.0, 4.0, 6.0]]))
def test_multiset_jaccard_ignores_order_but_not_membership() -> None:
left = np.asarray([[1, 2, 3, 0], [4, 5, 6, 0], [4, 5, 6, 0]], dtype=np.float32)
reversed_points = left[::-1]
changed = left.copy()
changed[0, 0] = 99
assert ANALYZE.multiset_jaccard(left, reversed_points) == 1.0
assert ANALYZE.multiset_jaccard(left, changed) < 1.0
def test_probe_worker_artifact_is_deterministic_and_bounded(tmp_path: Path) -> None:
patch_id = "mission-core-m49-t3-ravnoves-unit-001"
revision = "a" * 40
first = BUILDER.build(patch_id, tmp_path / "first", revision=revision)
second = BUILDER.build(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 = {
member.name: archive.extractfile(member).read() # type: ignore[union-attr]
for member in members
if member.isfile()
}
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"]),
}
release = json.loads(regular["payload/release.json"])
assert release["code_revision"] == revision
assert release["candidate_id"] == "travel"
assert release["authority"] == {
"navigation_or_actuation_allowed": False,
"ravnoves00_visual_quality_accepted": False,
"realtime_accepted": False,
}
assert "--gpus" not in regular["payload/Invoke-M49T3TravelRavnovesProbe.ps1"].decode()