8 Commits
18 changed files with 1514 additions and 6 deletions
@@ -0,0 +1,59 @@
{
"schema_version": "missioncore.camera-lidar-traversability/v1",
"roles": {
"camera_semantics": "material-and-risk-meaning-primary",
"lidar": "metric-geometry-and-positive-support-primary",
"arbitration": "preserve-contradiction-never-unilateral-clear"
},
"camera_contract": {
"raster": "native-800x600-fisheye",
"rectification_allowed": false,
"new_model_allowed": false,
"vegetation_classes": [
"grass_low_vegetation",
"tree_woody_vegetation"
]
},
"states": [
{
"id": "RIGID_OR_UNKNOWN_OBSTACLE",
"required_evidence": "stable-metric-nonground-without-vegetation-support",
"cost": "lethal-or-unknown"
},
{
"id": "VEGETATION_UNKNOWN",
"required_evidence": "camera-vegetation-without-positive-ground-support",
"cost": "unknown-never-free"
},
{
"id": "VEGETATION_POTENTIALLY_TRAVERSABLE",
"required_evidence": "camera-low-vegetation-plus-positive-ground-without-concentrated-rigid-return",
"cost": "high-cost-terrain"
},
{
"id": "VEGETATION_WITH_RIGID_GEOMETRY",
"required_evidence": "camera-vegetation-plus-concentrated-persistent-vertical-geometry",
"cost": "lethal"
},
{
"id": "SUPPORTED_GROUND",
"required_evidence": "positive-ground-support-without-blocking-geometry",
"cost": "candidate-traversable"
}
],
"invariants": {
"camera_can_clear_positive_rigid_geometry": false,
"lidar_nonground_alone_means_rigid_object": false,
"missing_lidar_support_means_free": false,
"vegetation_is_binary_free_or_lethal": false,
"physical_navigation_authority": false
},
"deferred_robot_profile": [
"footprint",
"ground_clearance",
"traction",
"maximum_slope",
"admissible_vegetation_height",
"vegetation_contact_allowed"
]
}
@@ -15,6 +15,12 @@
"candidate_id": "gseg3d-ground-consistency",
"role": "primary",
"components": [
{
"repository": "https://github.com/dfki-ric/ground_segmentation.git",
"revision": "116f510485ea5b4707e2b476958d34a5a067f647",
"observed_ref": "refs/heads/master",
"license": "BSD-3-Clause"
},
{
"repository": "https://github.com/dfki-ric/ground_segmentation_ros2.git",
"revision": "5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95",
@@ -33,8 +33,13 @@ The sequential evaluation order is:
RF-DETR remains the semantic risk provider for people, children, animals,
cars, trucks, motorcycles and bicycles. It does not decide the presence or
shape of an unknown static obstacle. Native `800x600` fisheye input remains
unchanged.
shape of an unknown static obstacle. The admitted camera semantic layer owns
surface/material hints such as low vegetation and woody vegetation. Native
`800x600` fisheye input remains unchanged.
There is no single globally primary sensor. The camera is primary for semantic
meaning; LiDAR is primary for metric geometry and positive support. Neither is
allowed to erase contradictory evidence from the other.
## Required product representation
@@ -55,12 +60,42 @@ robot-footprint collision cost + traversability
+----> camera projection (diagnostic visualization only)
camera RF-DETR ----> dynamic semantic risk hints ----------------+
camera semantics --> material / vegetation hint -----------------+
```
The canonical safety output is a costmap/elevation/occupancy product. Camera
boxes may be derived for operator review, but a box must never be the source of
occupied cells, clearance, passage width or a control action.
## Camera/LiDAR arbitration and vegetation
Tall grass demonstrates why a binary obstacle mask is insufficient. LiDAR may
correctly report many non-ground returns while being unable to tell whether
they came from compressible grass, a rigid trunk hidden inside the grass, or a
drop-off with no positive support. Camera semantics can identify vegetation,
but a camera label cannot prove that the ground below is load-bearing or free
of a hidden rigid object.
Mission Core therefore preserves at least these distinct states:
| Evidence | Traversability state |
| --- | --- |
| Stable metric non-ground evidence without vegetation support | `RIGID_OR_UNKNOWN_OBSTACLE`; lethal or unknown according to support confidence |
| Camera vegetation plus diffuse LiDAR returns, but no positive ground support below | `VEGETATION_UNKNOWN`; never silently free |
| Camera low vegetation plus positive local ground support and no concentrated rigid return | `VEGETATION_POTENTIALLY_TRAVERSABLE`; high-cost terrain, not a wall |
| Camera vegetation plus concentrated persistent vertical geometry | `VEGETATION_WITH_RIGID_GEOMETRY`; lethal |
| Positive ground support without blocking geometry | `SUPPORTED_GROUND`; candidate traversable terrain |
`VEGETATION_POTENTIALLY_TRAVERSABLE` is a representation decision, not an
authorization to drive through it. The later robot profile must still declare
ground clearance, footprint, traction, admissible slope, vegetation height and
whether contact with vegetation is allowed. Until that profile exists, the
planner may prefer a supported hard path and retain vegetation as a higher-cost
or unknown alternative.
The executable evidence contract is
[`config/perception/m49-camera-lidar-traversability-v1.json`](../config/perception/m49-camera-lidar-traversability-v1.json).
## Candidate A — GSeg3D and Ground Consistency
### GSeg3D
@@ -78,6 +113,11 @@ publishes separate ground and obstacle point clouds and provides:
Mission Core evaluates it as the first replacement for the current local
height connected-component heuristic.
The upstream GSeg3D documentation explicitly lists recall degradation in dense
vegetation as a remaining challenge and semantic-aware refinement as future
work. Candidate A can therefore qualify the metric ground/non-ground seam, but
cannot alone close the vegetation product gate.
### Nav2 Ground Consistency
[DFKI Ground Consistency](https://github.com/dfki-ric/nav2_ground_consistency_costmap_plugin)
@@ -239,6 +279,13 @@ failure.
- Run upstream sample/demo data first to prove the unmodified components.
- Measure component latency and memory before adding Mission Core adapters.
T1 completed on `2026-08-26` with an upstream candidate rejection. The pinned
sources build, but the Ground Consistency suite passes only `11/12` gtests;
`testBoundsReflectFootprint` fails because the test fixture has no configured
footprint. The sealed evidence and complete run ledger are recorded in
[`experiments/perception/M49_T1_GSEG3D_UPSTREAM_QUALIFICATION_2026-08-26.md`](../experiments/perception/M49_T1_GSEG3D_UPSTREAM_QUALIFICATION_2026-08-26.md).
Candidate A receives no replay or runtime authority in this revision set.
### T2 — Candidate A RAVNOVES00 replay
- Feed the bounded causal local cloud with exact pose lineage.
@@ -246,6 +293,8 @@ failure.
- Review the ten mandatory anchors and compute full-run stability/resource
metrics.
- Do not draw system camera boxes until the costmap result is sealed.
- Replay the existing camera semantic vegetation evidence as an independent
arbitration input; do not let it clear unsupported or concentrated geometry.
### T3 — Candidate B challenger
@@ -255,6 +304,13 @@ failure.
- Stop if Candidate A already passes and TRAVEL adds no measurable value that
justifies GPL runtime implications.
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.
### T4 — Candidate C occupancy/ESDF probe
- Run nvblox separately with RF-DETR disabled for the isolated probe.
@@ -272,7 +328,9 @@ failure.
## Immediate next action
Resume T1 for Candidate A only after the Worker resource gate in the T0 report
passes. No LOW-STEP tuning, new object model, camera resize, fisheye
rectification, manual dataset or parallel heavy Worker job is authorized by
this decision.
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.
@@ -61,6 +61,7 @@ This T0 check does not reopen or alter that evidence.
| Order | Candidate | Exact revision | Admission role |
| ---: | --- | --- | --- |
| 1 | DFKI GSeg3D core | `116f510485ea5b4707e2b476958d34a5a067f647` | Pinned core dependency |
| 1 | DFKI GSeg3D | `5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95` | Primary terrain-relative ground/non-ground provider |
| 1 | DFKI Nav2 Ground Consistency | `41cec620efba6c370dccfc59a6ec1134775ff48a` | Primary temporal costmap evidence layer |
| 2 | TRAVEL | `95dc2fbd66a343efd9060c45a5711b6307a950a4` | Class-free challenger |
@@ -86,6 +87,28 @@ Candidate A may start only when all of the following are true:
4. canonical Mission Core Triton is healthy and is not stopped or replaced;
5. the candidate uses a new `ndc-` isolated build/runtime contour.
## Resource incident localized before T1
The apparent continued Worker pressure was not caused by an active GAUSS job.
A PowerShell process from an earlier Mission Core diagnostic had been orphaned
after loading and recursively serializing the complete nested M4.8R3
`result.json`. At discovery it held approximately `74,912.6 MiB` working set
and `193,275.8 MiB` private memory. The pressure killed
`ndc-mission-core-triton` with exit code `137`.
The orphaned diagnostic process was terminated at
`2026-08-26T15:17:39Z`. Free physical memory subsequently recovered to
`83,374,248 KiB`. The existing canonical Triton container was started without
replacement or rebuild and returned to `healthy` at
`2026-08-26T15:18:10Z`.
Future Worker inspection must not pass a complete nested replay result through
PowerShell `ConvertFrom-Json` followed by deep `ConvertTo-Json`. Diagnostics
must select bounded scalar fields before serialization and must have a wall
timeout plus process cleanup. A separate compute-runtime change is required to
give canonical Triton a declared restart policy; this report does not mutate
the Worker Compose source.
## Next command boundary
The next authorized action is T1 Candidate A upstream qualification: build the
@@ -0,0 +1,93 @@
# M4.9 T1 GSeg3D upstream qualification — 2026-08-26
Status: **rejected as an unmodified Candidate A stack**; RAVNOVES00 replay and
all navigation or actuation authority remain disabled
## Decision
The pinned GSeg3D, ROS 2 wrapper and Nav2 Ground Consistency sources build in
an isolated Worker 006 container. The unmodified Ground Consistency test suite
does not pass: `11/12` gtests pass and
`GroundConsistencyLayerTest.testBoundsReflectFootprint` fails.
The failure is inside the pinned upstream candidate rather than the Mission
Core replay adapter. The layer logs that no footprint is configured, skips
footprint clearing and leaves `max_x/max_y` at `0`, while its own test requires
both bounds to be at least `1.0`.
Mission Core therefore does not call this revision set plug-and-play ready,
does not patch upstream silently and does not start Candidate A RAVNOVES00
replay. This result does not prove that the GSeg3D algorithm is unusable. It
proves that the exact unmodified stack frozen for T1 fails its admission gate.
## Frozen identity
| Component | Revision |
| --- | --- |
| DFKI `ground_segmentation` | `116f510485ea5b4707e2b476958d34a5a067f647` |
| DFKI `ground_segmentation_ros2` | `5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95` |
| DFKI `nav2_ground_consistency_costmap_plugin` | `41cec620efba6c370dccfc59a6ec1134775ff48a` |
| Final Mission Core qualification source | `316f0d7f8582f47cdd2a05e7e7404cfe33a40d6d` |
| Final deterministic Worker artifact | `f681ba58aa3c69b7cd3572fda4abc1c4303bf73d62262fb1c534e220f7136801` |
| Worker image | `sha256:57a481faa9ad50d69a5aae537ed24c1770867413b38e91399060c5dd448b3471` |
The final artifact was built twice locally with identical SHA-256, copied to a
new immutable Worker release directory, verified again before extraction and
run through the interactive Worker executor. The image size is
`1,172,862,313` bytes.
## Run ledger
| Run | Outcome | Interpretation |
| --- | --- | --- |
| `upstream-001` | pre-build failure | Non-interactive Docker credential helper; no candidate result |
| `upstream-002` | pre-build failure | Isolated Docker config was still evaluated in the non-interactive SSH context; no candidate result |
| `upstream-003` | harness failure | Image built; strict shell nounset conflicted with the standard ROS 2 setup hook |
| `upstream-004` | harness failure | Test command omitted the merged-install layout used during the image build |
| `upstream-005` | **candidate test failure** | Harness reached the pinned upstream gtests; `11/12` passed and footprint-bound test failed |
The two harness failures were corrected in new content-addressed artifacts and
were never presented as candidate failures. No identical failed artifact was
retried.
## Sealed final evidence
Worker output root:
`D:\NDC_MISSIONCORE\runtime\results\m49-t1-gseg3d\upstream-005`
| File | Bytes | SHA-256 |
| --- | ---: | --- |
| `result.json` | `640` | `6ff189d7c160fe98063672a848ced205199e7bd3ed3de568b1638b7e325848ac` |
| `qualification.log` | `37,813` | `ffafad5cc09ec3d46302111ed0fe9ec6c02d0dbd982bc7307c28b44db071abba` |
| `resource.txt` | `772` | `5955b1eb01a7f587efaeb3f9d547b80e44c1d6651f4b2c9906b47d11e8a5f894` |
The GSeg3D core checkout exposes no CTest tests in the selected build, so its
`ctest` step reports `No tests were found` and returns success. The decisive
executable evidence is the Ground Consistency suite, which does contain twelve
tests and returns one failure.
## Resource and isolation result
- Worker 006 remained the only execution host.
- The candidate build/test used CPU and did not request CUDA, Vulkan or a GPU
lease.
- The concurrent approximately `47–51%` GPU utilization and roughly `9 GiB`
VRAM belonged to `sentinel-frigate`, not GAUSS or this qualification.
- Canonical `ndc-mission-core-triton` remained `running/healthy` after the
final run.
- Temporary candidate containers and the one-shot scheduled task were removed
after evidence capture.
- No Mission Core source evidence, GAUSS process, Frigate process or canonical
Triton image was stopped, rebuilt or replaced.
## Authority boundary and next action
Candidate A has no RAVNOVES00 quality, realtime, navigation, collision-safety
or actuation authority. T2 is blocked for this exact unmodified revision set.
The next sequential action is an isolated upstream qualification of Candidate
B, TRAVEL, at its frozen revision. Its GPL-3.0 license limits it to evaluation
until a separate distribution decision is made. Camera/LiDAR vegetation
arbitration remains an independent product requirement and is not waived by
any terrain candidate.
@@ -0,0 +1,99 @@
# M4.9 T3 TRAVEL upstream qualification — 2026-08-26
Status: **upstream candidate qualified**; RAVNOVES00 quality, realtime,
navigation and actuation remain disabled
## Decision
The frozen TRAVEL revision builds and passes its official C++ regression path
on Worker 006. The run also passes the standalone example smoke and produces a
discoverable ROS 2 Jazzy `travel_node`.
This accepts TRAVEL for the next recorded-replay compatibility gate. It does
not yet accept the RAVNOVES00 `lio_pcl` representation, Mission Core latency,
vegetation handling, obstacle quality, navigation or control.
## Frozen identity
| Item | Identity |
| --- | --- |
| TRAVEL source | `95dc2fbd66a343efd9060c45a5711b6307a950a4` |
| Mission Core qualification source | `67578f4ce16316dbaa562cba466ca9ab820fa380` |
| Deterministic Worker artifact | `01cb4954de748d2e23567734a5dc5043ebfa7d1596313c3ddba792fa94ac3cb7` |
| Worker image | `sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f` |
| Image size | `1,030,088,960` bytes |
| KITTI fixture | `1,994,688` bytes; SHA-256 `bf272996d5b6d25cc5589e1089137cb20a98b63bd4823a7fea5631b359f6d68c` |
| Committed gold result | SHA-256 `8aaacaa57d17a8a2f043c3ea0a0c134d36a3c50da2385f2364ca4eff53ea9b9f` |
The release artifact was built twice with identical SHA-256, copied into a new
Worker release directory and verified again before extraction. The run used
the exact upstream revision and did not modify its source.
## Upstream result
- `124,668` KITTI points loaded.
- Official C++ regression output was bit-identical to the committed gold dump.
- Ground segmentation produced `70,175` ground and `54,462` non-ground points.
- Above-ground clustering received `54,462` points, projected `47,311`,
labeled `45,792` and produced `185` canonical clusters in the regression
representation.
- The standalone example wrote non-empty ground, non-ground and labeled
outputs.
- ROS 2 Jazzy discovered `travel_ros travel_node` from the isolated merged
install.
The example prints `The # of point clouds and # of labels are not same`
because the official smoke fixture intentionally contains a Velodyne frame but
no optional KITTI labels directory. The example returns success and writes all
three required outputs; the official upstream CI uses the same unlabeled smoke
shape. This diagnostic is recorded rather than hidden.
## Timing and memory
| Measurement | Observation |
| --- | ---: |
| Worker image build | `290.528115 s` |
| Worker qualification wrapper | `1.667882 s` |
| Sealed in-container qualification section | `617 ms` |
| C++ bit-regression wall time | `0.04 s` |
| C++ bit-regression maximum RSS | `39,504 KiB` |
| Reported TGS time | `11.168 ms` |
| Reported AOS range-map / clustering / post stages | `3 / 6 / 1 ms` |
These numbers are promising but are not Mission Core realtime acceptance. They
cover one native KITTI scan and include neither RAVNOVES00 causal support
assembly nor pose transforms, camera arbitration, costmap publication or full
graph load.
## Sealed evidence
Worker output root:
`D:\NDC_MISSIONCORE\runtime\results\m49-t3-travel\upstream-001`
| File | Bytes | SHA-256 |
| --- | ---: | --- |
| `result.json` | `674` | `d3fb582b96f907c92156767bf0186fa565897f593bcfd89943ad6a639c0aed38` |
| `worker-summary.json` | `824` | `baedc1b292691be50c5ef70169df1ff43f99f0e122f527a557a599a906a90d28` |
| `qualification.log` | `3,557` | `7dc5b8ecb1cb8ad91fc08edbfcc6ab818e4524e271b0524bb02914cf5f96d933` |
| `resource.txt` | `893` | `dc5638295170cb1edd9c16e2c4ae9b53a89b22b014d27a847c127d6b9afa541c` |
After capture, the candidate container and one-shot scheduled task were absent.
Canonical `ndc-mission-core-triton` remained `running/healthy`. The build and
qualification requested no CUDA, Vulkan or GPU lease; the concurrent Frigate
GPU allocation was not modified.
## Compatibility gate before RAVNOVES00 replay
TRAVEL's accepted KITTI profile assumes an organized spinning-LiDAR geometry
(`64` vertical scans, `4,500` horizontal bins and a fixed vertical angle
range). RAVNOVES00 supplies registered vendor-map increments rather than one
conventional complete raw scan per video frame. The next step must therefore
prove that a bounded causal body-frame representation preserves the angular
and temporal information TRAVEL requires. Missing returns may not be invented,
treated as free, or filled from future frames.
TRAVEL remains GPL-3.0-or-later and is evaluation-only until a separate
distribution decision. Camera semantics remain primary for material meaning;
LiDAR/TRAVEL remains metric evidence and cannot independently declare tall
vegetation traversable.
@@ -0,0 +1,120 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BuildContext,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId,
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m49-t1-gseg3d"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
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 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]
}
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") {
throw "M49 T1 is pinned to Worker 006"
}
$context = Resolve-DDirectory $BuildContext "M49 T1 build context" $false
$output = Resolve-DDirectory $OutputRoot "M49 T1 output root" $true
$runOutput = Join-Path $output $RunId
if (Test-Path -LiteralPath $runOutput) { throw "M49 T1 output already exists" }
$null = New-Item -ItemType Directory -Path $runOutput
$runOutput = Resolve-DDirectory $runOutput "M49 T1 run output" $false
$dockerConfig = Join-Path $runOutput "docker-config"
$null = New-Item -ItemType Directory -Path $dockerConfig
'{"auths":{}}' | Set-Content -LiteralPath (Join-Path $dockerConfig "config.json") -Encoding ascii
$env:DOCKER_CONFIG = $dockerConfig
$os = Get-CimInstance Win32_OperatingSystem
$freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB
if ($freeMemoryGiB -lt 16.0) {
throw ("M49 T1 requires 16 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB)
}
$canonicalTriton = Get-Container "ndc-mission-core-triton"
if (-not $canonicalTriton.State.Running -or $canonicalTriton.State.Health.Status -cne "healthy") {
throw "Canonical Mission Core Triton must remain healthy during M49 T1"
}
$containerName = "ndc-mission-core-m49-t1-gseg3d-$RunId"
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$containerName$") {
throw "M49 T1 container name already exists"
}
$imageTag = "ndc/mission-core-m49-t1-gseg3d:20260826"
$buildStarted = [DateTimeOffset]::UtcNow
& docker build --pull=false --tag $imageTag $context
Assert-LastExitCode "M49 T1 image build"
$buildCompleted = [DateTimeOffset]::UtcNow
$runStarted = [DateTimeOffset]::UtcNow
try {
& docker run --rm --name $containerName --cpus 16 --memory 24g `
--volume ((($runOutput -replace "\\", "/")) + ":/evidence") `
$imageTag
Assert-LastExitCode "M49 T1 upstream qualification"
} finally {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$containerName$") {
& docker rm --force $containerName *> $null
}
}
$runCompleted = [DateTimeOffset]::UtcNow
$image = @((& docker image inspect $imageTag) | ConvertFrom-Json)[0]
Assert-LastExitCode "M49 T1 image inspection"
$resultPath = Join-Path $runOutput "result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "M49 T1 result.json is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if ($result.status -cne "passed") { throw "M49 T1 qualification did not pass" }
$summary = [ordered]@{
schema_version = "missioncore.m49-t1-worker-summary/v1"
worker_id = "worker-006"
run_id = $RunId
image_tag = $imageTag
image_id = [string]$image.Id
image_size_bytes = [long]$image.Size
build_started_utc = $buildStarted.ToString("o")
build_wall_seconds = [math]::Round(($buildCompleted - $buildStarted).TotalSeconds, 6)
qualification_wall_seconds = [math]::Round(($runCompleted - $runStarted).TotalSeconds, 6)
free_memory_gib_before = [math]::Round($freeMemoryGiB, 6)
canonical_triton_id = [string]$canonicalTriton.Id
canonical_triton_health = [string]$canonicalTriton.State.Health.Status
candidate_accepted = $true
ravnoves00_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,62 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId
)
$ErrorActionPreference = "Stop"
$taskName = "MissionCore-M49T1GSeg3D"
$release = (Resolve-Path -LiteralPath $ReleaseRoot).Path
$payload = Join-Path $release "payload"
$runner = Join-Path $payload "Invoke-M49T1GSeg3DQualification.ps1"
if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
throw "M49 T1 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`"",
"-BuildContext", "`"$payload`"",
"-RunId", "`"$RunId`""
) -join " "
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction `
-Execute $powerShell `
-Argument $arguments `
-WorkingDirectory $payload
$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(3))
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Principal $principal `
-Trigger $trigger `
-Settings $settings `
-Description "One-shot M49 T1 pinned GSeg3D upstream qualification." `
-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,120 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BuildContext,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId,
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m49-t3-travel"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
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 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]
}
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") {
throw "M49 T3 is pinned to Worker 006"
}
$context = Resolve-DDirectory $BuildContext "M49 T3 build context" $false
$output = Resolve-DDirectory $OutputRoot "M49 T3 output root" $true
$runOutput = Join-Path $output $RunId
if (Test-Path -LiteralPath $runOutput) { throw "M49 T3 output already exists" }
$null = New-Item -ItemType Directory -Path $runOutput
$runOutput = Resolve-DDirectory $runOutput "M49 T3 run output" $false
$dockerConfig = Join-Path $runOutput "docker-config"
$null = New-Item -ItemType Directory -Path $dockerConfig
'{"auths":{}}' | Set-Content -LiteralPath (Join-Path $dockerConfig "config.json") -Encoding ascii
$env:DOCKER_CONFIG = $dockerConfig
$os = Get-CimInstance Win32_OperatingSystem
$freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB
if ($freeMemoryGiB -lt 16.0) {
throw ("M49 T3 requires 16 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB)
}
$canonicalTriton = Get-Container "ndc-mission-core-triton"
if (-not $canonicalTriton.State.Running -or $canonicalTriton.State.Health.Status -cne "healthy") {
throw "Canonical Mission Core Triton must remain healthy during M49 T3"
}
$containerName = "ndc-mission-core-m49-t3-travel-$RunId"
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$containerName$") {
throw "M49 T3 container name already exists"
}
$imageTag = "ndc/mission-core-m49-t3-travel:20260826"
$buildStarted = [DateTimeOffset]::UtcNow
& docker build --pull=false --tag $imageTag $context
Assert-LastExitCode "M49 T3 image build"
$buildCompleted = [DateTimeOffset]::UtcNow
$runStarted = [DateTimeOffset]::UtcNow
try {
& docker run --rm --name $containerName --cpus 16 --memory 24g `
--volume ((($runOutput -replace "\\", "/")) + ":/evidence") `
$imageTag
Assert-LastExitCode "M49 T3 upstream qualification"
} finally {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$containerName$") {
& docker rm --force $containerName *> $null
}
}
$runCompleted = [DateTimeOffset]::UtcNow
$image = @((& docker image inspect $imageTag) | ConvertFrom-Json)[0]
Assert-LastExitCode "M49 T3 image inspection"
$resultPath = Join-Path $runOutput "result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "M49 T3 result.json is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if ($result.status -cne "passed") { throw "M49 T3 qualification did not pass" }
$summary = [ordered]@{
schema_version = "missioncore.m49-t3-worker-summary/v1"
worker_id = "worker-006"
run_id = $RunId
image_tag = $imageTag
image_id = [string]$image.Id
image_size_bytes = [long]$image.Size
build_started_utc = $buildStarted.ToString("o")
build_wall_seconds = [math]::Round(($buildCompleted - $buildStarted).TotalSeconds, 6)
qualification_wall_seconds = [math]::Round(($runCompleted - $runStarted).TotalSeconds, 6)
free_memory_gib_before = [math]::Round($freeMemoryGiB, 6)
canonical_triton_id = [string]$canonicalTriton.Id
canonical_triton_health = [string]$canonicalTriton.State.Health.Status
candidate_accepted = $true
ravnoves00_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,62 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId
)
$ErrorActionPreference = "Stop"
$taskName = "MissionCore-M49T3Travel"
$release = (Resolve-Path -LiteralPath $ReleaseRoot).Path
$payload = Join-Path $release "payload"
$runner = Join-Path $payload "Invoke-M49T3TravelQualification.ps1"
if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
throw "M49 T3 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`"",
"-BuildContext", "`"$payload`"",
"-RunId", "`"$RunId`""
) -join " "
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction `
-Execute $powerShell `
-Argument $arguments `
-WorkingDirectory $payload
$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(3))
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Principal $principal `
-Trigger $trigger `
-Settings $settings `
-Description "One-shot M49 T3 pinned TRAVEL upstream qualification." `
-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,65 @@
FROM ros:jazzy-ros-base@sha256:2589a8fba5257307857890173c069852c2abf913a0be7970f172478baecb09e4
ARG GSEG3D_CORE_REVISION=116f510485ea5b4707e2b476958d34a5a067f647
ARG GSEG3D_ROS2_REVISION=5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95
ARG GROUND_CONSISTENCY_REVISION=41cec620efba6c370dccfc59a6ec1134775ff48a
ARG BUILD_JOBS=12
ENV DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
build-essential \
cmake \
git \
jq \
libeigen3-dev \
libgtest-dev \
libnanoflann-dev \
libpcl-dev \
ninja-build \
ros-jazzy-message-filters \
ros-jazzy-nav2-costmap-2d \
ros-jazzy-pcl-conversions \
ros-jazzy-rclcpp-lifecycle \
ros-jazzy-tf2-eigen \
ros-jazzy-tf2-geometry-msgs \
time \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/m49/src
RUN git clone --filter=blob:none https://github.com/dfki-ric/ground_segmentation.git \
&& git -C ground_segmentation checkout --detach "${GSEG3D_CORE_REVISION}" \
&& git clone --filter=blob:none https://github.com/dfki-ric/ground_segmentation_ros2.git \
&& git -C ground_segmentation_ros2 checkout --detach "${GSEG3D_ROS2_REVISION}" \
&& git clone --filter=blob:none https://github.com/dfki-ric/nav2_ground_consistency_costmap_plugin.git \
&& git -C nav2_ground_consistency_costmap_plugin checkout --detach "${GROUND_CONSISTENCY_REVISION}"
RUN cmake -S /opt/m49/src/ground_segmentation \
-B /opt/m49/core-build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/opt/m49/core-install \
&& cmake --build /opt/m49/core-build --parallel "${BUILD_JOBS}" \
&& ctest --test-dir /opt/m49/core-build --output-on-failure \
&& cmake --install /opt/m49/core-build
RUN source /opt/ros/jazzy/setup.bash \
&& export CMAKE_PREFIX_PATH="/opt/m49/core-install:${CMAKE_PREFIX_PATH:-}" \
&& colcon build \
--base-paths \
/opt/m49/src/ground_segmentation_ros2 \
/opt/m49/src/nav2_ground_consistency_costmap_plugin \
--build-base /opt/m49/ros-build \
--install-base /opt/m49/ros-install \
--merge-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON \
--executor sequential \
--event-handlers console_direct+
COPY qualify.sh /usr/local/bin/m49-gseg3d-qualify
RUN chmod 0755 /usr/local/bin/m49-gseg3d-qualify
ENTRYPOINT ["/usr/local/bin/m49-gseg3d-qualify"]
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
readonly EVIDENCE_ROOT=/evidence
readonly RESULT_PATH="${EVIDENCE_ROOT}/result.json"
readonly LOG_PATH="${EVIDENCE_ROOT}/qualification.log"
readonly RESOURCE_PATH="${EVIDENCE_ROOT}/resource.txt"
readonly STARTED_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
readonly STARTED_NS="$(date +%s%N)"
mkdir -p "${EVIDENCE_ROOT}"
if [[ -e "${RESULT_PATH}" || -e "${LOG_PATH}" || -e "${RESOURCE_PATH}" ]]; then
echo "evidence output already exists" >&2
exit 2
fi
status=failed
failure_stage=bootstrap
finalize() {
local exit_code=$?
local completed_ns
local elapsed_ms
completed_ns="$(date +%s%N)"
elapsed_ms="$(( (completed_ns - STARTED_NS) / 1000000 ))"
python3 - "${RESULT_PATH}" "${status}" "${failure_stage}" "${exit_code}" \
"${STARTED_UTC}" "${elapsed_ms}" <<'PY'
import json
import pathlib
import sys
target, status, stage, exit_code, started_utc, elapsed_ms = sys.argv[1:]
document = {
"schema_version": "missioncore.m49-t1-gseg3d-upstream-qualification/v1",
"status": status,
"failure_stage": None if status == "passed" else stage,
"exit_code": int(exit_code),
"started_utc": started_utc,
"elapsed_ms": int(elapsed_ms),
"authority": {
"candidate_build_qualified": status == "passed",
"ravnoves00_quality_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
"revisions": {
"ground_segmentation": "116f510485ea5b4707e2b476958d34a5a067f647",
"ground_segmentation_ros2": "5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95",
"ground_consistency": "41cec620efba6c370dccfc59a6ec1134775ff48a",
},
}
pathlib.Path(target).write_text(
json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
PY
}
trap finalize EXIT
exec > >(tee "${LOG_PATH}") 2>&1
failure_stage=source-integrity
test "$(git -C /opt/m49/src/ground_segmentation rev-parse HEAD)" = \
"116f510485ea5b4707e2b476958d34a5a067f647"
test "$(git -C /opt/m49/src/ground_segmentation_ros2 rev-parse HEAD)" = \
"5c5ba6f5ca0d682db2d59b2ad2f6a317ee938b95"
test "$(git -C /opt/m49/src/nav2_ground_consistency_costmap_plugin rev-parse HEAD)" = \
"41cec620efba6c370dccfc59a6ec1134775ff48a"
test -z "$(git -C /opt/m49/src/ground_segmentation status --porcelain)"
test -z "$(git -C /opt/m49/src/ground_segmentation_ros2 status --porcelain)"
test -z "$(git -C /opt/m49/src/nav2_ground_consistency_costmap_plugin status --porcelain)"
failure_stage=core-upstream-tests
/usr/bin/time -v -o "${RESOURCE_PATH}" \
ctest --test-dir /opt/m49/core-build --output-on-failure
failure_stage=ros2-plugin-tests
# ROS 2 environment hooks legitimately probe unset variables. Keep strict mode
# for the qualification itself, but do not let nounset turn a standard setup
# hook into a false candidate failure.
set +u
source /opt/ros/jazzy/setup.bash
source /opt/m49/ros-install/setup.bash
set -u
colcon test \
--build-base /opt/m49/ros-build \
--install-base /opt/m49/ros-install \
--test-result-base /opt/m49/test-results \
--merge-install \
--packages-select nav2_ground_consistency_costmap_plugin \
--event-handlers console_direct+
colcon test-result --test-result-base /opt/m49/test-results --verbose
failure_stage=ros2-discovery
ros2 pkg prefix ground_segmentation_ros2
ros2 pkg prefix nav2_ground_consistency_costmap_plugin
ros2 pkg executables ground_segmentation_ros2 | \
grep -F "ground_segmentation_ros2_node"
failure_stage=complete
status=passed
@@ -0,0 +1,72 @@
FROM ros:jazzy-ros-base@sha256:2589a8fba5257307857890173c069852c2abf913a0be7970f172478baecb09e4
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG TRAVEL_REVISION=95dc2fbd66a343efd9060c45a5711b6307a950a4
ARG KITTI_FIXTURE_SHA256=bf272996d5b6d25cc5589e1089137cb20a98b63bd4823a7fea5631b359f6d68c
ARG KITTI_GOLD_SHA256=8aaacaa57d17a8a2f043c3ea0a0c134d36a3c50da2385f2364ca4eff53ea9b9f
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
curl \
git \
libboost-filesystem-dev \
libboost-system-dev \
libeigen3-dev \
libpcl-dev \
mpi-default-dev \
python3-colcon-common-extensions \
ros-jazzy-pcl-conversions \
ros-jazzy-rclcpp \
ros-jazzy-sensor-msgs \
ros-jazzy-std-msgs \
time \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /opt/travel/src/TRAVEL \
&& git -C /opt/travel/src/TRAVEL init \
&& git -C /opt/travel/src/TRAVEL remote add origin https://github.com/url-kaist/TRAVEL.git \
&& git -C /opt/travel/src/TRAVEL fetch --depth 1 origin "${TRAVEL_REVISION}" \
&& git -C /opt/travel/src/TRAVEL checkout --detach FETCH_HEAD \
&& test "$(git -C /opt/travel/src/TRAVEL rev-parse HEAD)" = "${TRAVEL_REVISION}" \
&& test -z "$(git -C /opt/travel/src/TRAVEL status --porcelain)"
RUN mkdir -p /opt/travel/fixture/00/velodyne \
&& curl -L --fail --retry 3 \
-o /opt/travel/fixture/00/velodyne/000000.bin \
https://github.com/url-kaist/TRAVEL/releases/download/test-data-v1/kitti00_000000.bin \
&& test "$(stat -c%s /opt/travel/fixture/00/velodyne/000000.bin)" = "1994688" \
&& echo "${KITTI_FIXTURE_SHA256} /opt/travel/fixture/00/velodyne/000000.bin" | sha256sum -c - \
&& echo "${KITTI_GOLD_SHA256} /opt/travel/src/TRAVEL/cpp/tests/data/kitti00_000000_gold.bin" | sha256sum -c -
RUN cmake \
-S /opt/travel/src/TRAVEL/cpp/travel \
-B /opt/travel/core-build \
-DTRAVEL_BUILD_EXAMPLES=ON \
-DTRAVEL_BUILD_TESTS=ON \
-DCMAKE_BUILD_TYPE=Release \
&& cmake --build /opt/travel/core-build \
--target regression_kitti run_travel_kitti \
--parallel 16
RUN set +u \
&& source /opt/ros/jazzy/setup.bash \
&& set -u \
&& colcon build \
--base-paths /opt/travel/src/TRAVEL \
--build-base /opt/travel/ros-build \
--install-base /opt/travel/ros-install \
--merge-install \
--packages-select travel_ros \
--cmake-args -DCMAKE_BUILD_TYPE=Release \
&& test -x /opt/travel/ros-install/lib/travel_ros/travel_node
COPY qualify.sh /usr/local/bin/m49-t3-travel-qualify
RUN chmod 0755 /usr/local/bin/m49-t3-travel-qualify
ENTRYPOINT ["/usr/local/bin/m49-t3-travel-qualify"]
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
readonly EVIDENCE_ROOT=/evidence
readonly RESULT_PATH="${EVIDENCE_ROOT}/result.json"
readonly LOG_PATH="${EVIDENCE_ROOT}/qualification.log"
readonly RESOURCE_PATH="${EVIDENCE_ROOT}/resource.txt"
readonly STARTED_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
readonly STARTED_NS="$(date +%s%N)"
mkdir -p "${EVIDENCE_ROOT}"
if [[ -e "${RESULT_PATH}" || -e "${LOG_PATH}" || -e "${RESOURCE_PATH}" ]]; then
echo "evidence output already exists" >&2
exit 2
fi
status=failed
failure_stage=bootstrap
finalize() {
local exit_code=$?
local completed_ns
local elapsed_ms
completed_ns="$(date +%s%N)"
elapsed_ms="$(( (completed_ns - STARTED_NS) / 1000000 ))"
python3 - "${RESULT_PATH}" "${status}" "${failure_stage}" "${exit_code}" \
"${STARTED_UTC}" "${elapsed_ms}" <<'PY'
import json
import pathlib
import sys
target, status, stage, exit_code, started_utc, elapsed_ms = sys.argv[1:]
document = {
"schema_version": "missioncore.m49-t3-travel-upstream-qualification/v1",
"status": status,
"failure_stage": None if status == "passed" else stage,
"exit_code": int(exit_code),
"started_utc": started_utc,
"elapsed_ms": int(elapsed_ms),
"authority": {
"candidate_build_qualified": status == "passed",
"ravnoves00_quality_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
"revisions": {
"travel": "95dc2fbd66a343efd9060c45a5711b6307a950a4",
},
"fixture": {
"bytes": 1994688,
"sha256": "bf272996d5b6d25cc5589e1089137cb20a98b63bd4823a7fea5631b359f6d68c",
"gold_sha256": "8aaacaa57d17a8a2f043c3ea0a0c134d36a3c50da2385f2364ca4eff53ea9b9f",
},
}
pathlib.Path(target).write_text(
json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
PY
}
trap finalize EXIT
exec > >(tee "${LOG_PATH}") 2>&1
failure_stage=source-integrity
test "$(git -C /opt/travel/src/TRAVEL rev-parse HEAD)" = \
"95dc2fbd66a343efd9060c45a5711b6307a950a4"
test -z "$(git -C /opt/travel/src/TRAVEL status --porcelain)"
test "$(stat -c%s /opt/travel/fixture/00/velodyne/000000.bin)" = "1994688"
echo "bf272996d5b6d25cc5589e1089137cb20a98b63bd4823a7fea5631b359f6d68c /opt/travel/fixture/00/velodyne/000000.bin" | sha256sum -c -
echo "8aaacaa57d17a8a2f043c3ea0a0c134d36a3c50da2385f2364ca4eff53ea9b9f /opt/travel/src/TRAVEL/cpp/tests/data/kitti00_000000_gold.bin" | sha256sum -c -
failure_stage=cpp-kitti-regression
/usr/bin/time -v -o "${RESOURCE_PATH}" \
/opt/travel/core-build/tests/regression_kitti \
/opt/travel/fixture/00/velodyne/000000.bin \
/tmp/travel-run-dump.bin \
/opt/travel/src/TRAVEL/cpp/tests/data/kitti00_000000_gold.bin
failure_stage=cpp-example-smoke
mkdir -p /tmp/travel-example
/opt/travel/core-build/examples/run_travel_kitti \
/opt/travel/fixture/00 0 /tmp/travel-example
for output in \
/tmp/travel-example/0_ground.bin \
/tmp/travel-example/0_nonground.bin \
/tmp/travel-example/0_labeled.bin; do
test -s "${output}"
done
failure_stage=ros2-discovery
set +u
source /opt/ros/jazzy/setup.bash
source /opt/travel/ros-install/setup.bash
set -u
test -x /opt/travel/ros-install/lib/travel_ros/travel_node
test "$(ros2 pkg prefix travel_ros)" = "/opt/travel/ros-install"
ros2 pkg executables travel_ros | grep -F "travel_ros travel_node"
failure_stage=complete
status=passed
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Build the deterministic M49 T1 GSeg3D Worker 006 qualification 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_t1_gseg3d/Dockerfile"),
Path("experiments/perception/worker/m49_t1_gseg3d/qualify.sh"),
Path("experiments/perception/worker/Invoke-M49T1GSeg3DQualification.ps1"),
Path(
"experiments/perception/worker/"
"Invoke-M49T1GSeg3DQualificationAsInteractiveUser.ps1"
),
Path("config/perception/m49-traversability-candidate-manifest-v1.json"),
Path("config/perception/m49-camera-lidar-traversability-v1.json"),
)
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
class ArtifactBuildError(RuntimeError):
"""The qualification 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"} 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-t1-") 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-t1-worker-release/v1",
"patch_id": patch_id,
"code_revision": selected_revision,
"worker_id": "worker-006",
"candidate_id": "gseg3d-ground-consistency",
"authority": {
"ravnoves00_quality_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,172 @@
#!/usr/bin/env python3
"""Build the deterministic M49 T3 TRAVEL Worker 006 qualification 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/Dockerfile"),
Path("experiments/perception/worker/m49_t3_travel/qualify.sh"),
Path("experiments/perception/worker/Invoke-M49T3TravelQualification.ps1"),
Path(
"experiments/perception/worker/"
"Invoke-M49T3TravelQualificationAsInteractiveUser.ps1"
),
Path("config/perception/m49-traversability-candidate-manifest-v1.json"),
Path("config/perception/m49-camera-lidar-traversability-v1.json"),
)
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
class ArtifactBuildError(RuntimeError):
"""The qualification 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"} 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-") 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-worker-release/v1",
"patch_id": patch_id,
"code_revision": selected_revision,
"worker_id": "worker-006",
"candidate_id": "travel",
"license": "GPL-3.0-or-later",
"authority": {
"ravnoves00_quality_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,65 @@
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_m49_t1_gseg3d_worker_artifact.py"
SPEC = importlib.util.spec_from_file_location("m49_t1_gseg3d_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_m49_t1_worker_artifact_is_deterministic_and_bounded(tmp_path: Path) -> None:
patch_id = "mission-core-m49-t1-gseg3d-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 = _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"]
assert regular["manifest.env"].decode().splitlines() == [
f"id={patch_id}",
"component=mission-core-worker",
"type=qualification-release",
]
release = json.loads(regular["payload/release.json"])
assert release["code_revision"] == revision
assert release["candidate_id"] == "gseg3d-ground-consistency"
assert release["authority"] == {
"navigation_or_actuation_allowed": False,
"ravnoves00_quality_accepted": False,
}
serialized = json.dumps(release).lower()
assert "password=" not in serialized
assert "private key" not in serialized
@@ -0,0 +1,61 @@
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_m49_t3_travel_worker_artifact.py"
SPEC = importlib.util.spec_from_file_location("m49_t3_travel_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_m49_t3_worker_artifact_is_deterministic_and_bounded(tmp_path: Path) -> None:
patch_id = "mission-core-m49-t3-travel-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 = _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["candidate_id"] == "travel"
assert release["license"] == "GPL-3.0-or-later"
assert release["authority"] == {
"navigation_or_actuation_allowed": False,
"ravnoves00_quality_accepted": False,
}
serialized = json.dumps(release).lower()
assert "password=" not in serialized
assert "private key" not in serialized