Compare commits
8
Commits
2e20ca7777
...
b0738438d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0738438d4 | ||
|
|
67578f4ce1 | ||
|
|
83ef98e9e5 | ||
|
|
316f0d7f85 | ||
|
|
2e7abbc56b | ||
|
|
b445745d27 | ||
|
|
1cc87e1836 | ||
|
|
aed59030b4 |
@@ -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
|
||||
Reference in New Issue
Block a user