2 Commits
10 changed files with 1238 additions and 8 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
}
}
@@ -308,8 +308,17 @@ The upstream T3 qualification passed on `2026-08-26`: exact-source KITTI
regression was bit-identical to the committed gold result, the standalone
example produced all outputs and the ROS 2 Jazzy node was discoverable. See
[`experiments/perception/M49_T3_TRAVEL_UPSTREAM_QUALIFICATION_2026-08-26.md`](../experiments/perception/M49_T3_TRAVEL_UPSTREAM_QUALIFICATION_2026-08-26.md).
RAVNOVES00 replay remains gated on scan-model compatibility and the GPL runtime
boundary.
The direct RAVNOVES00 compatibility gate then rejected the combined TGS+AOS
profile. TGS ground/non-ground membership was invariant under point-order
reversal, but AOS failed all `20/20` order comparisons with labeled-point
Jaccard as low as `0.469`; its `64 × 4,500`, `-24.8°…+2.0°` KITTI scan model is
not the K1 registered-map-increment contract. The complete evidence is in
[`experiments/perception/M49_T3_TRAVEL_RAVNOVES_COMPATIBILITY_2026-08-26.md`](../experiments/perception/M49_T3_TRAVEL_RAVNOVES_COMPATIBILITY_2026-08-26.md).
Direct AOS admission is closed. TGS alone remains eligible for a bounded
ground/non-ground adapter that retains the exact unreturned complement as
unknown evidence. GPL runtime, visual quality and full-source realtime remain
open gates.
### T4 — Candidate C occupancy/ESDF probe
@@ -328,9 +337,11 @@ boundary.
## Immediate next action
Verify TRAVEL's spinning-scan assumptions against the bounded causal
RAVNOVES00 `lio_pcl` representation, then build the recorded-replay adapter if
the required information exists. Candidate A T2 replay remains blocked by the
failed unmodified T1 gate. No LOW-STEP tuning, new object model, camera resize,
fisheye rectification, manual dataset or parallel heavy Worker job is
authorized by this decision.
Build the TGS-only recorded adapter over the bounded causal RAVNOVES00
representation. Preserve `GROUND_SUPPORT`, `NONGROUND_OCCUPIED`, the exact
`UNKNOWN_REJECTED` complement and `UNOBSERVED` as separate products; do not use
AOS or infer free cells from absent republication. Review the ten anchors in
metric 3D/costmap space before adding diagnostic camera projection. Candidate A
T2 replay remains blocked by the failed unmodified T1 gate. No LOW-STEP tuning,
new object model, camera resize, fisheye rectification, manual dataset or
parallel heavy Worker job is authorized by this decision.
@@ -0,0 +1,148 @@
# M4.9 T3 TRAVEL compatibility on RAVNOVES00 — 2026-08-26
Status: **direct TRAVEL TGS+AOS profile rejected**; TGS retained for a bounded
ground/non-ground adapter probe; visual quality, realtime integration,
navigation and actuation remain disabled
## Decision
The unmodified TRAVEL KITTI profile cannot consume the RAVNOVES00 `lio_pcl`
representation as a complete ground-and-instance provider. Ground segmentation
is deterministic and fast on the tested inputs, but the above-ground object
segmentation changes materially when the exact same points are presented in
reverse order. The fixed KITTI vertical-angle profile also does not describe
the K1 registered-map increments.
Mission Core therefore rejects direct admission of TRAVEL AOS. It retains the
unmodified TGS stage for the next bounded adapter test because its ground and
non-ground partitions were bit-set invariant across all order comparisons. The
adapter must preserve TGS-rejected points as explicit unknown evidence rather
than silently dropping them. No AOS cluster, camera box, free cell or command
receives authority from this run.
## Frozen identity
| Item | Identity |
| --- | --- |
| TRAVEL source | `95dc2fbd66a343efd9060c45a5711b6307a950a4` |
| Mission Core probe source | `28d52972370097d3f3e376e17c9fdb388b0e7ebf` |
| Deterministic Worker artifact | `c726018a62f602ff1f51432fbdf4bba67aa969735f9798a55472b079eff8a0fb` |
| TRAVEL image | `sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f` |
| CPU preparation/analysis image | `sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0` |
| RAVNOVES00 LiDAR pack | `0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944` |
| Compatibility profile | [`config/perception/m49-travel-ravnoves-compatibility-v1.json`](../../config/perception/m49-travel-ravnoves-compatibility-v1.json) |
The release artifact was built from a clean committed revision, copied into a
new Worker release root and verified before extraction. The run used the two
already-qualified images by exact image ID. It performed no image build,
network access, CUDA/Vulkan request or GPU lease.
## Input contract
The probe used the ten operator-reviewed anchors `171`, `306`, `368`, `402`,
`450`, `509`, `525`, `744`, `1122` and `1856` with two causal representations:
1. `current_increment`: the exact registered map increment transformed into
the current LiDAR frame;
2. `causal_rolling_1s`: current and past increments from the preceding one
second, bounded to the accepted `12 m` local-map radius and transformed into
the current LiDAR frame.
No future frame was used. Missing republication was not interpreted as free
space. The source pack has no retained intensity, ring/channel, per-point time
or native ray model; intensity was zero-filled only to satisfy the XYZI input
ABI.
Each representation was run in source order and exact reverse order. This
produced `10 × 2 × 2 = 40` isolated CPU invocations.
## Result
### TGS ground/non-ground stage
| Measurement | Current increment | Causal rolling 1 s |
| --- | ---: | ---: |
| Eligible points over ten source-order anchors | `20,468` | `144,872` |
| Ground + non-ground points returned | `20,337` | `144,871` |
| Unreturned eligible points | `131` | `1` |
| Anchors with complete accounting | `7/10` | `9/10` |
| Ground multiset order Jaccard, minimum | `1.000` | `1.000` |
| Non-ground multiset order Jaccard, minimum | `1.000` | `1.000` |
| TGS stage p50 | `0.269 ms` | `1.775 ms` |
| TGS stage p95 | `0.329 ms` | `4.379 ms` |
| TGS stage maximum | `0.583 ms` | `4.807 ms` |
TGS classification is order-invariant on all twenty source/reverse
comparisons. The missing points are not random process loss. TRAVEL internally
routes below-plane or filtered points into its outlier cloud, while the public
example and ROS wrapper publish only `ground`, `nonground` and AOS `labeled`.
Frame `171` accounts for `129` of the `131` missing current-increment points;
they lie approximately `2.59–5.09 m` from the sensor. A safety adapter must
retain the exact input complement as `UNKNOWN_REJECTED`, never as free.
### AOS above-ground object stage
| Measurement | Result |
| --- | ---: |
| Accepted source/reverse comparisons | `0/20` |
| Labeled-point multiset Jaccard, minimum | `0.469` |
| Labeled-point multiset Jaccard, median | `0.663` |
| Labeled-point multiset Jaccard, maximum | `0.963` |
| Current-increment non-ground points retained as labeled, median | `20.6%` |
| Causal-rolling non-ground points retained as labeled, median | `38.4%` |
This is a representation incompatibility, not a threshold-quality judgment.
AOS projects points into a `64 × 4,500` spherical range matrix and keeps the
first point that reaches an occupied angular bin. A registered-map increment
does not carry an upstream ring order, so reversing an otherwise identical
array changes which points survive projection and changes cluster membership.
The pinned KITTI profile declares a vertical range of `-24.8°…+2.0°`. Only
`26.7%` of current-increment non-ground points and `34.4%` of causal-rolling
non-ground points lie inside that declared range. The pinned implementation's
row lookup also has no valid end-of-range branch for an angle above the maximum.
Changing the angle table and inventing a pseudo-ring layout would create a new
K1-specific algorithm profile; it is not accepted as the ready upstream system
tested here.
### Wrapper and resource envelope
- Forty isolated invocations completed in `6.654335 s`, including container
startup, input preparation and analysis.
- Per-invocation wall p50 was `50 ms`; p95 was `70.5 ms`. These include
process startup and are not the candidate-stage realtime measurement.
- Maximum measured process RSS was `18,140 KiB`.
- Canonical `ndc-mission-core-triton` retained the same container ID and stayed
`running/healthy`.
- The one-shot scheduled task and all three exact candidate container names
were removed. Worker free memory after cleanup was approximately `52.82 GiB`.
- Frigate, GAUSS/PlayCanvas, the GPU allocation and the canonical port `8000`
were not modified.
## Sealed evidence
Worker root:
`D:\NDC_MISSIONCORE\runtime\results\m49-t3-travel-ravnoves\ravnoves-compat-001`
| File | Bytes | SHA-256 |
| --- | ---: | --- |
| `result.json` | `26,810` | `ff0b27f469cccad258c53ae07cb04ad00e696d82b32021c4f1f38ba7fa39d66a` |
| `worker-summary.json` | `1,007` | `007eb400f6a12c950e81a631dc1c7824bc10c0024327fb9965f5689d3b4c6a22` |
| `inputs/input-manifest.json` | `22,967` | `1239f2517c5dfcb46827cfd8f370e642a70eac3a83e39d6b769564321e2ff930` |
| `travel-timing.tsv` | `1,606` | `959b82372dbb197525071f030a4be1d6f3ff848e32b62b4b47b4f2bf2fa9d3d7` |
## Next gate
Build a TGS-only recorded adapter with four explicit outputs:
1. positive `GROUND_SUPPORT` from TGS ground;
2. `NONGROUND_OCCUPIED` from every TGS non-ground point, without AOS;
3. `UNKNOWN_REJECTED` from the exact eligible-input complement;
4. `UNOBSERVED` for cells with no positive support, never free by omission.
The first review remains the ten anchors in a 3D occupancy/costmap view. Camera
projection is diagnostic only and follows after the metric output is sealed.
The adapter is admitted only if compact obstacles and the passages between them
remain explicit without large vegetation/curb walls. GPL distribution and
full-source realtime remain separate gates.
@@ -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()