feat(perception): qualify inline temporal stability
This commit is contained in:
parent
cfc7b062da
commit
23181c867b
|
|
@ -13,11 +13,10 @@
|
|||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.spatial-toolbar__mode,
|
||||
.spatial-toolbar__actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
|
|
@ -39,39 +38,6 @@
|
|||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.spatial-toolbar__perception-status,
|
||||
.spatial-toolbar__perception-retry {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.07);
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 0.035);
|
||||
padding: 0.42rem 0.62rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font: inherit;
|
||||
font-size: 0.58rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spatial-toolbar__perception-status .busy-indicator {
|
||||
width: 0.72rem;
|
||||
height: 0.72rem;
|
||||
flex-basis: 0.72rem;
|
||||
}
|
||||
|
||||
.spatial-toolbar__perception-retry {
|
||||
color: var(--nodedc-text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spatial-toolbar__perception-retry:hover,
|
||||
.spatial-toolbar__perception-retry:focus-visible {
|
||||
background: rgb(255 255 255 / 0.09);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.spatial-viewport-shell {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
|
@ -369,6 +335,58 @@
|
|||
.scene-selection small { color: var(--nodedc-text-muted); font-size: 0.55rem; }
|
||||
.scene-selection strong { overflow: hidden; font-size: 0.66rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.scene-operation-status-stack {
|
||||
position: absolute;
|
||||
z-index: 11;
|
||||
right: 0.85rem;
|
||||
bottom: 7.15rem;
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.scene-operation-status {
|
||||
display: inline-flex;
|
||||
max-width: min(24rem, calc(100% - 1.7rem));
|
||||
align-items: center;
|
||||
gap: 0.42rem;
|
||||
border: 1px solid rgb(255 255 255 / 0.07);
|
||||
border-radius: 999px;
|
||||
background: rgb(9 10 13 / 0.7);
|
||||
color: var(--nodedc-text-muted);
|
||||
padding: 0.42rem 0.65rem;
|
||||
font: inherit;
|
||||
font-size: 0.52rem;
|
||||
font-weight: 680;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.scene-operation-status .busy-indicator {
|
||||
width: 0.66rem;
|
||||
height: 0.66rem;
|
||||
flex-basis: 0.66rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.scene-operation-status--action {
|
||||
color: var(--nodedc-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scene-operation-status--action:hover,
|
||||
.scene-operation-status--action:focus-visible {
|
||||
border-color: rgb(255 255 255 / 0.14);
|
||||
background: rgb(20 21 25 / 0.82);
|
||||
color: var(--nodedc-text-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.scene-operation-status--error {
|
||||
color: rgb(var(--nodedc-danger-rgb));
|
||||
}
|
||||
|
||||
.scene-timeline {
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -536,9 +536,6 @@ function SpatialWorkspace({
|
|||
data-focused={pointCloudFocused || floatingSourceMaximized ? "true" : undefined}
|
||||
>
|
||||
<div className="spatial-toolbar" data-viewer-controls="v1">
|
||||
<div className="spatial-toolbar__mode">
|
||||
<span className="section-eyebrow">СЦЕНА 3D · RERUN</span>
|
||||
</div>
|
||||
<div className="spatial-toolbar__actions">
|
||||
{recordedPerceptionSupported || livePerceptionAvailable ? (
|
||||
<div className="spatial-toolbar__view-switch" role="group" aria-label="Слои распознавания сцены">
|
||||
|
|
@ -598,33 +595,6 @@ function SpatialWorkspace({
|
|||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && perceptionLoad.phase === "loading" ? (
|
||||
<div className="spatial-toolbar__perception-status" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>{perceptionLoad.message || "Готовим AI-слои."}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && perceptionLoad.phase === "error" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="spatial-toolbar__perception-retry"
|
||||
onClick={() => setPerceptionRetryGeneration((generation) => generation + 1)}
|
||||
>
|
||||
<Icon name="refresh" size={14} />
|
||||
Повторить AI
|
||||
</button>
|
||||
) : null}
|
||||
{recordedSource && pointColorLoad.phase === "loading" ? (
|
||||
<div className="spatial-toolbar__perception-status" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && pointColorLoad.phase === "error" ? (
|
||||
<div className="spatial-toolbar__perception-status" role="status">
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{recordedSource && presentedViewerStatus === "ready" ? (
|
||||
<Button
|
||||
size="compact"
|
||||
|
|
@ -804,6 +774,43 @@ function SpatialWorkspace({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{!floatingSourceMaximized && recordedSource && (
|
||||
perceptionLoad.phase === "loading" ||
|
||||
perceptionLoad.phase === "error" ||
|
||||
pointColorLoad.phase === "loading" ||
|
||||
pointColorLoad.phase === "error"
|
||||
) ? (
|
||||
<div className="scene-operation-status-stack">
|
||||
{perceptionLoad.phase === "loading" ? (
|
||||
<div className="scene-operation-status" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>{perceptionLoad.message || "Готовим AI-слои."}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{perceptionLoad.phase === "error" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="scene-operation-status scene-operation-status--action"
|
||||
onClick={() => setPerceptionRetryGeneration((generation) => generation + 1)}
|
||||
>
|
||||
<Icon name="refresh" size={12} />
|
||||
<span>Повторить AI</span>
|
||||
</button>
|
||||
) : null}
|
||||
{pointColorLoad.phase === "loading" ? (
|
||||
<div className="scene-operation-status" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{pointColorLoad.phase === "error" ? (
|
||||
<div className="scene-operation-status scene-operation-status--error" role="status">
|
||||
<span>{pointColorLoad.message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{presentedViewerStatus === "ready" && !floatingSourceMaximized ? (
|
||||
<div className="scene-navigation-hint" aria-label="Навигация по 3D-сцене">
|
||||
<span>Колесо · зум к курсору</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,276 @@
|
|||
# LAB E23 — inline temporal stability at 1×
|
||||
|
||||
Date: 2026-07-24
|
||||
|
||||
Final accepted run: **LAB E23.2**
|
||||
|
||||
Status: **accepted for bounded shadow real-time qualification**
|
||||
|
||||
Control authority: **disabled**
|
||||
|
||||
## Executive result
|
||||
|
||||
LAB E23 answers one narrow but important question: can the temporal stabilization used to reduce 2D, semantic, and 3D jitter execute inside the warm perception worker while a recorded sensor stream is paced at real time?
|
||||
|
||||
The answer is **yes for this 60-second urban RAVNOVES00 slice on the RTX 4090 worker**.
|
||||
|
||||
- The source replay ran at `1.0×`, without look-ahead: 59.994 s of source time in 60.241 s of wall time.
|
||||
- The stabilizer ran inline after fusion and before world/result publication.
|
||||
- Its P95 overhead was 1.721 ms for 2D/3D and 12.307 ms for a semantic frame.
|
||||
- Detector throughput was 9.894 FPS for an approximately 10 FPS camera stream.
|
||||
- Semantic throughput was 2.009 FPS by design: one semantic inference for every five camera frames.
|
||||
- World-state age P95 was 185.375 ms.
|
||||
- All runtime, bounded-state, memory, authority, and final quality checks passed.
|
||||
- The result was published as a new immutable saved session, not written over an earlier LAB.
|
||||
|
||||
This is not yet permission to drive a vehicle. It is a shadow diagnostic qualification on a recorded stream. The physical K1 live ingress, hardware-clock synchronization, forest-domain models, and navigation/safety acceptance remain separate gates.
|
||||
|
||||
## Runs and disposition
|
||||
|
||||
### LAB E23.1
|
||||
|
||||
- Worker result: `e15-shadow-inference-37381767fb06928951aa173c3e2b43a81b8e492916e4ac8622d7d78d7a475f8e`
|
||||
- Profile SHA-256: `a22de018626e11a2c4d18d87d6b682846921f72c6cd9f290f20db9a71218e1e3`
|
||||
- Runtime envelope: passed.
|
||||
- Final raw-versus-inline quality audit: failed.
|
||||
- Failure: 3D cuboid center-step P95 reduction was 17.10%, below the required 20%.
|
||||
- Publication: rejected; no saved LAB instance was created.
|
||||
|
||||
This distinction is intentional. Passing timing and memory gates does not allow a run to be called successful when the requested quality effect was not demonstrated.
|
||||
|
||||
### LAB E23.2
|
||||
|
||||
- Worker result: `e15-shadow-inference-32a95297664241f7102b31e064ab0c168bbd97d4f5cc152892fd378d58617ffb`
|
||||
- Profile SHA-256: `86238c369aa2ec0f5de50a2c676faf9b11884e1355f8cccf416125b5f73fd38e`
|
||||
- Published result: `e10-integrated-perception-f3cefd9464c8b0ed19bb0c8949c1d890b55bf57353850ba155c4dc8d7d90c371`
|
||||
- Saved session: `lab-e23-2-86238c36`
|
||||
- Display label: `LAB E23.2 · Inline temporal 1× · synchronized 60s · 86238c36`
|
||||
- Runtime envelope: passed.
|
||||
- Final raw-versus-inline quality audit: passed.
|
||||
- Browser playback and all visual layers: verified.
|
||||
|
||||
The only configuration adjustment after E23.1 was bounded and measured. A deterministic sweep over the raw E23.1 fusion artifact evaluated several 3D center coefficients; `0.32` was selected to clear the quality gate while avoiding an unnecessarily slow response to real rig motion. E23.2 was then rerun from the source at 1×. The sweep result itself was not treated as acceptance evidence.
|
||||
|
||||
## Experiment boundary
|
||||
|
||||
### What this run proves
|
||||
|
||||
1. The full temporal stage can execute inside the persistent warm worker.
|
||||
2. The stage uses only current and bounded past state; there is no future-frame look-ahead.
|
||||
3. The camera, detector, semantic, LiDAR fusion, 2D tracking, 3D cuboids, and world-state publication can operate together inside the measured 1× envelope.
|
||||
4. State and telemetry collections stay explicitly bounded.
|
||||
5. Inline output materially reduces the selected jitter metrics.
|
||||
6. The exact accepted result can be replayed in Mission Core as a live 60-second session with synchronized camera, 2D, segmentation, point cloud, and 3D cuboids.
|
||||
|
||||
### What this run does not prove
|
||||
|
||||
1. It does not prove a physical K1 live connection is stable for an extended mission.
|
||||
2. It does not prove cross-device hardware timestamp accuracy. The replay is paced by recorded host-arrival time.
|
||||
3. It does not validate COCO/Cityscapes models for forest obstacles or safety decisions.
|
||||
4. It does not grant command, navigation, collision-avoidance, or safety authority.
|
||||
5. It does not establish performance on the final onboard computer unless that computer is the measured RTX 4090 worker.
|
||||
6. It does not establish ten-hour memory stability; this run is 60 seconds.
|
||||
|
||||
## Input data
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Source session | `20260720T065719Z_viewer_live` |
|
||||
| Source mode | recorded source paced near-live |
|
||||
| Speed | `1.0×` |
|
||||
| Look-ahead | disabled |
|
||||
| Selected source span | 59.994249 s |
|
||||
| Wall time | 60.240711 s |
|
||||
| Camera | `sensor.camera.right` |
|
||||
| Calibration slot | `camera_1` |
|
||||
| Resolution | 800 × 600 |
|
||||
| Camera frames | 601 |
|
||||
| LiDAR events | 585 |
|
||||
| Pose events | 600 |
|
||||
| Camera index SHA-256 | `e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14` |
|
||||
| Raw MQTT SHA-256 | `70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af` |
|
||||
| Calibration SHA-256 | `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9` |
|
||||
| Valid-FOV pixels | 270,606 / 480,000 |
|
||||
| Valid-FOV mask SHA-256 | `a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63` |
|
||||
|
||||
Payload loading was streaming and bounded to one event per modality at the source adapter. No complete video, LiDAR stream, or MQTT capture was accumulated in RAM.
|
||||
|
||||
## Software and model identity
|
||||
|
||||
Pipeline: `warm-worker-inline-bounded-temporal-2d-3d-semantic/v1`
|
||||
|
||||
| Component | Identity |
|
||||
|---|---|
|
||||
| Worker package | `e15-worker-package-dbf55ccb75664b778a2e0f5d34284af10ec0971945bbbee67b27bc264b765b51` |
|
||||
| Worker package manifest SHA-256 | `d3089e241286b871ff4e9db569df10222bf2feb07500e10eb641ecce88ace868` |
|
||||
| Projection pack | `e15-live-projection-713fa7344805b7a592d12e949a6db38116e5370f768bde1cf9144fab5f93f0ae` |
|
||||
| Detector | YOLOX-S, COCO-80, ONNX |
|
||||
| Detector model SHA-256 | `c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063` |
|
||||
| Semantic model | `tue-mps/cityscapes_semantic_eomt_large_1024` |
|
||||
| Semantic revision | `8d6b6d1a3f7b50d441afd7d247c2ed10db186e8f` |
|
||||
| Semantic weights SHA-256 | `c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782` |
|
||||
| Semantic precision | FP16 autocast |
|
||||
| Python | 3.12.3 |
|
||||
| PyTorch | 2.13.0+cu130 |
|
||||
| Transformers | 4.57.6 |
|
||||
| NumPy | 1.26.4 |
|
||||
| SciPy | 1.16.3 |
|
||||
| Triton image | `nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794` |
|
||||
|
||||
Profile identities:
|
||||
|
||||
- detector: `819bdfb0da521d187ec9ce3d3039a0ff75daa0ab473fb3d8e5fa92db9c1ddca1`
|
||||
- live: `8b7f267ee947932bdd52d1ef4b241adc8c9b4c4d5a8a3cd6c1e7f6e066983699`
|
||||
- 3D/fusion E14: `a010e32070459b123423f3d4f57aa1d839f73ed05f3df8580d12693406a1b218`
|
||||
- semantic: `ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875`
|
||||
- inline temporal E23.2: `86238c369aa2ec0f5de50a2c676faf9b11884e1355f8cccf416125b5f73fd38e`
|
||||
|
||||
## Processing topology
|
||||
|
||||
The warm worker processed independent bounded branches:
|
||||
|
||||
1. Camera decode and valid-FOV application.
|
||||
2. Detector branch at the camera rate, with a latest-wins queue of capacity 2.
|
||||
3. Semantic branch every fifth camera frame, with a latest-wins queue of capacity 1.
|
||||
4. Bounded LiDAR and pose synchronizer, capacity 32 per modality and 3-second retention.
|
||||
5. Camera/LiDAR projection and object association.
|
||||
6. Raw 2D/3D fusion artifact emission for qualification only.
|
||||
7. Inline temporal stabilization of 2D tracks and 3D cuboids.
|
||||
8. Inline streaming semantic hysteresis with one previous output mask.
|
||||
9. Stabilized fusion/world-state publication to the diagnostic result queue.
|
||||
|
||||
Detector and semantic inference therefore ran concurrently. They were not executed as one long sequential offline job. The temporal stage sat directly in the publication path and its measured latency is included in the result.
|
||||
|
||||
## Bounded-state contract
|
||||
|
||||
| Bound | Configured | Observed |
|
||||
|---|---:|---:|
|
||||
| 2D/3D track states | 128 | 19 peak |
|
||||
| Track idle lifetime | 1.25 s | bounded by eviction |
|
||||
| Semantic history | 1 mask | 1 mask |
|
||||
| Runtime diagnostic samples | 4096 | 61 used |
|
||||
| Detector queue | 2 | depth 2 peak |
|
||||
| Semantic queue | 1 | depth 1 peak |
|
||||
| Synchronizer per modality | 32 | depth 32 peak |
|
||||
|
||||
This is the key architectural difference from accumulating an entire mission in Rerun or in the AI worker. Runtime memory is bounded by live state, queue capacity, model state, and fixed diagnostic rings—not by mission duration.
|
||||
|
||||
## Throughput and freshness
|
||||
|
||||
| Metric | Result |
|
||||
|---|---:|
|
||||
| Detector effective rate | 9.8939 FPS |
|
||||
| Detector camera frames published / consumed | 601 / 596 |
|
||||
| Detector latest-wins replacements | 5 (0.83%) |
|
||||
| Semantic effective rate | 2.0087 FPS |
|
||||
| Semantic frames published / consumed | 121 / 121 |
|
||||
| Semantic queue drops | 0 |
|
||||
| Semantic fresh coverage | 98.8255% |
|
||||
| Fused frames | 544 |
|
||||
| Fused fraction | 91.2752% |
|
||||
| Raw accepted cuboids | 867 |
|
||||
| Stabilized/held cuboids | 1323 |
|
||||
|
||||
The five detector replacements are represented as explicit empty frames in the visual projection: 12, 240, 433, 496, and 499. They are not silently copied from an older inference.
|
||||
|
||||
## Latency
|
||||
|
||||
| Stage | Mean | P95 | Max |
|
||||
|---|---:|---:|---:|
|
||||
| Detector | 36.330 ms | 55.757 ms | 171.674 ms |
|
||||
| Semantic full processing | 205.385 ms | 247.682 ms | 481.552 ms |
|
||||
| Semantic completion age | 216.966 ms | 265.524 ms | 535.985 ms |
|
||||
| Projection | 0.756 ms | 1.187 ms | 17.499 ms |
|
||||
| Association | 4.383 ms | 12.736 ms | 25.382 ms |
|
||||
| Inline temporal 2D/3D | 0.911 ms | 1.721 ms | 11.348 ms |
|
||||
| Inline temporal semantic | 3.616 ms | 12.307 ms | 31.396 ms |
|
||||
| World-state construction | 1.063 ms | 2.022 ms | 12.583 ms |
|
||||
| World-state age | 72.670 ms | 185.375 ms | 366.839 ms |
|
||||
|
||||
Semantic inference is deliberately slower than the camera interval and is reused between sampled frames under a freshness TTL. It is not claimed to run at 10 FPS.
|
||||
|
||||
## Resource use
|
||||
|
||||
Measured worker: NVIDIA GeForce RTX 4090.
|
||||
|
||||
| Metric | Result |
|
||||
|---|---:|
|
||||
| Mean process CPU | 141.75% (~1.42 logical cores) |
|
||||
| Mean process RSS | 1547.11 MiB |
|
||||
| RSS growth over run | 12.14 MiB |
|
||||
| Maximum worker threads | 134 |
|
||||
| CUDA peak allocated | 2107.93 MiB |
|
||||
| CUDA peak reserved | 2840.00 MiB |
|
||||
| Mean device GPU memory used | 13,544.25 MiB |
|
||||
| Mean GPU utilization | 60.18% |
|
||||
| GPU utilization P95 | 82% |
|
||||
| Mean GPU power | 192.93 W |
|
||||
| Maximum GPU temperature | 45 °C |
|
||||
| D: free before | 407,605,469,184 bytes |
|
||||
| D: free after | 407,577,206,784 bytes |
|
||||
| Enforced D: free floor | 386,547,056,640 bytes (360 GiB) |
|
||||
|
||||
The device-wide GPU-memory figure includes the persistent Triton/model-serving footprint and must not be interpreted as the incremental cost of the temporal filter. The temporal filter itself added bounded CPU/NumPy state and the process RSS grew by only 12.14 MiB during the run.
|
||||
|
||||
No experiment payload was intentionally placed on C:. Worker packages, runner roots, and result artifacts remained on D:.
|
||||
|
||||
## Quality result
|
||||
|
||||
Raw and stabilized values were computed from artifacts produced during the same E23.2 worker run. The stabilized result was not recomputed offline for acceptance.
|
||||
|
||||
| Metric | Raw P95 | Inline P95 | Reduction |
|
||||
|---|---:|---:|---:|
|
||||
| 2D normalized acceleration | 0.525774 | 0.395351 | 24.81% |
|
||||
| 2D normalized size step | 0.267858 | 0.138933 | 48.13% |
|
||||
| 3D center step | 0.893014 m | 0.591626 m | 33.75% |
|
||||
| 3D half-size step | 0.060727 m | 0.016893 m | 72.18% |
|
||||
| 3D yaw step | 18.0226° | 4.9804° | 72.37% |
|
||||
| Unsupported semantic change | — | — | 78.05% reduction |
|
||||
|
||||
Required minimums were 20% for 2D acceleration, 3D center, and 3D yaw, and 10% for unsupported semantic change. All passed.
|
||||
|
||||
The result also reduced 2D tracks of at most two frames from 39 to 4 and reduced 3D cuboid gap count from 179 to 69. These are diagnostic continuity metrics, not object-truth accuracy metrics.
|
||||
|
||||
## Publication and visual verification
|
||||
|
||||
The accepted projection contains:
|
||||
|
||||
- 601 synchronized visual frames;
|
||||
- 121 exact stabilized semantic masks;
|
||||
- 1323 accepted visual cuboids;
|
||||
- the source point cloud and trajectory;
|
||||
- five explicit detector-replacement frames;
|
||||
- no copied raw sensor payloads.
|
||||
|
||||
Semantic pixels were reconstructed from the immutable reference only after every one of the 121 inline mask SHA-256 values matched. The published session was then verified in the running Mission Core interface on port 8000:
|
||||
|
||||
1. the saved-session card appeared as `LAB E23.2`;
|
||||
2. preparation transitioned from the lower-right status pill to ready;
|
||||
3. the camera and point-cloud timeline advanced;
|
||||
4. 2D boxes, segmentation, and 3D cuboids could be enabled together;
|
||||
5. all three layers changed with playback;
|
||||
6. the source session and earlier LAB instances remained present.
|
||||
|
||||
The UI cleanup delivered with this experiment removes the redundant `СЦЕНА 3D · RERUN` label and moves AI preparation state from the toolbar into the lower-right navigation/status stack.
|
||||
|
||||
## Acceptance decision
|
||||
|
||||
LAB E23.2 is accepted as evidence that bounded temporal stabilization can be part of the near-real-time perception publication path on the measured RTX 4090 worker.
|
||||
|
||||
It remains diagnostic-only:
|
||||
|
||||
- `commands_enabled = false`
|
||||
- `navigation_or_safety_accepted = false`
|
||||
|
||||
## Next gate
|
||||
|
||||
The next experiment should stop refining the recorded view in isolation and move one boundary closer to the vehicle:
|
||||
|
||||
1. use the same hashed worker package and E23.2 profile against physical K1 live ingress;
|
||||
2. retain raw and stabilized diagnostic artifacts for a bounded comparison window;
|
||||
3. run at least a 10-minute soak before any longer mission test;
|
||||
4. measure live packet loss, camera/LiDAR clock skew, world-state age, queue replacements, RSS slope, GPU temperature, and disk growth;
|
||||
5. keep commands and navigation authority disabled;
|
||||
6. if the 10-minute gate passes, extend to a 60-minute soak without changing the configuration.
|
||||
|
||||
Model-domain replacement and safety validation are separate workstreams. They should not be mixed into the first physical live-ingress qualification because that would make timing, synchronization, and model-quality failures impossible to distinguish.
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"schema_version": "missioncore.e23-inline-temporal-profile/v1",
|
||||
"profile_id": "lab-e23-warm-worker-inline-temporal-v1",
|
||||
"mode": "inline-shadow-qualification",
|
||||
"stage": "warm-worker-after-fusion-before-result-publication",
|
||||
"source": {
|
||||
"source_id": "sensor.camera.right",
|
||||
"resolution": [
|
||||
800,
|
||||
600
|
||||
],
|
||||
"calibration_slot": "camera_1",
|
||||
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
},
|
||||
"tracking_2d": {
|
||||
"center_alpha_low": 0.32,
|
||||
"center_alpha_high": 0.78,
|
||||
"size_alpha_low": 0.22,
|
||||
"size_alpha_high": 0.65,
|
||||
"velocity_alpha": 0.35,
|
||||
"adaptive_innovation_low": 0.08,
|
||||
"adaptive_innovation_high": 0.3,
|
||||
"maximum_normalized_innovation": 1.2,
|
||||
"hold_frames": 2,
|
||||
"stitch_gap_frames": 8,
|
||||
"stitch_minimum_iou": 0.42,
|
||||
"stitch_maximum_normalized_center_distance": 0.22
|
||||
},
|
||||
"cuboids_3d": {
|
||||
"center_alpha": 0.32,
|
||||
"size_alpha": 0.18,
|
||||
"yaw_alpha": 0.18,
|
||||
"velocity_alpha": 0.25,
|
||||
"maximum_center_innovation_m": 2.0,
|
||||
"maximum_yaw_innovation_degrees": 55.0,
|
||||
"hold_seconds": 0.35
|
||||
},
|
||||
"semantic": {
|
||||
"mode": "spatially-supported-streaming-hysteresis-v1",
|
||||
"minimum_same_label_neighbors": 3,
|
||||
"class_count": 16
|
||||
},
|
||||
"bounds": {
|
||||
"maximum_track_states": 128,
|
||||
"maximum_track_idle_seconds": 1.25,
|
||||
"semantic_history_masks": 1
|
||||
},
|
||||
"acceptance": {
|
||||
"minimum_2d_acceleration_p95_reduction_fraction": 0.2,
|
||||
"minimum_3d_center_step_p95_reduction_fraction": 0.2,
|
||||
"minimum_3d_yaw_step_p95_reduction_fraction": 0.2,
|
||||
"minimum_semantic_unsupported_change_reduction_fraction": 0.1,
|
||||
"maximum_camera_frame_processing_p95_ms": 5.0,
|
||||
"maximum_semantic_frame_processing_p95_ms": 30.0,
|
||||
"maximum_track_states_observed": 128,
|
||||
"maximum_rss_growth_mib": 64.0
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"schema_version": "missioncore.e23-inline-temporal-qualification/v1",
|
||||
"experiment": {
|
||||
"lab_id": "LAB E23.2",
|
||||
"created_at_utc": "2026-07-24T05:40:18.949Z",
|
||||
"state": "accepted",
|
||||
"mode": "recorded-source-paced-near-live",
|
||||
"speed": 1.0,
|
||||
"look_ahead": false,
|
||||
"authority": {
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"session_id": "20260720T065719Z_viewer_live",
|
||||
"worker_session_id": "e23-ravnoves00-1784871558075385000",
|
||||
"camera_source": "sensor.camera.right",
|
||||
"camera_calibration_slot": "camera_1",
|
||||
"camera_resolution": [
|
||||
800,
|
||||
600
|
||||
],
|
||||
"duration_seconds": 60.0,
|
||||
"selected_span_seconds": 59.994249001,
|
||||
"wall_seconds": 60.2407113750005,
|
||||
"camera_frames": 601,
|
||||
"lidar_events": 585,
|
||||
"pose_events": 600,
|
||||
"camera_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14",
|
||||
"mqtt_raw_sha256": "70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af",
|
||||
"pacing_clock": "recorded-host-arrival"
|
||||
},
|
||||
"identity": {
|
||||
"pipeline": "warm-worker-inline-bounded-temporal-2d-3d-semantic/v1",
|
||||
"worker_result_id": "e15-shadow-inference-32a95297664241f7102b31e064ab0c168bbd97d4f5cc152892fd378d58617ffb",
|
||||
"published_result_id": "e10-integrated-perception-f3cefd9464c8b0ed19bb0c8949c1d890b55bf57353850ba155c4dc8d7d90c371",
|
||||
"published_session_id": "lab-e23-2-86238c36",
|
||||
"reference_result_id": "e10-integrated-perception-5cbba0b9839ae843c05607cec8448a2b43464eb81edeb11cf7c01cfec8464c75",
|
||||
"profile_sha256": "86238c369aa2ec0f5de50a2c676faf9b11884e1355f8cccf416125b5f73fd38e",
|
||||
"worker_package_id": "e15-worker-package-dbf55ccb75664b778a2e0f5d34284af10ec0971945bbbee67b27bc264b765b51",
|
||||
"worker_package_manifest_sha256": "d3089e241286b871ff4e9db569df10222bf2feb07500e10eb641ecce88ace868",
|
||||
"projection_pack_id": "e15-live-projection-713fa7344805b7a592d12e949a6db38116e5370f768bde1cf9144fab5f93f0ae",
|
||||
"valid_fov_mask_sha256": "a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63",
|
||||
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||
},
|
||||
"runtime": {
|
||||
"gpu": "NVIDIA GeForce RTX 4090",
|
||||
"python": "3.12.3",
|
||||
"torch": "2.13.0+cu130",
|
||||
"transformers": "4.57.6",
|
||||
"numpy": "1.26.4",
|
||||
"scipy": "1.16.3",
|
||||
"triton_image": "nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794",
|
||||
"detector": {
|
||||
"model": "YOLOX-S",
|
||||
"classes": "COCO-80",
|
||||
"effective_fps": 9.893899244013618,
|
||||
"frames_published": 601,
|
||||
"frames_consumed": 596,
|
||||
"latest_wins_replacements": 5
|
||||
},
|
||||
"semantic": {
|
||||
"model": "tue-mps/cityscapes_semantic_eomt_large_1024",
|
||||
"precision": "fp16-autocast",
|
||||
"effective_fps": 2.0086607525598117,
|
||||
"frames_published": 121,
|
||||
"frames_consumed": 121,
|
||||
"latest_wins_replacements": 0,
|
||||
"fresh_coverage_fraction": 0.988255033557047
|
||||
},
|
||||
"fusion": {
|
||||
"fused_frames": 544,
|
||||
"fused_fraction": 0.912751677852349,
|
||||
"raw_accepted_cuboids": 867,
|
||||
"stabilized_cuboids": 1323
|
||||
},
|
||||
"latency_ms": {
|
||||
"detector_p95": 55.757249,
|
||||
"semantic_completion_age_p95": 265.523974,
|
||||
"world_state_age_p95": 185.374918,
|
||||
"inline_temporal_2d_3d_p95": 1.720854,
|
||||
"inline_temporal_semantic_p95": 12.306933
|
||||
},
|
||||
"resource": {
|
||||
"process_cpu_percent_mean": 141.749058,
|
||||
"process_rss_mib_mean": 1547.110848,
|
||||
"process_rss_growth_mib": 12.144531,
|
||||
"process_threads_max": 134,
|
||||
"cuda_peak_allocated_mib": 2107.92529296875,
|
||||
"cuda_peak_reserved_mib": 2840.0,
|
||||
"device_gpu_memory_used_mib_mean": 13544.245902,
|
||||
"gpu_utilization_percent_mean": 60.180328,
|
||||
"gpu_utilization_percent_p95": 82.0,
|
||||
"gpu_power_w_mean": 192.926721,
|
||||
"gpu_temperature_c_max": 45.0,
|
||||
"disk_free_bytes_before": 407605469184,
|
||||
"disk_free_bytes_after": 407577206784,
|
||||
"disk_free_floor_bytes": 386547056640
|
||||
}
|
||||
},
|
||||
"bounded_state": {
|
||||
"maximum_track_states": 128,
|
||||
"peak_track_states": 19,
|
||||
"semantic_history_masks": 1,
|
||||
"diagnostic_sample_capacity": 4096,
|
||||
"maximum_track_idle_seconds": 1.25
|
||||
},
|
||||
"quality": {
|
||||
"tracking_2d_acceleration_p95": {
|
||||
"raw": 0.5257735155179932,
|
||||
"stabilized": 0.395351460036327,
|
||||
"reduction_fraction": 0.2480574841301661
|
||||
},
|
||||
"tracking_2d_size_step_p95": {
|
||||
"raw": 0.26785786216659935,
|
||||
"stabilized": 0.13893264173009742,
|
||||
"reduction_fraction": 0.4813195304168985
|
||||
},
|
||||
"cuboid_center_step_p95_m": {
|
||||
"raw": 0.8930137938715039,
|
||||
"stabilized": 0.5916256441083151,
|
||||
"reduction_fraction": 0.3374955144383309
|
||||
},
|
||||
"cuboid_half_size_step_p95_m": {
|
||||
"raw": 0.06072732822316884,
|
||||
"stabilized": 0.016892938813881653,
|
||||
"reduction_fraction": 0.7218231180564829
|
||||
},
|
||||
"cuboid_yaw_step_p95_degrees": {
|
||||
"raw": 18.02256860299971,
|
||||
"stabilized": 4.980429289278814,
|
||||
"reduction_fraction": 0.7236559671938293
|
||||
},
|
||||
"semantic_unsupported_change_reduction_fraction": 0.7804878048780487
|
||||
},
|
||||
"publication": {
|
||||
"visual_frames": 601,
|
||||
"semantic_masks": 121,
|
||||
"detector_replacement_frames": [
|
||||
12,
|
||||
240,
|
||||
433,
|
||||
496,
|
||||
499
|
||||
],
|
||||
"accepted_visual_cuboids": 1323,
|
||||
"source_payloads_mutated": false,
|
||||
"semantic_reconstruction": "exact-inline-sha256-match",
|
||||
"browser_verified": true
|
||||
},
|
||||
"predecessor": {
|
||||
"lab_id": "LAB E23.1",
|
||||
"worker_result_id": "e15-shadow-inference-37381767fb06928951aa173c3e2b43a81b8e492916e4ac8622d7d78d7a475f8e",
|
||||
"profile_sha256": "a22de018626e11a2c4d18d87d6b682846921f72c6cd9f290f20db9a71218e1e3",
|
||||
"runtime_gate": "accepted",
|
||||
"final_quality_gate": "rejected",
|
||||
"reason": "cuboid center-step P95 reduction was 0.1710, below the required 0.20",
|
||||
"published": false
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ from typing import Any
|
|||
SCHEMA = "missioncore.e15-worker-package/v1"
|
||||
COPIED_FILES = (
|
||||
"k1link/__init__.py",
|
||||
"k1link/compute/inline_temporal.py",
|
||||
"k1link/compute/live_perception.py",
|
||||
"k1link/data_plane/__init__.py",
|
||||
"k1link/data_plane/views.py",
|
||||
|
|
@ -68,6 +69,8 @@ def _identity(source_root: Path) -> dict[str, Any]:
|
|||
for path, content in sorted(GENERATED_FILES.items())
|
||||
},
|
||||
"imports": [
|
||||
"k1link.compute.inline_temporal.TemporalStabilizer",
|
||||
"k1link.compute.inline_temporal.StreamingSemanticStabilizer",
|
||||
"k1link.compute.live_perception.LiveSensorSynchronizer",
|
||||
"k1link.data_plane.DecodedPointCloudView",
|
||||
"k1link.data_plane.DecodedPoseView",
|
||||
|
|
@ -85,9 +88,7 @@ def validate_worker_package(root: Path, *, allow_staging: bool = False) -> dict[
|
|||
artifacts = manifest.get("artifacts")
|
||||
expected_paths = {*COPIED_FILES, *GENERATED_FILES, "manifest.json"}
|
||||
actual_paths = {
|
||||
path.relative_to(resolved).as_posix()
|
||||
for path in resolved.rglob("*")
|
||||
if path.is_file()
|
||||
path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file()
|
||||
}
|
||||
if (
|
||||
manifest.get("schema_version") != SCHEMA
|
||||
|
|
|
|||
|
|
@ -169,7 +169,11 @@ def _wait_for(predicate: object, *, timeout_seconds: float, label: str) -> None:
|
|||
def run(args: argparse.Namespace) -> dict[str, object]:
|
||||
experiment_id = str(getattr(args, "experiment_id", _EXPERIMENT_ID))
|
||||
speed = float(getattr(args, "speed", 1.0))
|
||||
if experiment_id not in {"e12", "e21"} or not math.isfinite(speed) or not 0.1 <= speed <= 10:
|
||||
if (
|
||||
experiment_id not in {"e12", "e21", "e23"}
|
||||
or not math.isfinite(speed)
|
||||
or not 0.1 <= speed <= 10
|
||||
):
|
||||
raise RuntimeError("replay experiment identity or speed is invalid")
|
||||
repository_root = Path(args.repository_root).expanduser().resolve()
|
||||
session_root = Path(args.session_root).expanduser().resolve()
|
||||
|
|
@ -292,11 +296,11 @@ def run(args: argparse.Namespace) -> dict[str, object]:
|
|||
raise RuntimeError("selected replay interval is empty")
|
||||
source_span_seconds = max(0.0, (source_last_ns - source_start_ns) / 1_000_000_000)
|
||||
wall_seconds = completed_monotonic - started_monotonic
|
||||
report_schema = (
|
||||
REPORT_SCHEMA
|
||||
if experiment_id == _EXPERIMENT_ID
|
||||
else "missioncore.e21-replay-source-report/v1"
|
||||
)
|
||||
report_schema = {
|
||||
"e12": REPORT_SCHEMA,
|
||||
"e21": "missioncore.e21-replay-source-report/v1",
|
||||
"e23": "missioncore.e23-replay-source-report/v1",
|
||||
}[experiment_id]
|
||||
peak_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
peak_rss_mib = peak_rss / (1024 * 1024 if sys.platform == "darwin" else 1024)
|
||||
report: dict[str, object] = {
|
||||
|
|
@ -375,7 +379,7 @@ def _arguments() -> argparse.Namespace:
|
|||
parser.add_argument("--port", type=int, default=8012)
|
||||
parser.add_argument("--duration-seconds", type=float, default=15.0)
|
||||
parser.add_argument("--speed", type=float, default=1.0)
|
||||
parser.add_argument("--experiment-id", choices=("e12", "e21"), default="e12")
|
||||
parser.add_argument("--experiment-id", choices=("e12", "e21", "e23"), default="e12")
|
||||
parser.add_argument("--consumer-timeout-seconds", type=float, default=30.0)
|
||||
parser.add_argument("--completion-timeout-seconds", type=float, default=30.0)
|
||||
return parser.parse_args()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ param(
|
|||
[Parameter(Mandatory = $true)] [string]$ValidFovRoot,
|
||||
[Parameter(Mandatory = $true)] [string]$ProjectionPackRoot,
|
||||
[Parameter(Mandatory = $true)] [string]$PackageRoot,
|
||||
[string]$StabilityProfilePath = "",
|
||||
[switch]$PreflightOnly,
|
||||
[switch]$PersistentService,
|
||||
[switch]$TokenStdin,
|
||||
|
|
@ -84,6 +85,10 @@ $liveProfile = Resolve-DFile $LiveProfilePath "LAB E15 live profile"
|
|||
$e14Profile = Resolve-DFile $E14ProfilePath "Accepted E14 profile"
|
||||
$detectorProfile = Resolve-DFile $DetectorProfilePath "Detector profile"
|
||||
$semanticProfile = Resolve-DFile $SemanticProfilePath "Semantic profile"
|
||||
$stabilityProfile = $null
|
||||
if ($StabilityProfilePath) {
|
||||
$stabilityProfile = Resolve-DFile $StabilityProfilePath "LAB E23 stability profile"
|
||||
}
|
||||
$validFov = Resolve-DDirectory $ValidFovRoot "Valid-FOV root"
|
||||
$projectionPack = Resolve-DDirectory $ProjectionPackRoot "E15 projection pack"
|
||||
$package = Resolve-DDirectory $PackageRoot "Mission Core package root"
|
||||
|
|
@ -97,6 +102,9 @@ foreach ($path in @($liveProfile, $e14Profile, $detectorProfile, $semanticProfil
|
|||
throw "Runner and profiles must share one immutable mount"
|
||||
}
|
||||
}
|
||||
if ($stabilityProfile -and (Split-Path $stabilityProfile -Parent) -ne $runnerRoot) {
|
||||
throw "Runner and LAB E23 stability profile must share one immutable mount"
|
||||
}
|
||||
foreach ($dependency in @(
|
||||
"e10_fusion_runtime.py",
|
||||
"e15_shadow_runtime.py",
|
||||
|
|
@ -115,6 +123,9 @@ foreach ($dependency in @(
|
|||
if (-not (Test-Path -LiteralPath (Join-Path $package "k1link\compute\live_perception.py") -PathType Leaf)) {
|
||||
throw "Mission Core package mount lacks live perception synchronization"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $package "k1link\compute\inline_temporal.py") -PathType Leaf)) {
|
||||
throw "Mission Core package mount lacks inline temporal state"
|
||||
}
|
||||
|
||||
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
|
||||
if (
|
||||
|
|
@ -130,6 +141,17 @@ if (
|
|||
[bool]$live.authority.navigation_or_safety_accepted -or
|
||||
$live.transport.pyav_version -ne "18.0.0"
|
||||
) { throw "LAB E15 replay-shadow authority contract changed" }
|
||||
if ($stabilityProfile) {
|
||||
$stability = Get-Content -LiteralPath $stabilityProfile -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$stability.schema_version -ne "missioncore.e23-inline-temporal-profile/v1" -or
|
||||
$stability.mode -ne "inline-shadow-qualification" -or
|
||||
$stability.stage -ne "warm-worker-after-fusion-before-result-publication" -or
|
||||
$stability.source.calibration_sha256 -ne $live.source.calibration_sha256 -or
|
||||
[bool]$stability.authority.commands_enabled -or
|
||||
[bool]$stability.authority.navigation_or_safety_accepted
|
||||
) { throw "LAB E23 inline temporal authority contract changed" }
|
||||
}
|
||||
$projection = Get-Content -LiteralPath (Join-Path $projectionPack "manifest.json") -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$projection.schema_version -ne "missioncore.e15-live-projection-pack/v1" -or
|
||||
|
|
@ -137,6 +159,12 @@ if (
|
|||
$projection.identity.calibration_slot -ne "camera_1" -or
|
||||
$projection.identity.calibration_sha256 -ne $live.source.calibration_sha256
|
||||
) { throw "LAB E15 projection pack binding changed" }
|
||||
$packageManifest = Get-Content -LiteralPath (Join-Path $package "manifest.json") -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$packageManifest.schema_version -ne "missioncore.e15-worker-package/v1" -or
|
||||
$packageManifest.package_id -ne (Split-Path $package -Leaf) -or
|
||||
$packageManifest.identity_sha256 -notmatch "^[a-f0-9]{64}$"
|
||||
) { throw "LAB E15 worker package manifest changed" }
|
||||
$mediaManifest = Get-Content -LiteralPath (Join-Path $mediaRuntime "manifest.json") -Raw | ConvertFrom-Json
|
||||
if (
|
||||
$mediaManifest.schema_version -ne "missioncore.e15-media-runtime/v1" -or
|
||||
|
|
@ -165,6 +193,7 @@ $liveProfileName = Split-Path $liveProfile -Leaf
|
|||
$e14ProfileName = Split-Path $e14Profile -Leaf
|
||||
$detectorProfileName = Split-Path $detectorProfile -Leaf
|
||||
$semanticProfileName = Split-Path $semanticProfile -Leaf
|
||||
$stabilityProfileName = if ($stabilityProfile) { Split-Path $stabilityProfile -Leaf } else { $null }
|
||||
$projectionMount = "/" + (Split-Path $projectionPack -Leaf)
|
||||
$packageMount = "/" + (Split-Path $package -Leaf)
|
||||
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
|
|
@ -200,6 +229,11 @@ $commonRunnerArgs = @(
|
|||
"--environment", "/environment",
|
||||
"--worker-package", $packageMount
|
||||
)
|
||||
if ($stabilityProfileName) {
|
||||
$commonRunnerArgs += @(
|
||||
"--stability-profile", ("/runner/{0}" -f $stabilityProfileName)
|
||||
)
|
||||
}
|
||||
|
||||
if ($PersistentService) {
|
||||
if ($PreflightOnly -or $TokenStdin) {
|
||||
|
|
@ -214,6 +248,14 @@ if ($PersistentService) {
|
|||
throw "Persistent output root must be a direct child of its guarded D: parent"
|
||||
}
|
||||
$runnerSha256 = (Get-FileHash -LiteralPath $runner -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$liveProfileSha256 = (Get-FileHash -LiteralPath $liveProfile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$e14ProfileSha256 = (Get-FileHash -LiteralPath $e14Profile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$detectorProfileSha256 = (Get-FileHash -LiteralPath $detectorProfile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$semanticProfileSha256 = (Get-FileHash -LiteralPath $semanticProfile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$stabilityProfileSha256 = if ($stabilityProfile) {
|
||||
(Get-FileHash -LiteralPath $stabilityProfile -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
else { "none" }
|
||||
$existingContainerId = docker ps -a --filter ("name=^{0}$" -f $PersistentContainer) --format "{{.ID}}"
|
||||
Assert-LastExitCode "Persistent worker container lookup"
|
||||
if ($existingContainerId) {
|
||||
|
|
@ -221,7 +263,14 @@ if ($PersistentService) {
|
|||
Assert-LastExitCode "Persistent worker label inspection"
|
||||
if (
|
||||
$labels.'missioncore.role' -ne "perception-persistent-worker" -or
|
||||
$labels.'missioncore.runner.sha256' -ne $runnerSha256
|
||||
$labels.'missioncore.runner.sha256' -ne $runnerSha256 -or
|
||||
$labels.'missioncore.live.sha256' -ne $liveProfileSha256 -or
|
||||
$labels.'missioncore.e14.sha256' -ne $e14ProfileSha256 -or
|
||||
$labels.'missioncore.detector.sha256' -ne $detectorProfileSha256 -or
|
||||
$labels.'missioncore.semantic.sha256' -ne $semanticProfileSha256 -or
|
||||
$labels.'missioncore.stability.sha256' -ne $stabilityProfileSha256 -or
|
||||
$labels.'missioncore.worker-package.identity' -ne $packageManifest.identity_sha256 -or
|
||||
$labels.'missioncore.projection.identity' -ne $projection.identity_sha256
|
||||
) { throw "Existing persistent worker has a different immutable identity" }
|
||||
$running = docker inspect --format "{{.State.Running}}" $PersistentContainer
|
||||
Assert-LastExitCode "Persistent worker state inspection"
|
||||
|
|
@ -258,6 +307,13 @@ if ($PersistentService) {
|
|||
"run", "--detach", "--name", $PersistentContainer,
|
||||
"--label", "missioncore.role=perception-persistent-worker",
|
||||
"--label", ("missioncore.runner.sha256={0}" -f $runnerSha256),
|
||||
"--label", ("missioncore.live.sha256={0}" -f $liveProfileSha256),
|
||||
"--label", ("missioncore.e14.sha256={0}" -f $e14ProfileSha256),
|
||||
"--label", ("missioncore.detector.sha256={0}" -f $detectorProfileSha256),
|
||||
"--label", ("missioncore.semantic.sha256={0}" -f $semanticProfileSha256),
|
||||
"--label", ("missioncore.stability.sha256={0}" -f $stabilityProfileSha256),
|
||||
"--label", ("missioncore.worker-package.identity={0}" -f $packageManifest.identity_sha256),
|
||||
"--label", ("missioncore.projection.identity={0}" -f $projection.identity_sha256),
|
||||
"--gpus", "all", "--network", ("container:{0}" -f $TritonContainer),
|
||||
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
|
||||
"--pids-limit", "512", "--shm-size", "4g",
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ def semantic_worker(
|
|||
completed: list[SemanticResult],
|
||||
failures: list[BaseException],
|
||||
stop_after_results: int | None,
|
||||
transform_target: Any | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
while (envelope := queue.take()) is not None:
|
||||
|
|
@ -225,6 +226,14 @@ def semantic_worker(
|
|||
raise RuntimeError("LAB E10 EoMT emitted an unknown category")
|
||||
target = target_lut[semantic].copy()
|
||||
target[~valid_mask] = 0
|
||||
if transform_target is not None:
|
||||
target = transform_target(target)
|
||||
if (
|
||||
not isinstance(target, np.ndarray)
|
||||
or target.dtype != np.uint8
|
||||
or target.shape != valid_mask.shape
|
||||
):
|
||||
raise RuntimeError("semantic target transform returned an invalid mask")
|
||||
for name, value in measured.items():
|
||||
latency[name].append(float(value))
|
||||
finished = time.perf_counter()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import struct
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import Counter
|
||||
from collections import Counter, deque
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
|
@ -83,6 +83,15 @@ from run_recorded_perception_epoch import (
|
|||
_write_json,
|
||||
)
|
||||
|
||||
from k1link.compute.inline_temporal import (
|
||||
PIPELINE_ID as INLINE_TEMPORAL_PIPELINE_ID,
|
||||
)
|
||||
from k1link.compute.inline_temporal import (
|
||||
StreamingSemanticStabilizer,
|
||||
TemporalStabilizer,
|
||||
read_inline_profile,
|
||||
stabilize_world_state,
|
||||
)
|
||||
from k1link.compute.live_perception import (
|
||||
LIVE_RESULT_MAX_PAYLOAD_BYTES,
|
||||
LiveSensorSynchronizer,
|
||||
|
|
@ -119,6 +128,7 @@ def arguments() -> argparse.Namespace:
|
|||
command.add_argument("--cache", type=Path, required=True)
|
||||
command.add_argument("--environment", type=Path, required=True)
|
||||
command.add_argument("--worker-package", type=Path, required=True)
|
||||
command.add_argument("--stability-profile", type=Path)
|
||||
if name in {"run", "serve"}:
|
||||
command.add_argument("--host", default="host.docker.internal")
|
||||
command.add_argument("--port", type=int, default=18012)
|
||||
|
|
@ -172,8 +182,7 @@ def read_live_profile(path: Path) -> tuple[dict[str, Any], str]:
|
|||
or source.get("source_id") != "sensor.camera.right"
|
||||
or source.get("resolution") != [800, 600]
|
||||
or source.get("calibration_slot") != "camera_1"
|
||||
or authority
|
||||
!= {"commands_enabled": False, "navigation_or_safety_accepted": False}
|
||||
or authority != {"commands_enabled": False, "navigation_or_safety_accepted": False}
|
||||
or transport.get("wire_schema") != "missioncore.live-perception-wire/v1"
|
||||
or transport.get("camera_media") != "persistent-fmp4-pyav"
|
||||
):
|
||||
|
|
@ -190,9 +199,7 @@ def read_live_profile(path: Path) -> tuple[dict[str, Any], str]:
|
|||
if (
|
||||
not 100 <= float(scheduling.get("semantic_ttl_ms", 0)) <= 5000
|
||||
or not 0 <= float(scheduling.get("sensor_wait_ms", -1)) <= 100
|
||||
or not 1_048_576
|
||||
<= int(transport.get("maximum_media_buffer_bytes", 0))
|
||||
<= 64 * 1024 * 1024
|
||||
or not 1_048_576 <= int(transport.get("maximum_media_buffer_bytes", 0)) <= 64 * 1024 * 1024
|
||||
or not 2 <= int(transport.get("camera_metadata_capacity", 0)) <= 128
|
||||
or not 1 <= int(temporal.get("buffer_capacity_per_modality", 0)) <= 256
|
||||
or not 0.1 <= float(temporal.get("retention_seconds", 0)) <= 30
|
||||
|
|
@ -294,14 +301,13 @@ def read_worker_package(root: Path) -> dict[str, Any]:
|
|||
manifest.get("schema_version") != WORKER_PACKAGE_SCHEMA
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != WORKER_PACKAGE_SCHEMA
|
||||
or identity.get("classification")
|
||||
!= "minimal-live-worker-import-projection"
|
||||
or identity.get("classification") != "minimal-live-worker-import-projection"
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or manifest.get("package_id") != f"e15-worker-package-{identity_sha256}"
|
||||
or resolved.name != manifest.get("package_id")
|
||||
or not isinstance(artifacts, list)
|
||||
or len(artifacts) != 11
|
||||
or len(artifacts) != 12
|
||||
):
|
||||
raise RuntimeError("LAB E15 worker package identity is invalid")
|
||||
expected = set()
|
||||
|
|
@ -321,9 +327,7 @@ def read_worker_package(root: Path) -> dict[str, Any]:
|
|||
raise RuntimeError("LAB E15 worker package artifact changed")
|
||||
expected.add(relative)
|
||||
actual = {
|
||||
path.relative_to(resolved).as_posix()
|
||||
for path in resolved.rglob("*")
|
||||
if path.is_file()
|
||||
path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file()
|
||||
}
|
||||
if actual != expected | {"manifest.json"}:
|
||||
raise RuntimeError("LAB E15 worker package file set changed")
|
||||
|
|
@ -386,7 +390,7 @@ class _RuntimeTelemetry:
|
|||
self._started = time.perf_counter()
|
||||
self._previous_wall = self._started
|
||||
self._previous_cpu = time.process_time()
|
||||
self.samples: list[dict[str, Any]] = []
|
||||
self.samples: deque[dict[str, Any]] = deque(maxlen=4096)
|
||||
|
||||
def __enter__(self) -> _RuntimeTelemetry:
|
||||
self._thread.start()
|
||||
|
|
@ -498,6 +502,7 @@ class _RuntimeTelemetry:
|
|||
self._sample()
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
samples = list(self.samples)
|
||||
numeric_fields = (
|
||||
"process_cpu_percent",
|
||||
"process_rss_mib",
|
||||
|
|
@ -506,25 +511,26 @@ class _RuntimeTelemetry:
|
|||
"cgroup_memory_current_mib",
|
||||
)
|
||||
summary: dict[str, Any] = {
|
||||
"sample_count": len(self.samples),
|
||||
"sample_count": len(samples),
|
||||
"sample_capacity": self.samples.maxlen,
|
||||
"interval_seconds": self._interval,
|
||||
}
|
||||
for field in numeric_fields:
|
||||
values = [
|
||||
float(sample[field])
|
||||
for sample in self.samples
|
||||
for sample in samples
|
||||
if isinstance(sample.get(field), int | float)
|
||||
]
|
||||
summary[field] = _percentiles(values)
|
||||
if self.samples:
|
||||
quarter = max(1, len(self.samples) // 4)
|
||||
early = [float(value["process_rss_mib"]) for value in self.samples[:quarter]]
|
||||
late = [float(value["process_rss_mib"]) for value in self.samples[-quarter:]]
|
||||
if samples:
|
||||
quarter = max(1, len(samples) // 4)
|
||||
early = [float(value["process_rss_mib"]) for value in samples[:quarter]]
|
||||
late = [float(value["process_rss_mib"]) for value in samples[-quarter:]]
|
||||
summary["rss_growth_mib"] = round(
|
||||
float(_percentiles(late)["p95"]) - float(_percentiles(early)["p95"]),
|
||||
6,
|
||||
)
|
||||
summary["final_queues"] = self.samples[-1]["queues"]
|
||||
summary["final_queues"] = samples[-1]["queues"]
|
||||
else:
|
||||
summary["rss_growth_mib"] = 0.0
|
||||
summary["final_queues"] = {}
|
||||
|
|
@ -627,9 +633,7 @@ def _receiver(
|
|||
if state.last_ingress_sequence is not None:
|
||||
if sequence <= state.last_ingress_sequence:
|
||||
raise ShadowRuntimeError("shadow ingress sequence is not increasing")
|
||||
state.ingress_sequence_gaps += max(
|
||||
0, sequence - state.last_ingress_sequence - 1
|
||||
)
|
||||
state.ingress_sequence_gaps += max(0, sequence - state.last_ingress_sequence - 1)
|
||||
if state.first_ingress_sequence is None:
|
||||
state.first_ingress_sequence = sequence
|
||||
state.last_ingress_sequence = sequence
|
||||
|
|
@ -661,9 +665,7 @@ def _receiver(
|
|||
f"expected at least {expected_camera_sequence}, "
|
||||
f"got {camera_source_sequence}"
|
||||
)
|
||||
state.camera_sequence_gaps += (
|
||||
camera_source_sequence - expected_camera_sequence
|
||||
)
|
||||
state.camera_sequence_gaps += camera_source_sequence - expected_camera_sequence
|
||||
state.last_camera_source_sequence = camera_source_sequence
|
||||
decoder.feed_segment(
|
||||
CameraFragmentMetadata(
|
||||
|
|
@ -686,9 +688,7 @@ def _receiver(
|
|||
),
|
||||
processing_started_monotonic_ns=time.monotonic_ns(),
|
||||
)
|
||||
sensor_decode_ms[modality].append(
|
||||
(time.perf_counter() - decode_started) * 1000
|
||||
)
|
||||
sensor_decode_ms[modality].append((time.perf_counter() - decode_started) * 1000)
|
||||
if modality == "lidar" and isinstance(normalized, DecodedPointCloudView):
|
||||
synchronizer.publish_point_cloud(normalized)
|
||||
elif modality == "pose" and isinstance(normalized, DecodedPoseView):
|
||||
|
|
@ -707,9 +707,7 @@ def _receiver(
|
|||
sender_thread.join(timeout=5)
|
||||
if sender_thread.is_alive():
|
||||
assert state.failures is not None
|
||||
state.failures.append(
|
||||
ShadowRuntimeError("live result publisher did not stop")
|
||||
)
|
||||
state.failures.append(ShadowRuntimeError("live result publisher did not stop"))
|
||||
if stream is not None:
|
||||
with suppress(Exception), write_lock:
|
||||
_send_client_frame(stream, 0x8, b"")
|
||||
|
|
@ -760,6 +758,12 @@ def _common(args: argparse.Namespace) -> dict[str, Any]:
|
|||
if dependency["identity"]["profile_sha256"] != semantic_sha256:
|
||||
raise RuntimeError("LAB E15 semantic dependency identity changed")
|
||||
worker_package = read_worker_package(args.worker_package)
|
||||
stability = None
|
||||
stability_sha256 = None
|
||||
if args.stability_profile is not None:
|
||||
stability, stability_sha256 = read_inline_profile(args.stability_profile)
|
||||
if stability["source"] != live["source"] or stability["authority"] != live["authority"]:
|
||||
raise RuntimeError("LAB E23 inline temporal source binding changed")
|
||||
return {
|
||||
"job": job,
|
||||
"live": live,
|
||||
|
|
@ -777,6 +781,8 @@ def _common(args: argparse.Namespace) -> dict[str, Any]:
|
|||
"detector_files": detector_files,
|
||||
"dependency": dependency,
|
||||
"worker_package": worker_package,
|
||||
"stability": stability,
|
||||
"stability_sha256": stability_sha256,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -804,6 +810,7 @@ def preflight(args: argparse.Namespace) -> int:
|
|||
"e14_profile_sha256": common["e14_sha256"],
|
||||
"projection_pack": common["projection_manifest"]["pack_id"],
|
||||
"worker_package": common["worker_package"]["package_id"],
|
||||
"stability_profile_sha256": common["stability_sha256"],
|
||||
"pyav": av.__version__,
|
||||
"lz4": getattr(lz4, "__version__", importlib.metadata.version("lz4")),
|
||||
"detector_files": common["detector_files"],
|
||||
|
|
@ -840,8 +847,7 @@ def _load_models(args: argparse.Namespace, common: dict[str, Any]) -> _LoadedMod
|
|||
)
|
||||
infer_semantic = e9._semantic_infer_factory(processor, semantic_model, device)
|
||||
target_names = {
|
||||
int(key): str(value)
|
||||
for key, value in common["semantic"]["target_taxonomy"].items()
|
||||
int(key): str(value) for key, value in common["semantic"]["target_taxonomy"].items()
|
||||
}
|
||||
warm = np.zeros((600, 800, 3), dtype=np.uint8)
|
||||
_infer(
|
||||
|
|
@ -894,6 +900,10 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
infer_semantic = loaded.infer_semantic
|
||||
semantic_files = loaded.semantic_files
|
||||
target_lut = loaded.target_lut
|
||||
stability = common["stability"]
|
||||
temporal_stabilizer = TemporalStabilizer(stability) if stability is not None else None
|
||||
semantic_stabilizer = StreamingSemanticStabilizer(stability) if stability is not None else None
|
||||
temporal_world_memory: dict[int, dict[str, Any]] = {}
|
||||
tracker = TwoStageTracker(detector["tracking"])
|
||||
linear_sum_assignment(np.zeros((1, 1), dtype=np.float64))
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
|
|
@ -988,16 +998,19 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"source_epoch_age_ms_unqualified",
|
||||
)
|
||||
}
|
||||
latency["temporal_2d_3d_ms"] = deque(maxlen=4096)
|
||||
status_counts: Counter[str] = Counter()
|
||||
fusion_state_counts: Counter[str] = Counter()
|
||||
rejection_counts: Counter[str] = Counter()
|
||||
fused_frames = 0
|
||||
accepted_cuboids = 0
|
||||
stabilized_cuboids = 0
|
||||
detector_failures = 0
|
||||
history = distance_history(int(e14["association"]["distance_history_frames"]))
|
||||
projector = WorldStateProjector(float(e14["world_state"]["velocity_history_limit_s"]))
|
||||
completion_tracker = CuboidCompletionTracker(e14["cuboid_completion"])
|
||||
semantic_path = output / "semantic-frames.jsonl"
|
||||
raw_fusion_path = output / "raw-fusion-frames.jsonl"
|
||||
fusion_path = output / "fusion-frames.jsonl"
|
||||
world_path = output / "world-state.jsonl"
|
||||
gpu_path = output / "gpu-telemetry.jsonl"
|
||||
|
|
@ -1007,6 +1020,7 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
|
||||
with (
|
||||
semantic_path.open("x", encoding="utf-8", newline="\n") as semantic_stream,
|
||||
raw_fusion_path.open("x", encoding="utf-8", newline="\n") as raw_fusion_stream,
|
||||
fusion_path.open("x", encoding="utf-8", newline="\n") as fusion_stream,
|
||||
world_path.open("x", encoding="utf-8", newline="\n") as world_stream,
|
||||
gpu_path.open("x", encoding="utf-8", newline="\n") as gpu_stream,
|
||||
|
|
@ -1029,6 +1043,7 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
},
|
||||
) as runtime_telemetry,
|
||||
):
|
||||
|
||||
class SemanticResultStream:
|
||||
def __init__(self) -> None:
|
||||
self.count = 0
|
||||
|
|
@ -1067,6 +1082,9 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"completed": completed_semantics,
|
||||
"failures": semantic_errors,
|
||||
"stop_after_results": None,
|
||||
"transform_target": (
|
||||
None if semantic_stabilizer is None else semantic_stabilizer.update
|
||||
),
|
||||
},
|
||||
name="lab-e15-semantic",
|
||||
daemon=True,
|
||||
|
|
@ -1115,18 +1133,14 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
while (envelope := detector_queue.take()) is not None:
|
||||
started = time.perf_counter()
|
||||
latency["decode_age_ms"].append(float(envelope.decode_ms))
|
||||
latency["queue_wait_ms"].append(
|
||||
max(0.0, (started - envelope.decoded_monotonic) * 1000)
|
||||
)
|
||||
latency["queue_wait_ms"].append(max(0.0, (started - envelope.decoded_monotonic) * 1000))
|
||||
try:
|
||||
detector_started = time.perf_counter()
|
||||
tensor = _preprocess(envelope.image, valid_mask, detector)
|
||||
output_tensor, _request_ms = _infer(args.triton_url, detector["model"], tensor)
|
||||
detections, _rejected = _detections(output_tensor, detector, valid_mask)
|
||||
tracks = tracker.update(detections, envelope.frame_index)
|
||||
latency["detector_ms"].append(
|
||||
(time.perf_counter() - detector_started) * 1000
|
||||
)
|
||||
latency["detector_ms"].append((time.perf_counter() - detector_started) * 1000)
|
||||
|
||||
frame_seconds = float(envelope.timeline["session_seconds"])
|
||||
current_semantic = latest.snapshot()
|
||||
|
|
@ -1141,9 +1155,7 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
int(envelope.timeline["captured_at_epoch_ns"]),
|
||||
wait_seconds=float(scheduling["sensor_wait_ms"]) / 1000,
|
||||
)
|
||||
latency["sensor_wait_ms"].append(
|
||||
(time.perf_counter() - sensor_wait_started) * 1000
|
||||
)
|
||||
latency["sensor_wait_ms"].append((time.perf_counter() - sensor_wait_started) * 1000)
|
||||
fusions = ()
|
||||
points_lidar = np.empty((0, 3), dtype=np.float64)
|
||||
if binding.state != "fused-ready":
|
||||
|
|
@ -1194,18 +1206,12 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
rejection_counts[item.status] += 1
|
||||
clearance_started = time.perf_counter()
|
||||
clearance_state = clearance(points_lidar, e14["world_state"]["clearance"])
|
||||
latency["clearance_ms"].append(
|
||||
(time.perf_counter() - clearance_started) * 1000
|
||||
)
|
||||
latency["clearance_ms"].append((time.perf_counter() - clearance_started) * 1000)
|
||||
world_started = time.perf_counter()
|
||||
result_age = max(
|
||||
0.0, (world_started - envelope.scheduled_monotonic) * 1000
|
||||
)
|
||||
result_age = max(0.0, (world_started - envelope.scheduled_monotonic) * 1000)
|
||||
if result_age >= 1000:
|
||||
health = "unavailable"
|
||||
elif result_age >= float(
|
||||
live["acceptance"]["maximum_p95_world_state_age_ms"]
|
||||
):
|
||||
elif result_age >= float(live["acceptance"]["maximum_p95_world_state_age_ms"]):
|
||||
health = "stale"
|
||||
elif fusion_state != "fused":
|
||||
health = "degraded"
|
||||
|
|
@ -1229,37 +1235,58 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"clock_qualification": "worker-ingress-only",
|
||||
},
|
||||
)
|
||||
latency["world_state_ms"].append(
|
||||
(time.perf_counter() - world_started) * 1000
|
||||
)
|
||||
result_age = max(
|
||||
0.0, (time.perf_counter() - envelope.scheduled_monotonic) * 1000
|
||||
raw_fusion_objects = [fusion_document(item) for item in fusions]
|
||||
if temporal_stabilizer is None:
|
||||
fusion_objects = raw_fusion_objects
|
||||
else:
|
||||
temporal_started = time.perf_counter()
|
||||
fusion_objects = temporal_stabilizer.update(
|
||||
frame_index=envelope.frame_index,
|
||||
session_seconds=frame_seconds,
|
||||
objects=raw_fusion_objects,
|
||||
)
|
||||
world = stabilize_world_state(
|
||||
world,
|
||||
fusion_objects,
|
||||
temporal_world_memory,
|
||||
)
|
||||
latency["temporal_2d_3d_ms"].append(
|
||||
(time.perf_counter() - temporal_started) * 1000
|
||||
)
|
||||
stabilized_cuboids += sum(
|
||||
str(item.get("cuboid_status", "")).startswith("accepted-")
|
||||
for item in fusion_objects
|
||||
)
|
||||
latency["world_state_ms"].append((time.perf_counter() - world_started) * 1000)
|
||||
result_age = max(0.0, (time.perf_counter() - envelope.scheduled_monotonic) * 1000)
|
||||
world["delivery"]["result_age_ms"] = result_age
|
||||
latency["world_state_age_ms"].append(result_age)
|
||||
latency["source_epoch_age_ms_unqualified"].append(
|
||||
(time.time_ns() - int(envelope.timeline["captured_at_epoch_ns"]))
|
||||
/ 1_000_000
|
||||
(time.time_ns() - int(envelope.timeline["captured_at_epoch_ns"])) / 1_000_000
|
||||
)
|
||||
fusion_row = {
|
||||
"schema_version": FUSION_SCHEMA,
|
||||
"frame_index": envelope.frame_index,
|
||||
"source_frame_index": int(envelope.timeline["source_frame_index"]),
|
||||
"session_seconds": frame_seconds,
|
||||
"fusion_state": fusion_state,
|
||||
"semantic_status": semantic_status,
|
||||
"semantic_source_frame_index": (
|
||||
None if current_semantic is None else current_semantic.source_frame_index
|
||||
),
|
||||
}
|
||||
raw_fusion_stream.write(
|
||||
json.dumps(
|
||||
{**fusion_row, "objects": raw_fusion_objects},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
fusion_objects = [fusion_document(item) for item in fusions]
|
||||
fusion_stream.write(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": FUSION_SCHEMA,
|
||||
"frame_index": envelope.frame_index,
|
||||
"source_frame_index": int(
|
||||
envelope.timeline["source_frame_index"]
|
||||
),
|
||||
"session_seconds": frame_seconds,
|
||||
"fusion_state": fusion_state,
|
||||
"semantic_status": semantic_status,
|
||||
"semantic_source_frame_index": (
|
||||
None
|
||||
if current_semantic is None
|
||||
else current_semantic.source_frame_index
|
||||
),
|
||||
"objects": fusion_objects,
|
||||
},
|
||||
{**fusion_row, "objects": fusion_objects},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
|
|
@ -1315,18 +1342,9 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
receiver_thread.join(timeout=args.max_duration_seconds + 10)
|
||||
decoder_watcher.join(timeout=30)
|
||||
semantic_thread.join(timeout=30)
|
||||
if (
|
||||
receiver_thread.is_alive()
|
||||
or decoder_watcher.is_alive()
|
||||
or semantic_thread.is_alive()
|
||||
):
|
||||
if receiver_thread.is_alive() or decoder_watcher.is_alive() or semantic_thread.is_alive():
|
||||
raise RuntimeError("LAB E15 runtime thread did not stop")
|
||||
if (
|
||||
transport.failures
|
||||
or decoder_watch_failures
|
||||
or callback_failures
|
||||
or semantic_errors
|
||||
):
|
||||
if transport.failures or decoder_watch_failures or callback_failures or semantic_errors:
|
||||
failures = [
|
||||
*(transport.failures or []),
|
||||
*decoder_watch_failures,
|
||||
|
|
@ -1334,11 +1352,15 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
*semantic_errors,
|
||||
]
|
||||
summary = "; ".join(
|
||||
f"{type(failure).__name__}: {str(failure)[:240]}"
|
||||
for failure in failures[:8]
|
||||
f"{type(failure).__name__}: {str(failure)[:240]}" for failure in failures[:8]
|
||||
)
|
||||
raise RuntimeError(f"LAB E15 runtime worker failed: {summary}")
|
||||
for stream in (semantic_stream, fusion_stream, world_stream):
|
||||
for stream in (
|
||||
semantic_stream,
|
||||
raw_fusion_stream,
|
||||
fusion_stream,
|
||||
world_stream,
|
||||
):
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
|
@ -1354,22 +1376,23 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
detector_fps = int(detector_state["consumed"]) / max(source_span, run_wall, 1e-9)
|
||||
semantic_fps = int(semantic_state["consumed"]) / max(source_span, run_wall, 1e-9)
|
||||
semantic_scheduled = (
|
||||
(decoded_frame_count - 1)
|
||||
// int(scheduling["semantic_sample_every_frames"])
|
||||
(decoded_frame_count - 1) // int(scheduling["semantic_sample_every_frames"])
|
||||
) + 1
|
||||
fresh_coverage = status_counts["fresh"] / max(1, int(detector_state["consumed"]))
|
||||
fused_fraction = fused_frames / max(1, int(detector_state["consumed"]))
|
||||
latency_summary = {name: _percentiles(values) for name, values in latency.items()}
|
||||
semantic_summary = {
|
||||
name: _percentiles(values) for name, values in semantic_latency.items()
|
||||
}
|
||||
semantic_summary = {name: _percentiles(values) for name, values in semantic_latency.items()}
|
||||
sensor_decode_summary = {
|
||||
name: _percentiles(values) for name, values in sensor_decode_ms.items()
|
||||
}
|
||||
runtime_summary = runtime_telemetry.summary()
|
||||
temporal_track_summary = None if temporal_stabilizer is None else temporal_stabilizer.snapshot()
|
||||
temporal_semantic_summary = (
|
||||
None if semantic_stabilizer is None else semantic_stabilizer.snapshot()
|
||||
)
|
||||
acceptance = live["acceptance"]
|
||||
checks = {
|
||||
"minimum_camera_frames": decoded_frame_count
|
||||
>= int(acceptance["minimum_camera_frames"]),
|
||||
"minimum_camera_frames": decoded_frame_count >= int(acceptance["minimum_camera_frames"]),
|
||||
"camera_decoder_accounting": decoder.snapshot()["decoded_frames"]
|
||||
== transport.counts["camera-frame"],
|
||||
"detector_accounting": int(detector_state["consumed"])
|
||||
|
|
@ -1394,13 +1417,10 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
<= float(acceptance["semantic_maximum_p95_completion_age_ms"]),
|
||||
"minimum_fresh_semantic_coverage": fresh_coverage
|
||||
>= float(acceptance["minimum_fresh_semantic_coverage"]),
|
||||
"minimum_fused_fraction": fused_fraction
|
||||
>= float(acceptance["minimum_fused_fraction"]),
|
||||
"minimum_fused_fraction": fused_fraction >= float(acceptance["minimum_fused_fraction"]),
|
||||
"maximum_p95_decode_age_ms": float(latency_summary["decode_age_ms"]["p95"])
|
||||
<= float(acceptance["maximum_p95_decode_age_ms"]),
|
||||
"maximum_p95_world_state_age_ms": float(
|
||||
latency_summary["world_state_age_ms"]["p95"]
|
||||
)
|
||||
"maximum_p95_world_state_age_ms": float(latency_summary["world_state_age_ms"]["p95"])
|
||||
<= float(acceptance["maximum_p95_world_state_age_ms"]),
|
||||
"zero_transport_gaps": (
|
||||
transport.ingress_sequence_gaps == 0
|
||||
|
|
@ -1424,10 +1444,39 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"authority_remains_shadow_only": live["authority"]
|
||||
== {"commands_enabled": False, "navigation_or_safety_accepted": False},
|
||||
}
|
||||
if stability is not None:
|
||||
temporal_acceptance = stability["acceptance"]
|
||||
assert temporal_track_summary is not None
|
||||
assert temporal_semantic_summary is not None
|
||||
checks.update(
|
||||
{
|
||||
"temporal_camera_processing_p95": float(latency_summary["temporal_2d_3d_ms"]["p95"])
|
||||
<= float(temporal_acceptance["maximum_camera_frame_processing_p95_ms"]),
|
||||
"temporal_semantic_processing_p95": float(
|
||||
temporal_semantic_summary["processing_ms"]["p95"]
|
||||
)
|
||||
<= float(temporal_acceptance["maximum_semantic_frame_processing_p95_ms"]),
|
||||
"temporal_track_state_bound": int(temporal_track_summary["peak_track_states"])
|
||||
<= int(temporal_acceptance["maximum_track_states_observed"]),
|
||||
"temporal_semantic_unsupported_change_reduction": float(
|
||||
temporal_semantic_summary["unsupported_change_reduction_fraction"]
|
||||
)
|
||||
>= float(
|
||||
temporal_acceptance["minimum_semantic_unsupported_change_reduction_fraction"]
|
||||
),
|
||||
"temporal_rss_growth_bound": float(runtime_summary["rss_growth_mib"])
|
||||
<= float(temporal_acceptance["maximum_rss_growth_mib"]),
|
||||
"temporal_authority_remains_shadow_only": stability["authority"]
|
||||
== {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
accepted = all(checks.values())
|
||||
identity = {
|
||||
"schema_version": IDENTITY_SCHEMA,
|
||||
"pipeline": PIPELINE_ID,
|
||||
"pipeline": (PIPELINE_ID if stability is None else INLINE_TEMPORAL_PIPELINE_ID),
|
||||
"session_id": transport.session_id,
|
||||
"bootstrap_job_id": common["job"]["job_id"],
|
||||
"source_id": live["source"]["source_id"],
|
||||
|
|
@ -1436,6 +1485,7 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"e14_sha256": common["e14_sha256"],
|
||||
"detector_sha256": common["detector_sha256"],
|
||||
"semantic_sha256": common["semantic_sha256"],
|
||||
"stability_sha256": common["stability_sha256"],
|
||||
},
|
||||
"projection_pack": {
|
||||
"id": common["projection_manifest"]["pack_id"],
|
||||
|
|
@ -1498,14 +1548,22 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"fused_frames": fused_frames,
|
||||
"fused_fraction": fused_fraction,
|
||||
"accepted_cuboids": accepted_cuboids,
|
||||
"stabilized_cuboids": stabilized_cuboids,
|
||||
"state_counts": dict(fusion_state_counts),
|
||||
"rejection_counts": dict(rejection_counts),
|
||||
"synchronizer": synchronizer.snapshot(),
|
||||
"sensor_decode_ms": sensor_decode_summary,
|
||||
},
|
||||
"latency_ms": latency_summary,
|
||||
"temporal_stability": {
|
||||
"enabled": stability is not None,
|
||||
"profile_sha256": common["stability_sha256"],
|
||||
"tracking_2d_3d": temporal_track_summary,
|
||||
"semantic": temporal_semantic_summary,
|
||||
"world_memory_objects": len(temporal_world_memory),
|
||||
},
|
||||
"gpu_telemetry": gpu.summary(),
|
||||
"runtime_telemetry": runtime_telemetry.summary(),
|
||||
"runtime_telemetry": runtime_summary,
|
||||
"process_cpu_seconds": time.process_time() - process_cpu_started,
|
||||
"process_peak_rss_mib": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
||||
"cuda_peak_memory_allocated_mib": torch.cuda.max_memory_allocated() / 2**20,
|
||||
|
|
@ -1547,12 +1605,26 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"Cross-host source epoch age is diagnostic and excluded from acceptance.",
|
||||
"COCO and Cityscapes models are not forest-domain or safety validated.",
|
||||
"Amodal cuboids infer unobserved volume from class priors.",
|
||||
*(
|
||||
[
|
||||
"E23 temporal outputs are inline bounded shadow diagnostics, "
|
||||
"not validated driving or safety authority."
|
||||
]
|
||||
if stability is not None
|
||||
else []
|
||||
),
|
||||
],
|
||||
}
|
||||
report_path = output / "run-report.json"
|
||||
_write_json(report_path, report)
|
||||
artifacts = [
|
||||
_artifact(semantic_path, "e15-semantic-frames", "application/x-ndjson", SEMANTIC_SCHEMA),
|
||||
_artifact(
|
||||
raw_fusion_path,
|
||||
"e23-raw-fusion-frames",
|
||||
"application/x-ndjson",
|
||||
FUSION_SCHEMA,
|
||||
),
|
||||
_artifact(fusion_path, "e15-fusion-frames", "application/x-ndjson", FUSION_SCHEMA),
|
||||
_artifact(world_path, "e15-world-state", "application/x-ndjson", WORLD_SCHEMA),
|
||||
_artifact(gpu_path, "worker-gpu-telemetry", "application/x-ndjson"),
|
||||
|
|
@ -1590,6 +1662,8 @@ def run(args: argparse.Namespace, loaded: _LoadedModels | None = None) -> int:
|
|||
"fused_fraction": fused_fraction,
|
||||
"world_state_age_p95_ms": latency_summary["world_state_age_ms"]["p95"],
|
||||
"accepted_cuboids": accepted_cuboids,
|
||||
"inline_temporal": stability is not None,
|
||||
"temporal_2d_3d_p95_ms": latency_summary["temporal_2d_3d_ms"]["p95"],
|
||||
},
|
||||
sort_keys=True,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from .lab_instances import (
|
|||
PublishedTemporalLabInstance,
|
||||
publish_e21_lab_instance,
|
||||
publish_e22_lab_instance,
|
||||
publish_e23_lab_instance,
|
||||
publish_integrated_lab_instance,
|
||||
)
|
||||
from .live_perception import (
|
||||
|
|
@ -148,6 +149,7 @@ __all__ = [
|
|||
"validate_integrated_perception_result",
|
||||
"publish_e21_lab_instance",
|
||||
"publish_e22_lab_instance",
|
||||
"publish_e23_lab_instance",
|
||||
"publish_integrated_lab_instance",
|
||||
"validate_multirate_perception_qualification_result",
|
||||
"prepare_recorded_qualification_slice",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,628 @@
|
|||
"""Bounded inline temporal state for the warm perception worker.
|
||||
|
||||
This module intentionally depends only on the Python standard library and
|
||||
NumPy so the exact implementation can be included in the minimal, hashed
|
||||
worker package. It has no command or navigation authority.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
PROFILE_SCHEMA = "missioncore.e23-inline-temporal-profile/v1"
|
||||
PIPELINE_ID = "warm-worker-inline-bounded-temporal-2d-3d-semantic/v1"
|
||||
TARGET_CLASS_COUNT = 16
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _TrackState:
|
||||
canonical_id: int
|
||||
source_ids: set[int]
|
||||
label: str
|
||||
group: str
|
||||
center: np.ndarray
|
||||
size: np.ndarray
|
||||
raw_center: np.ndarray
|
||||
velocity: np.ndarray
|
||||
last_frame: int
|
||||
last_seconds: float
|
||||
last_observed_frame: int
|
||||
score: float
|
||||
template: dict[str, Any]
|
||||
cuboid_center: np.ndarray | None = None
|
||||
cuboid_size: np.ndarray | None = None
|
||||
cuboid_yaw: float | None = None
|
||||
cuboid_velocity: np.ndarray | None = None
|
||||
cuboid_seconds: float | None = None
|
||||
distance_m: float | None = None
|
||||
|
||||
|
||||
class TemporalStabilizer:
|
||||
"""One-pass bounded state machine for 2D tracks and map-frame cuboids."""
|
||||
|
||||
def __init__(self, profile: dict[str, Any]) -> None:
|
||||
self.profile = profile
|
||||
self.states: dict[int, _TrackState] = {}
|
||||
self.aliases: dict[int, int] = {}
|
||||
self.next_id = 1
|
||||
self.peak_states = 0
|
||||
self.stitched_tracks = 0
|
||||
self.held_2d = 0
|
||||
self.held_3d = 0
|
||||
self.reset_2d = 0
|
||||
self.reset_3d = 0
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
frame_index: int,
|
||||
session_seconds: float,
|
||||
objects: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
self._prune(session_seconds)
|
||||
assigned: set[int] = set()
|
||||
result: list[dict[str, Any]] = []
|
||||
for source in sorted(objects, key=lambda item: int(item["track_id"])):
|
||||
state, stitched = self._resolve(source, frame_index, assigned)
|
||||
assigned.add(state.canonical_id)
|
||||
result.append(
|
||||
self._observe(
|
||||
state,
|
||||
source,
|
||||
frame_index=frame_index,
|
||||
session_seconds=session_seconds,
|
||||
stitched=stitched,
|
||||
)
|
||||
)
|
||||
|
||||
hold_frames = int(self.profile["tracking_2d"]["hold_frames"])
|
||||
for state in sorted(self.states.values(), key=lambda value: value.canonical_id):
|
||||
if state.canonical_id in assigned:
|
||||
continue
|
||||
gap = frame_index - state.last_observed_frame
|
||||
if not 1 <= gap <= hold_frames:
|
||||
continue
|
||||
held = json.loads(json.dumps(state.template))
|
||||
state.center = state.center + state.velocity
|
||||
state.last_frame = frame_index
|
||||
held["track_id"] = state.canonical_id
|
||||
held["bbox_xyxy"] = _box(state.center, state.size)
|
||||
held["score"] = max(0.0, state.score * (0.82**gap))
|
||||
held["temporal_2d_status"] = f"held-{gap}-frame"
|
||||
held["temporal_source_track_id"] = min(state.source_ids)
|
||||
self.held_2d += 1
|
||||
if not self._hold_cuboid(held, state, session_seconds):
|
||||
_clear_cuboid(held, "rejected-temporal-hold-expired-e23")
|
||||
result.append(held)
|
||||
|
||||
self.peak_states = max(self.peak_states, len(self.states))
|
||||
return sorted(result, key=lambda item: int(item["track_id"]))
|
||||
|
||||
def snapshot(self) -> dict[str, int]:
|
||||
return {
|
||||
"active_track_states": len(self.states),
|
||||
"peak_track_states": self.peak_states,
|
||||
"alias_count": len(self.aliases),
|
||||
"stitched_tracks": self.stitched_tracks,
|
||||
"held_2d": self.held_2d,
|
||||
"held_3d": self.held_3d,
|
||||
"reset_2d": self.reset_2d,
|
||||
"reset_3d": self.reset_3d,
|
||||
}
|
||||
|
||||
def _resolve(
|
||||
self,
|
||||
source: dict[str, Any],
|
||||
frame_index: int,
|
||||
assigned: set[int],
|
||||
) -> tuple[_TrackState, bool]:
|
||||
source_id = int(source["track_id"])
|
||||
canonical = self.aliases.get(source_id)
|
||||
if canonical is not None and canonical in self.states:
|
||||
return self.states[canonical], False
|
||||
|
||||
bbox = np.asarray(source["bbox_xyxy"], dtype=np.float64)
|
||||
center, size = _center_size(bbox)
|
||||
config = self.profile["tracking_2d"]
|
||||
best: tuple[float, _TrackState] | None = None
|
||||
for state in self.states.values():
|
||||
gap = frame_index - state.last_observed_frame
|
||||
if (
|
||||
state.canonical_id in assigned
|
||||
or state.label != str(source["label"])
|
||||
or not 1 <= gap <= int(config["stitch_gap_frames"])
|
||||
):
|
||||
continue
|
||||
predicted = state.center + state.velocity * gap
|
||||
predicted_box = np.asarray(_box(predicted, state.size), dtype=np.float64)
|
||||
iou = _iou(predicted_box, bbox)
|
||||
scale = max(1.0, math.sqrt(float(np.prod(np.maximum(state.size, 1.0)))))
|
||||
normalized_distance = float(np.linalg.norm(center - predicted) / scale)
|
||||
if iou < float(config["stitch_minimum_iou"]) and normalized_distance > float(
|
||||
config["stitch_maximum_normalized_center_distance"]
|
||||
):
|
||||
continue
|
||||
score = iou - 0.25 * normalized_distance
|
||||
if best is None or score > best[0]:
|
||||
best = (score, state)
|
||||
if best is not None:
|
||||
state = best[1]
|
||||
state.source_ids.add(source_id)
|
||||
self.aliases[source_id] = state.canonical_id
|
||||
self.stitched_tracks += 1
|
||||
return state, True
|
||||
|
||||
canonical_id = source_id
|
||||
if canonical_id in self.states:
|
||||
canonical_id = max(self.next_id, max(self.states, default=0) + 1)
|
||||
self.next_id = max(self.next_id, canonical_id + 1)
|
||||
state = _TrackState(
|
||||
canonical_id=canonical_id,
|
||||
source_ids={source_id},
|
||||
label=str(source["label"]),
|
||||
group=str(source.get("association_group", source["label"])),
|
||||
center=center.copy(),
|
||||
size=size.copy(),
|
||||
raw_center=center.copy(),
|
||||
velocity=np.zeros(2, dtype=np.float64),
|
||||
last_frame=frame_index,
|
||||
last_seconds=0.0,
|
||||
last_observed_frame=frame_index,
|
||||
score=float(source["score"]),
|
||||
template=json.loads(json.dumps(source)),
|
||||
)
|
||||
self.states[canonical_id] = state
|
||||
self.aliases[source_id] = canonical_id
|
||||
return state, False
|
||||
|
||||
def _observe(
|
||||
self,
|
||||
state: _TrackState,
|
||||
source: dict[str, Any],
|
||||
*,
|
||||
frame_index: int,
|
||||
session_seconds: float,
|
||||
stitched: bool,
|
||||
) -> dict[str, Any]:
|
||||
bbox = np.asarray(source["bbox_xyxy"], dtype=np.float64)
|
||||
observed_center, observed_size = _center_size(bbox)
|
||||
gap = max(1, frame_index - state.last_observed_frame)
|
||||
config = self.profile["tracking_2d"]
|
||||
predicted = state.center + state.velocity * gap
|
||||
scale = max(1.0, math.sqrt(float(np.prod(np.maximum(state.size, 1.0)))))
|
||||
innovation = float(np.linalg.norm(observed_center - predicted) / scale)
|
||||
if innovation > float(config["maximum_normalized_innovation"]):
|
||||
state.center = observed_center
|
||||
state.size = observed_size
|
||||
state.velocity.fill(0.0)
|
||||
status = "reset-large-innovation"
|
||||
self.reset_2d += 1
|
||||
else:
|
||||
blend = _adaptive(
|
||||
innovation,
|
||||
float(config["adaptive_innovation_low"]),
|
||||
float(config["adaptive_innovation_high"]),
|
||||
)
|
||||
center_alpha = _lerp(
|
||||
float(config["center_alpha_low"]),
|
||||
float(config["center_alpha_high"]),
|
||||
blend,
|
||||
)
|
||||
size_alpha = _lerp(
|
||||
float(config["size_alpha_low"]),
|
||||
float(config["size_alpha_high"]),
|
||||
blend,
|
||||
)
|
||||
state.center = predicted + center_alpha * (observed_center - predicted)
|
||||
state.size = state.size + size_alpha * (observed_size - state.size)
|
||||
observed_velocity = (observed_center - state.raw_center) / gap
|
||||
velocity_alpha = float(config["velocity_alpha"])
|
||||
state.velocity = (
|
||||
1.0 - velocity_alpha
|
||||
) * state.velocity + velocity_alpha * observed_velocity
|
||||
status = "stitched-observed" if stitched else "observed"
|
||||
state.raw_center = observed_center
|
||||
state.last_frame = frame_index
|
||||
state.last_observed_frame = frame_index
|
||||
state.last_seconds = session_seconds
|
||||
state.score = float(source["score"])
|
||||
state.template = json.loads(json.dumps(source))
|
||||
|
||||
normalized = json.loads(json.dumps(source))
|
||||
source_id = int(source["track_id"])
|
||||
normalized["track_id"] = state.canonical_id
|
||||
normalized["temporal_source_track_id"] = source_id
|
||||
normalized["bbox_xyxy"] = _box(state.center, state.size)
|
||||
normalized["temporal_2d_status"] = status
|
||||
self._observe_or_hold_cuboid(normalized, state, session_seconds)
|
||||
state.template = json.loads(json.dumps(normalized))
|
||||
return normalized
|
||||
|
||||
def _observe_or_hold_cuboid(
|
||||
self,
|
||||
item: dict[str, Any],
|
||||
state: _TrackState,
|
||||
session_seconds: float,
|
||||
) -> None:
|
||||
if str(item.get("cuboid_status", "")).startswith("accepted-"):
|
||||
self._observe_cuboid(item, state, session_seconds)
|
||||
else:
|
||||
self._hold_cuboid(item, state, session_seconds)
|
||||
|
||||
def _observe_cuboid(
|
||||
self,
|
||||
item: dict[str, Any],
|
||||
state: _TrackState,
|
||||
session_seconds: float,
|
||||
) -> None:
|
||||
center = np.asarray(item["cuboid_center_map"], dtype=np.float64)
|
||||
size = np.asarray(item["cuboid_half_size"], dtype=np.float64)
|
||||
yaw = _yaw(np.asarray(item["cuboid_quaternion_xyzw"], dtype=np.float64))
|
||||
config = self.profile["cuboids_3d"]
|
||||
status = "observed"
|
||||
if (
|
||||
state.cuboid_center is not None
|
||||
and state.cuboid_size is not None
|
||||
and state.cuboid_yaw is not None
|
||||
and state.cuboid_seconds is not None
|
||||
):
|
||||
dt = max(1e-3, session_seconds - state.cuboid_seconds)
|
||||
velocity = (
|
||||
np.zeros(3, dtype=np.float64)
|
||||
if state.cuboid_velocity is None
|
||||
else state.cuboid_velocity
|
||||
)
|
||||
predicted = state.cuboid_center + velocity * dt
|
||||
innovation = float(np.linalg.norm(center - predicted))
|
||||
yaw_delta = _yaw_delta(yaw, state.cuboid_yaw)
|
||||
if innovation > float(config["maximum_center_innovation_m"]) or abs(
|
||||
math.degrees(yaw_delta)
|
||||
) > float(config["maximum_yaw_innovation_degrees"]):
|
||||
status = "reset-large-innovation"
|
||||
self.reset_3d += 1
|
||||
velocity = np.zeros(3, dtype=np.float64)
|
||||
else:
|
||||
raw_velocity = (center - state.cuboid_center) / dt
|
||||
velocity_alpha = float(config["velocity_alpha"])
|
||||
velocity = (1.0 - velocity_alpha) * velocity + velocity_alpha * raw_velocity
|
||||
center = predicted + float(config["center_alpha"]) * (center - predicted)
|
||||
size = state.cuboid_size + float(config["size_alpha"]) * (size - state.cuboid_size)
|
||||
yaw = state.cuboid_yaw + float(config["yaw_alpha"]) * yaw_delta
|
||||
state.cuboid_velocity = velocity
|
||||
else:
|
||||
state.cuboid_velocity = np.zeros(3, dtype=np.float64)
|
||||
state.cuboid_center = center
|
||||
state.cuboid_size = size
|
||||
state.cuboid_yaw = yaw
|
||||
state.cuboid_seconds = session_seconds
|
||||
distance = item.get("distance_smoothed_m")
|
||||
if isinstance(distance, int | float) and not isinstance(distance, bool):
|
||||
state.distance_m = float(distance)
|
||||
item["cuboid_center_map"] = center.tolist()
|
||||
item["cuboid_half_size"] = size.tolist()
|
||||
item["cuboid_quaternion_xyzw"] = _quaternion(yaw)
|
||||
item["cuboid_status"] = "accepted-temporally-stabilized-e23-v1"
|
||||
item["temporal_status"] = f"e23-{status}"
|
||||
|
||||
def _hold_cuboid(
|
||||
self,
|
||||
item: dict[str, Any],
|
||||
state: _TrackState,
|
||||
session_seconds: float,
|
||||
) -> bool:
|
||||
if (
|
||||
state.cuboid_center is None
|
||||
or state.cuboid_size is None
|
||||
or state.cuboid_yaw is None
|
||||
or state.cuboid_seconds is None
|
||||
or session_seconds - state.cuboid_seconds
|
||||
> float(self.profile["cuboids_3d"]["hold_seconds"])
|
||||
):
|
||||
return False
|
||||
age_ms = max(0.0, (session_seconds - state.cuboid_seconds) * 1000.0)
|
||||
item["cuboid_center_map"] = state.cuboid_center.tolist()
|
||||
item["cuboid_half_size"] = state.cuboid_size.tolist()
|
||||
item["cuboid_quaternion_xyzw"] = _quaternion(state.cuboid_yaw)
|
||||
item["cuboid_status"] = "accepted-temporal-hold-e23-v1"
|
||||
item["geometry"] = "temporally-held-last-supported-cuboid"
|
||||
item["distance_smoothed_m"] = state.distance_m
|
||||
item["temporal_status"] = f"e23-held-{age_ms:.0f}ms"
|
||||
item["observed_cuboid_center_map"] = None
|
||||
item["observed_cuboid_half_size"] = None
|
||||
item["observed_cuboid_quaternion_xyzw"] = None
|
||||
self.held_3d += 1
|
||||
return True
|
||||
|
||||
def _prune(self, now: float) -> None:
|
||||
maximum_idle = float(self.profile["bounds"]["maximum_track_idle_seconds"])
|
||||
stale = [
|
||||
key
|
||||
for key, state in self.states.items()
|
||||
if state.last_seconds > 0 and now - state.last_seconds > maximum_idle
|
||||
]
|
||||
for key in stale:
|
||||
state = self.states.pop(key)
|
||||
for source_id in state.source_ids:
|
||||
self.aliases.pop(source_id, None)
|
||||
maximum = int(self.profile["bounds"]["maximum_track_states"])
|
||||
if len(self.states) <= maximum:
|
||||
return
|
||||
for state in sorted(self.states.values(), key=lambda value: value.last_seconds)[
|
||||
: len(self.states) - maximum
|
||||
]:
|
||||
self.states.pop(state.canonical_id, None)
|
||||
for source_id in state.source_ids:
|
||||
self.aliases.pop(source_id, None)
|
||||
|
||||
|
||||
class StreamingSemanticStabilizer:
|
||||
"""One-mask semantic hysteresis with bounded diagnostic samples."""
|
||||
|
||||
def __init__(self, profile: dict[str, Any]) -> None:
|
||||
self.minimum_same_label_neighbors = int(profile["semantic"]["minimum_same_label_neighbors"])
|
||||
self.previous_raw: np.ndarray | None = None
|
||||
self.previous_stabilized: np.ndarray | None = None
|
||||
self.baseline_unsupported = deque(maxlen=4096)
|
||||
self.stabilized_unsupported = deque(maxlen=4096)
|
||||
self.processing_ms = deque(maxlen=4096)
|
||||
self.frames = 0
|
||||
|
||||
def update(self, mask: np.ndarray) -> np.ndarray:
|
||||
if mask.dtype != np.uint8 or mask.shape != (600, 800):
|
||||
raise RuntimeError("LAB E23 semantic mask is invalid")
|
||||
started = time.perf_counter()
|
||||
if self.previous_raw is None or self.previous_stabilized is None:
|
||||
stabilized = mask.copy()
|
||||
else:
|
||||
support = _same_label_neighbor_count(mask)
|
||||
baseline_change = mask != self.previous_raw
|
||||
unsupported_baseline = baseline_change & (support < self.minimum_same_label_neighbors)
|
||||
stabilized = mask.copy()
|
||||
hold = (mask != self.previous_stabilized) & (
|
||||
support < self.minimum_same_label_neighbors
|
||||
)
|
||||
stabilized[hold] = self.previous_stabilized[hold]
|
||||
stabilized_support = _same_label_neighbor_count(stabilized)
|
||||
unsupported_stabilized = (stabilized != self.previous_stabilized) & (
|
||||
stabilized_support < self.minimum_same_label_neighbors
|
||||
)
|
||||
self.baseline_unsupported.append(float(np.mean(unsupported_baseline)))
|
||||
self.stabilized_unsupported.append(float(np.mean(unsupported_stabilized)))
|
||||
self.previous_raw = mask.copy()
|
||||
self.previous_stabilized = stabilized
|
||||
self.frames += 1
|
||||
self.processing_ms.append((time.perf_counter() - started) * 1000.0)
|
||||
return stabilized
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
baseline = _percentiles(self.baseline_unsupported)
|
||||
stabilized = _percentiles(self.stabilized_unsupported)
|
||||
baseline_mean = float(baseline["mean"])
|
||||
stabilized_mean = float(stabilized["mean"])
|
||||
reduction = (
|
||||
0.0
|
||||
if baseline_mean <= 0
|
||||
else max(0.0, (baseline_mean - stabilized_mean) / baseline_mean)
|
||||
)
|
||||
return {
|
||||
"frames": self.frames,
|
||||
"history_masks": int(self.previous_stabilized is not None),
|
||||
"diagnostic_sample_capacity": self.processing_ms.maxlen,
|
||||
"baseline_unsupported_change_fraction": baseline,
|
||||
"stabilized_unsupported_change_fraction": stabilized,
|
||||
"unsupported_change_reduction_fraction": reduction,
|
||||
"processing_ms": _percentiles(self.processing_ms),
|
||||
}
|
||||
|
||||
|
||||
def read_inline_profile(path: Path) -> tuple[dict[str, Any], str]:
|
||||
resolved = path.expanduser().resolve(strict=True)
|
||||
profile = json.loads(resolved.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(profile, dict):
|
||||
raise RuntimeError("LAB E23 inline temporal profile is invalid")
|
||||
source = profile.get("source")
|
||||
tracking = profile.get("tracking_2d")
|
||||
cuboids = profile.get("cuboids_3d")
|
||||
semantic = profile.get("semantic")
|
||||
bounds = profile.get("bounds")
|
||||
acceptance = profile.get("acceptance")
|
||||
authority = profile.get("authority")
|
||||
if (
|
||||
profile.get("schema_version") != PROFILE_SCHEMA
|
||||
or profile.get("mode") != "inline-shadow-qualification"
|
||||
or profile.get("stage") != "warm-worker-after-fusion-before-result-publication"
|
||||
or not all(
|
||||
isinstance(value, dict)
|
||||
for value in (
|
||||
source,
|
||||
tracking,
|
||||
cuboids,
|
||||
semantic,
|
||||
bounds,
|
||||
acceptance,
|
||||
)
|
||||
)
|
||||
or source.get("source_id") != "sensor.camera.right"
|
||||
or source.get("resolution") != [800, 600]
|
||||
or source.get("calibration_slot") != "camera_1"
|
||||
or semantic.get("mode") != "spatially-supported-streaming-hysteresis-v1"
|
||||
or int(semantic.get("class_count", 0)) != TARGET_CLASS_COUNT
|
||||
or authority != {"commands_enabled": False, "navigation_or_safety_accepted": False}
|
||||
or not 1 <= int(bounds.get("maximum_track_states", 0)) <= 512
|
||||
or int(bounds.get("semantic_history_masks", 0)) != 1
|
||||
):
|
||||
raise RuntimeError("LAB E23 inline temporal profile is invalid")
|
||||
for owner, keys in (
|
||||
(
|
||||
tracking,
|
||||
(
|
||||
"center_alpha_low",
|
||||
"center_alpha_high",
|
||||
"size_alpha_low",
|
||||
"size_alpha_high",
|
||||
"velocity_alpha",
|
||||
),
|
||||
),
|
||||
(cuboids, ("center_alpha", "size_alpha", "yaw_alpha", "velocity_alpha")),
|
||||
):
|
||||
if any(not 0 < float(owner.get(key, 0)) <= 1 for key in keys):
|
||||
raise RuntimeError("LAB E23 smoothing coefficient is invalid")
|
||||
if any(
|
||||
float(acceptance.get(key, 0)) <= 0
|
||||
for key in (
|
||||
"maximum_camera_frame_processing_p95_ms",
|
||||
"maximum_semantic_frame_processing_p95_ms",
|
||||
"maximum_rss_growth_mib",
|
||||
)
|
||||
):
|
||||
raise RuntimeError("LAB E23 acceptance contract is invalid")
|
||||
return profile, _sha256(resolved)
|
||||
|
||||
|
||||
def stabilize_world_state(
|
||||
source: dict[str, Any],
|
||||
fusion_objects: list[dict[str, Any]],
|
||||
memory: dict[int, dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
world = json.loads(json.dumps(source))
|
||||
source_objects = {
|
||||
int(item["track_id"]): item
|
||||
for item in source.get("objects", [])
|
||||
if isinstance(item, dict) and isinstance(item.get("track_id"), int)
|
||||
}
|
||||
objects: list[dict[str, Any]] = []
|
||||
active: set[int] = set()
|
||||
for fusion in fusion_objects:
|
||||
if not str(fusion.get("cuboid_status", "")).startswith("accepted-"):
|
||||
continue
|
||||
canonical = int(fusion["track_id"])
|
||||
source_id = int(fusion.get("temporal_source_track_id", canonical))
|
||||
template = source_objects.get(source_id) or memory.get(canonical) or {}
|
||||
item = json.loads(json.dumps(template))
|
||||
item.update(
|
||||
{
|
||||
"track_id": canonical,
|
||||
"class": str(fusion.get("association_group", "object")),
|
||||
"detector_label": str(fusion.get("label", "object")),
|
||||
"confidence": float(fusion.get("score", 0.0)),
|
||||
"position_map_m": fusion["cuboid_center_map"],
|
||||
"orientation_map_xyzw": fusion["cuboid_quaternion_xyzw"],
|
||||
"size_m": [2.0 * float(value) for value in fusion["cuboid_half_size"]],
|
||||
"range_m": fusion.get("distance_smoothed_m"),
|
||||
"support_points": int(fusion.get("clustered_points", 0)),
|
||||
"geometry": fusion.get("geometry"),
|
||||
"temporal_status": fusion.get("temporal_status"),
|
||||
}
|
||||
)
|
||||
if "held" in str(fusion.get("temporal_status", "")):
|
||||
item["velocity_status"] = "temporally-held-diagnostic"
|
||||
memory[canonical] = json.loads(json.dumps(item))
|
||||
active.add(canonical)
|
||||
objects.append(item)
|
||||
for canonical in set(memory) - active:
|
||||
memory.pop(canonical, None)
|
||||
world["objects"] = objects
|
||||
world["object_count"] = len(objects)
|
||||
world.setdefault("delivery", {})["temporal_stability"] = "e23-inline-bounded"
|
||||
return world
|
||||
|
||||
|
||||
def _same_label_neighbor_count(mask: np.ndarray) -> np.ndarray:
|
||||
padded = np.pad(mask, 1, mode="edge")
|
||||
count = np.zeros(mask.shape, dtype=np.uint8)
|
||||
for y in range(3):
|
||||
for x in range(3):
|
||||
if y == 1 and x == 1:
|
||||
continue
|
||||
count += padded[y : y + mask.shape[0], x : x + mask.shape[1]] == mask
|
||||
return count
|
||||
|
||||
|
||||
def _center_size(box: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
return (box[:2] + box[2:]) * 0.5, np.maximum(box[2:] - box[:2], 1.0)
|
||||
|
||||
|
||||
def _box(center: np.ndarray, size: np.ndarray) -> list[float]:
|
||||
half = np.maximum(size, 1.0) * 0.5
|
||||
values = np.concatenate((center - half, center + half))
|
||||
values[[0, 2]] = np.clip(values[[0, 2]], 0.0, 799.0)
|
||||
values[[1, 3]] = np.clip(values[[1, 3]], 0.0, 599.0)
|
||||
return [round(float(value), 6) for value in values]
|
||||
|
||||
|
||||
def _iou(left: np.ndarray, right: np.ndarray) -> float:
|
||||
intersection_min = np.maximum(left[:2], right[:2])
|
||||
intersection_max = np.minimum(left[2:], right[2:])
|
||||
intersection_size = np.maximum(0.0, intersection_max - intersection_min)
|
||||
intersection = float(np.prod(intersection_size))
|
||||
left_area = float(np.prod(np.maximum(0.0, left[2:] - left[:2])))
|
||||
right_area = float(np.prod(np.maximum(0.0, right[2:] - right[:2])))
|
||||
union = left_area + right_area - intersection
|
||||
return 0.0 if union <= 0 else intersection / union
|
||||
|
||||
|
||||
def _adaptive(value: float, lower: float, upper: float) -> float:
|
||||
if upper <= lower:
|
||||
return 1.0
|
||||
return min(1.0, max(0.0, (value - lower) / (upper - lower)))
|
||||
|
||||
|
||||
def _lerp(lower: float, upper: float, fraction: float) -> float:
|
||||
return lower + (upper - lower) * fraction
|
||||
|
||||
|
||||
def _yaw(quaternion: np.ndarray) -> float:
|
||||
x, y, z, w = quaternion
|
||||
return math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z))
|
||||
|
||||
|
||||
def _yaw_delta(value: float, reference: float) -> float:
|
||||
return math.atan2(math.sin(value - reference), math.cos(value - reference))
|
||||
|
||||
|
||||
def _quaternion(yaw: float) -> list[float]:
|
||||
return [0.0, 0.0, math.sin(yaw * 0.5), math.cos(yaw * 0.5)]
|
||||
|
||||
|
||||
def _clear_cuboid(item: dict[str, Any], status: str) -> None:
|
||||
for key in (
|
||||
"cuboid_center_map",
|
||||
"cuboid_half_size",
|
||||
"cuboid_quaternion_xyzw",
|
||||
"observed_cuboid_center_map",
|
||||
"observed_cuboid_half_size",
|
||||
"observed_cuboid_quaternion_xyzw",
|
||||
):
|
||||
item[key] = None
|
||||
item["cuboid_status"] = status
|
||||
item["temporal_status"] = status
|
||||
|
||||
|
||||
def _percentiles(values: Any) -> dict[str, float | int]:
|
||||
samples = np.asarray(list(values), dtype=np.float64)
|
||||
if samples.size == 0:
|
||||
return {"count": 0, "mean": 0.0, "p50": 0.0, "p95": 0.0, "max": 0.0}
|
||||
return {
|
||||
"count": int(samples.size),
|
||||
"mean": round(float(np.mean(samples)), 6),
|
||||
"p50": round(float(np.percentile(samples, 50)), 6),
|
||||
"p95": round(float(np.percentile(samples, 95)), 6),
|
||||
"max": round(float(np.max(samples)), 6),
|
||||
}
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
|
@ -22,6 +22,7 @@ from k1link.sessions import (
|
|||
publish_lab_replay_cache,
|
||||
)
|
||||
|
||||
from .inline_temporal import StreamingSemanticStabilizer, read_inline_profile
|
||||
from .integrated_perception import (
|
||||
IntegratedPerceptionResult,
|
||||
validate_integrated_perception_result,
|
||||
|
|
@ -29,6 +30,7 @@ from .integrated_perception import (
|
|||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||
from .temporal_stability import (
|
||||
TemporalStabilityBuild,
|
||||
_quality_metrics,
|
||||
build_temporal_stability_result,
|
||||
)
|
||||
|
||||
|
|
@ -224,9 +226,7 @@ def publish_e21_lab_instance(
|
|||
source_result_id=str(e21_document["result_id"]),
|
||||
config_sha256=str(e21_report["identity"]["profile_sha256"]),
|
||||
run_created_at_utc=str(e21_report["created_at_utc"]),
|
||||
duration_seconds=(
|
||||
validated.timeline_end_seconds - validated.timeline_start_seconds
|
||||
),
|
||||
duration_seconds=(validated.timeline_end_seconds - validated.timeline_start_seconds),
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e21-lab-publication/v1",
|
||||
|
|
@ -297,20 +297,14 @@ def publish_e22_lab_instance(
|
|||
)
|
||||
if not validated.accepted:
|
||||
failed = [
|
||||
name
|
||||
for name, accepted in build.report["acceptance"]["checks"].items()
|
||||
if not accepted
|
||||
name for name, accepted in build.report["acceptance"]["checks"].items() if not accepted
|
||||
]
|
||||
raise SessionIntegrityError(
|
||||
f"E22 temporal acceptance failed: {', '.join(failed)}"
|
||||
)
|
||||
raise SessionIntegrityError(f"E22 temporal acceptance failed: {', '.join(failed)}")
|
||||
|
||||
store = SessionStore(root)
|
||||
source_lab = store.get_lab_instance(source.job.session_id)
|
||||
source_session_id = (
|
||||
source.job.session_id
|
||||
if source_lab is None
|
||||
else source_lab.source_session_id
|
||||
source.job.session_id if source_lab is None else source_lab.source_session_id
|
||||
)
|
||||
publish_lab_replay_cache(
|
||||
store.data_dir,
|
||||
|
|
@ -330,26 +324,22 @@ def publish_e22_lab_instance(
|
|||
source_result_id=source.result_id,
|
||||
config_sha256=build.profile_sha256,
|
||||
run_created_at_utc=validated.created_at_utc,
|
||||
duration_seconds=(
|
||||
validated.timeline_end_seconds - validated.timeline_start_seconds
|
||||
),
|
||||
duration_seconds=(validated.timeline_end_seconds - validated.timeline_start_seconds),
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e22-lab-publication/v1",
|
||||
"storage_mode": "bounded-derived-replay-and-temporal-projection",
|
||||
"source_result_id": source.result_id,
|
||||
"source_lab_session_id": (
|
||||
None if source_lab is None else source_lab.session_id
|
||||
),
|
||||
"source_lab_session_id": (None if source_lab is None else source_lab.session_id),
|
||||
"source_payloads_mutated": False,
|
||||
"lookahead_frames": 0,
|
||||
"peak_track_states": metrics["runtime"]["peak_track_states"],
|
||||
"camera_frame_processing_p95_ms": metrics["runtime"][
|
||||
"camera_frame_processing_ms"
|
||||
]["p95"],
|
||||
"semantic_frame_processing_p95_ms": metrics["runtime"][
|
||||
"semantic_frame_processing_ms"
|
||||
]["p95"],
|
||||
"camera_frame_processing_p95_ms": metrics["runtime"]["camera_frame_processing_ms"][
|
||||
"p95"
|
||||
],
|
||||
"semantic_frame_processing_p95_ms": metrics["runtime"]["semantic_frame_processing_ms"][
|
||||
"p95"
|
||||
],
|
||||
"quality_reductions": metrics["reductions"],
|
||||
},
|
||||
)
|
||||
|
|
@ -361,6 +351,210 @@ def publish_e22_lab_instance(
|
|||
)
|
||||
|
||||
|
||||
def publish_e23_lab_instance(
|
||||
*,
|
||||
repository_root: Path,
|
||||
reference_result_root: Path,
|
||||
worker_result_root: Path,
|
||||
source_report_path: Path,
|
||||
profile_path: Path,
|
||||
lab_session_id: str,
|
||||
lab_id: str,
|
||||
display_name: str,
|
||||
) -> PublishedIntegratedLabInstance:
|
||||
"""Publish one accepted inline-temporal 1x worker run as an exact LAB replay."""
|
||||
|
||||
root = repository_root.expanduser().resolve(strict=True)
|
||||
jobs_root = root / ".runtime" / "compute-jobs"
|
||||
results_root = root / ".runtime" / "compute-experiments" / "e10" / "worker-results"
|
||||
packs_root = root / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
|
||||
reference_path = reference_result_root.expanduser().resolve(strict=True)
|
||||
reference_document = _read_object(reference_path / "result.json", reference_path)
|
||||
reference_identity = reference_document.get("identity")
|
||||
if not isinstance(reference_identity, dict) or not isinstance(
|
||||
reference_identity.get("job_id"), str
|
||||
):
|
||||
raise SessionIntegrityError("E23 semantic reference has no job identity")
|
||||
reference = validate_integrated_perception_result(
|
||||
jobs_root / reference_identity["job_id"],
|
||||
reference_path,
|
||||
packs_root,
|
||||
)
|
||||
if not reference.accepted or reference.source_start_frame_index != 0:
|
||||
raise SessionIntegrityError("E23 reference is not an accepted zero-based run")
|
||||
|
||||
profile, profile_sha256 = read_inline_profile(profile_path)
|
||||
worker_root = worker_result_root.expanduser().resolve(strict=True)
|
||||
source_path = source_report_path.expanduser().resolve(strict=True)
|
||||
worker_document, worker_report, source_report = _validate_e23_inputs(
|
||||
worker_root,
|
||||
source_path,
|
||||
profile_sha256,
|
||||
)
|
||||
frame_count = int(source_report["events_selected"]["camera-frame"])
|
||||
if frame_count != reference.frame_count:
|
||||
raise SessionIntegrityError("E23 source and reference frame counts differ")
|
||||
|
||||
lab_job = _publish_lab_job(reference.job, jobs_root, lab_session_id)
|
||||
lab_pack = _publish_e21_pack(
|
||||
reference,
|
||||
lab_job,
|
||||
packs_root,
|
||||
lab_session_id,
|
||||
frame_count,
|
||||
visual_projection="accepted-e23-inline-envelope/v1",
|
||||
)
|
||||
lab_result, quality = _publish_e23_visual_result(
|
||||
reference=reference,
|
||||
lab_job=lab_job,
|
||||
lab_pack=lab_pack,
|
||||
results_root=results_root,
|
||||
lab_session_id=lab_session_id,
|
||||
frame_count=frame_count,
|
||||
worker_root=worker_root,
|
||||
worker_document=worker_document,
|
||||
worker_report=worker_report,
|
||||
source_report=source_report,
|
||||
profile=profile,
|
||||
profile_sha256=profile_sha256,
|
||||
)
|
||||
validated = validate_integrated_perception_result(
|
||||
lab_job.job_root,
|
||||
lab_result,
|
||||
packs_root,
|
||||
)
|
||||
if not validated.accepted or not all(quality["checks"].values()):
|
||||
failed = [name for name, accepted in quality["checks"].items() if not accepted]
|
||||
raise SessionIntegrityError(f"E23 inline temporal acceptance failed: {', '.join(failed)}")
|
||||
|
||||
store = SessionStore(root)
|
||||
source_lab = store.get_lab_instance(reference.job.session_id)
|
||||
source_session_id = (
|
||||
reference.job.session_id if source_lab is None else source_lab.source_session_id
|
||||
)
|
||||
publish_lab_replay_cache(
|
||||
store.data_dir,
|
||||
source_session_id=source_session_id,
|
||||
lab_session_id=lab_session_id,
|
||||
timeline_start_ns=round(validated.timeline_start_seconds * 1_000_000_000),
|
||||
timeline_end_ns=round(validated.timeline_end_seconds * 1_000_000_000),
|
||||
)
|
||||
temporal = worker_report["metrics"]["temporal_stability"]
|
||||
binding = store.publish_lab_instance(
|
||||
session_id=lab_session_id,
|
||||
source_session_id=source_session_id,
|
||||
display_name=display_name,
|
||||
lab_id=lab_id,
|
||||
result_kind="e23-inline-temporal-stability",
|
||||
result_id=validated.result_id,
|
||||
source_result_id=str(worker_document["result_id"]),
|
||||
config_sha256=profile_sha256,
|
||||
run_created_at_utc=str(worker_report["created_at_utc"]),
|
||||
duration_seconds=(validated.timeline_end_seconds - validated.timeline_start_seconds),
|
||||
include_recorded_media=False,
|
||||
provenance={
|
||||
"schema_version": "missioncore.e23-lab-publication/v1",
|
||||
"storage_mode": "bounded-inline-worker-result-and-immutable-source-replay",
|
||||
"worker_result_id": worker_document["result_id"],
|
||||
"source_report_sha256": _sha256(source_path),
|
||||
"reference_result_id": reference.result_id,
|
||||
"source_payloads_mutated": False,
|
||||
"lookahead_frames": 0,
|
||||
"speed": 1.0,
|
||||
"quality_reductions": quality["reductions"],
|
||||
"temporal_2d_3d_p95_ms": worker_report["metrics"]["latency_ms"]["temporal_2d_3d_ms"][
|
||||
"p95"
|
||||
],
|
||||
"semantic_temporal_p95_ms": temporal["semantic"]["processing_ms"]["p95"],
|
||||
"peak_track_states": temporal["tracking_2d_3d"]["peak_track_states"],
|
||||
"rss_growth_mib": worker_report["metrics"]["runtime_telemetry"]["rss_growth_mib"],
|
||||
},
|
||||
)
|
||||
return PublishedIntegratedLabInstance(
|
||||
binding=binding,
|
||||
job=lab_job,
|
||||
result=validated,
|
||||
)
|
||||
|
||||
|
||||
def _validate_e23_inputs(
|
||||
worker_root: Path,
|
||||
source_report_path: Path,
|
||||
profile_sha256: str,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||||
worker_document = _read_object(worker_root / "result.json", worker_root)
|
||||
worker_report = _read_object(worker_root / "run-report.json", worker_root)
|
||||
source_report = _read_object(source_report_path, source_report_path.parent)
|
||||
worker_identity = worker_document.get("identity")
|
||||
report_identity = worker_report.get("identity")
|
||||
if (
|
||||
worker_document.get("schema_version") != "missioncore.e15-shadow-inference-result/v1"
|
||||
or worker_document.get("result_id") != worker_root.name
|
||||
or worker_document.get("acceptance_state") != "accepted"
|
||||
or worker_document.get("publication_scope") != "live-shadow-diagnostic-only"
|
||||
or not isinstance(worker_identity, dict)
|
||||
or worker_identity.get("pipeline")
|
||||
!= "warm-worker-inline-bounded-temporal-2d-3d-semantic/v1"
|
||||
or worker_identity.get("profiles", {}).get("stability_sha256") != profile_sha256
|
||||
or worker_report.get("schema_version") != "missioncore.e15-shadow-inference-report/v1"
|
||||
or worker_report.get("result_id") != worker_root.name
|
||||
or worker_report.get("state") != "accepted"
|
||||
or report_identity != worker_identity
|
||||
or not all(worker_report.get("acceptance", {}).get("checks", {}).values())
|
||||
or source_report.get("schema_version") != "missioncore.e23-replay-source-report/v1"
|
||||
or source_report.get("state") != "completed"
|
||||
or source_report.get("session_id") != worker_identity.get("session_id")
|
||||
or source_report.get("source", {}).get("speed") != 1.0
|
||||
or source_report.get("authority", {}).get("mode") != "shadow-diagnostic-only"
|
||||
or source_report.get("authority", {}).get("commands_enabled") is not False
|
||||
or source_report.get("authority", {}).get("navigation_or_safety_accepted") is not False
|
||||
):
|
||||
raise SessionIntegrityError("E23 accepted worker/source identity is inconsistent")
|
||||
selected = source_report.get("events_selected")
|
||||
diagnostics = source_report.get("diagnostic_results")
|
||||
if (
|
||||
not isinstance(selected, dict)
|
||||
or selected.get("camera-frame") != 601
|
||||
or selected.get("lidar") != 585
|
||||
or selected.get("pose") != 600
|
||||
or not isinstance(diagnostics, dict)
|
||||
or int(diagnostics.get("received", 0)) < 590
|
||||
):
|
||||
raise SessionIntegrityError("E23 source replay coverage is incomplete")
|
||||
|
||||
required = {
|
||||
"e15-semantic-frames": "semantic-frames.jsonl",
|
||||
"e23-raw-fusion-frames": "raw-fusion-frames.jsonl",
|
||||
"e15-fusion-frames": "fusion-frames.jsonl",
|
||||
"e15-world-state": "world-state.jsonl",
|
||||
"worker-gpu-telemetry": "gpu-telemetry.jsonl",
|
||||
"worker-runtime-telemetry": "runtime-telemetry.jsonl",
|
||||
"e15-run-report": "run-report.json",
|
||||
}
|
||||
artifacts = worker_document.get("artifacts")
|
||||
descriptors = (
|
||||
{
|
||||
value.get("kind"): value
|
||||
for value in artifacts
|
||||
if isinstance(value, dict) and value.get("kind") in required
|
||||
}
|
||||
if isinstance(artifacts, list)
|
||||
else {}
|
||||
)
|
||||
if set(descriptors) != set(required):
|
||||
raise SessionIntegrityError("E23 worker artifacts are incomplete")
|
||||
for kind, name in required.items():
|
||||
descriptor = descriptors[kind]
|
||||
path = worker_root / name
|
||||
if (
|
||||
descriptor.get("path") != name
|
||||
or descriptor.get("byte_length") != path.stat().st_size
|
||||
or descriptor.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise SessionIntegrityError("E23 worker artifact identity changed")
|
||||
return worker_document, worker_report, source_report
|
||||
|
||||
|
||||
def _validate_e21_inputs(
|
||||
e21_root: Path,
|
||||
worker_root: Path,
|
||||
|
|
@ -426,6 +620,8 @@ def _publish_e21_pack(
|
|||
packs_root: Path,
|
||||
lab_session_id: str,
|
||||
frame_count: int,
|
||||
*,
|
||||
visual_projection: str = "accepted-e21-envelope/v1",
|
||||
) -> Path:
|
||||
source_manifest = _read_object(reference.pack_root / "manifest.json", reference.pack_root)
|
||||
with np.load(reference.pack_root / "lidar-pack.npz", allow_pickle=False) as arrays:
|
||||
|
|
@ -438,9 +634,9 @@ def _publish_e21_pack(
|
|||
"cloud_offsets": arrays["cloud_offsets"][: frame_count + 1].copy(),
|
||||
"cloud_points_map": arrays["cloud_points_map"][:cloud_end].copy(),
|
||||
"pose_positions_map": arrays["pose_positions_map"][:frame_count].copy(),
|
||||
"pose_quaternions_map_from_lidar": arrays[
|
||||
"pose_quaternions_map_from_lidar"
|
||||
][:frame_count].copy(),
|
||||
"pose_quaternions_map_from_lidar": arrays["pose_quaternions_map_from_lidar"][
|
||||
:frame_count
|
||||
].copy(),
|
||||
"lidar_camera_delta_ms": arrays["lidar_camera_delta_ms"][:frame_count].copy(),
|
||||
"pose_point_delta_ms": arrays["pose_point_delta_ms"][:frame_count].copy(),
|
||||
"intrinsic_fx_fy_cx_cy": arrays["intrinsic_fx_fy_cx_cy"].copy(),
|
||||
|
|
@ -460,7 +656,7 @@ def _publish_e21_pack(
|
|||
"point_count": int(payload["cloud_points_map"].shape[0]),
|
||||
"timeline_start_seconds": float(payload["session_seconds"][0]),
|
||||
"timeline_end_seconds": float(payload["session_seconds"][-1]),
|
||||
"visual_projection": "accepted-e21-envelope/v1",
|
||||
"visual_projection": visual_projection,
|
||||
}
|
||||
)
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
|
|
@ -580,9 +776,7 @@ def _publish_e21_visual_result(
|
|||
)
|
||||
fusion_rows.append(normalized_fusion)
|
||||
world_rows.append(normalized_world)
|
||||
expected_drops = int(
|
||||
e21_report["metrics"]["worker"]["detector"]["queue"]["dropped_overflow"]
|
||||
)
|
||||
expected_drops = int(e21_report["metrics"]["worker"]["detector"]["queue"]["dropped_overflow"])
|
||||
if len(dropped_indices) != expected_drops:
|
||||
raise SessionIntegrityError("E21 detector replacement accounting changed")
|
||||
|
||||
|
|
@ -762,6 +956,329 @@ def _publish_e21_visual_result(
|
|||
return destination
|
||||
|
||||
|
||||
def _publish_e23_visual_result(
|
||||
*,
|
||||
reference: IntegratedPerceptionResult,
|
||||
lab_job: CameraComputeJob,
|
||||
lab_pack: Path,
|
||||
results_root: Path,
|
||||
lab_session_id: str,
|
||||
frame_count: int,
|
||||
worker_root: Path,
|
||||
worker_document: dict[str, Any],
|
||||
worker_report: dict[str, Any],
|
||||
source_report: dict[str, Any],
|
||||
profile: dict[str, Any],
|
||||
profile_sha256: str,
|
||||
) -> tuple[Path, dict[str, Any]]:
|
||||
with np.load(reference.arrays_path, allow_pickle=False) as arrays:
|
||||
frame_times = arrays["frame_times_ns"][:frame_count].copy()
|
||||
reference_semantic_indices = arrays["semantic_frame_indices"]
|
||||
selected = reference_semantic_indices < frame_count
|
||||
reference_indices = reference_semantic_indices[selected].copy()
|
||||
reference_masks = arrays["semantic_masks"][selected].copy()
|
||||
|
||||
semantic_rows = _read_jsonl(worker_root / "semantic-frames.jsonl")
|
||||
if [row.get("frame_index") for row in semantic_rows] != reference_indices.tolist():
|
||||
raise SessionIntegrityError("E23 semantic frame schedule changed")
|
||||
semantic_stabilizer = StreamingSemanticStabilizer(profile)
|
||||
stabilized_masks = np.stack(
|
||||
[semantic_stabilizer.update(mask) for mask in reference_masks]
|
||||
).astype(np.uint8, copy=False)
|
||||
for row, mask in zip(semantic_rows, stabilized_masks, strict=True):
|
||||
if row.get("mask_sha256") != hashlib.sha256(mask.tobytes()).hexdigest():
|
||||
raise SessionIntegrityError("E23 semantic mask does not match inline reconstruction")
|
||||
row["schema_version"] = "missioncore.e10-semantic-frame/v1"
|
||||
row["session_seconds"] = float(frame_times[int(row["frame_index"])]) / 1_000_000_000
|
||||
row["temporal_status"] = "e23-inline-spatially-supported-hysteresis"
|
||||
|
||||
raw_worker_rows = _read_jsonl(worker_root / "raw-fusion-frames.jsonl")
|
||||
stable_worker_rows = _read_jsonl(worker_root / "fusion-frames.jsonl")
|
||||
fusion_source = {int(row["source_frame_index"]): row for row in stable_worker_rows}
|
||||
world_source = {
|
||||
int(row["source_frame_index"]): row
|
||||
for row in _read_jsonl(worker_root / "world-state.jsonl")
|
||||
}
|
||||
if set(fusion_source) != set(world_source):
|
||||
raise SessionIntegrityError("E23 fusion and world timelines differ")
|
||||
fusion_rows: list[dict[str, Any]] = []
|
||||
world_rows: list[dict[str, Any]] = []
|
||||
dropped_indices: list[int] = []
|
||||
for index in range(frame_count):
|
||||
session_seconds = float(frame_times[index]) / 1_000_000_000
|
||||
fusion = fusion_source.get(index)
|
||||
world = world_source.get(index)
|
||||
if fusion is None or world is None:
|
||||
dropped_indices.append(index)
|
||||
fusion_rows.append(
|
||||
{
|
||||
"schema_version": "missioncore.e10-fusion-frame/v1",
|
||||
"frame_index": index,
|
||||
"source_frame_index": index,
|
||||
"session_seconds": session_seconds,
|
||||
"fusion_state": "detector-dropped-latest-wins",
|
||||
"semantic_source_frame_index": None,
|
||||
"semantic_status": "unavailable",
|
||||
"objects": [],
|
||||
}
|
||||
)
|
||||
world_rows.append(_dropped_world_row(index, session_seconds))
|
||||
continue
|
||||
normalized_fusion = json.loads(json.dumps(fusion))
|
||||
normalized_fusion.update(
|
||||
{
|
||||
"schema_version": "missioncore.e10-fusion-frame/v1",
|
||||
"frame_index": index,
|
||||
"source_frame_index": index,
|
||||
"session_seconds": session_seconds,
|
||||
}
|
||||
)
|
||||
normalized_world = json.loads(json.dumps(world))
|
||||
normalized_world.update(
|
||||
{
|
||||
"frame_index": index,
|
||||
"source_frame_index": index,
|
||||
"session_seconds": session_seconds,
|
||||
}
|
||||
)
|
||||
fusion_rows.append(normalized_fusion)
|
||||
world_rows.append(normalized_world)
|
||||
expected_drops = int(worker_report["metrics"]["detector"]["queue"]["dropped_overflow"])
|
||||
if len(dropped_indices) != expected_drops:
|
||||
raise SessionIntegrityError("E23 detector replacement accounting changed")
|
||||
|
||||
baseline = _quality_metrics(raw_worker_rows, reference_masks)
|
||||
stabilized = _quality_metrics(stable_worker_rows, stabilized_masks)
|
||||
reductions = {
|
||||
"tracking_2d_acceleration_p95_fraction": _fraction_reduction(
|
||||
baseline["tracking_2d"]["normalized_acceleration"]["p95"],
|
||||
stabilized["tracking_2d"]["normalized_acceleration"]["p95"],
|
||||
),
|
||||
"tracking_2d_size_step_p95_fraction": _fraction_reduction(
|
||||
baseline["tracking_2d"]["normalized_size_step"]["p95"],
|
||||
stabilized["tracking_2d"]["normalized_size_step"]["p95"],
|
||||
),
|
||||
"cuboid_center_step_p95_fraction": _fraction_reduction(
|
||||
baseline["cuboids_3d"]["center_step_m"]["p95"],
|
||||
stabilized["cuboids_3d"]["center_step_m"]["p95"],
|
||||
),
|
||||
"cuboid_size_step_p95_fraction": _fraction_reduction(
|
||||
baseline["cuboids_3d"]["half_size_step_m"]["p95"],
|
||||
stabilized["cuboids_3d"]["half_size_step_m"]["p95"],
|
||||
),
|
||||
"cuboid_yaw_step_p95_fraction": _fraction_reduction(
|
||||
baseline["cuboids_3d"]["yaw_step_degrees"]["p95"],
|
||||
stabilized["cuboids_3d"]["yaw_step_degrees"]["p95"],
|
||||
),
|
||||
"semantic_unsupported_change_fraction": float(
|
||||
worker_report["metrics"]["temporal_stability"]["semantic"][
|
||||
"unsupported_change_reduction_fraction"
|
||||
]
|
||||
),
|
||||
}
|
||||
temporal = worker_report["metrics"]["temporal_stability"]
|
||||
acceptance = profile["acceptance"]
|
||||
quality_checks = {
|
||||
"worker_runtime_accepted": worker_report["state"] == "accepted"
|
||||
and all(worker_report["acceptance"]["checks"].values()),
|
||||
"source_is_complete_1x": source_report["state"] == "completed"
|
||||
and source_report["source"]["speed"] == 1.0,
|
||||
"minimum_2d_acceleration_reduction": reductions["tracking_2d_acceleration_p95_fraction"]
|
||||
>= float(acceptance["minimum_2d_acceleration_p95_reduction_fraction"]),
|
||||
"minimum_3d_center_reduction": reductions["cuboid_center_step_p95_fraction"]
|
||||
>= float(acceptance["minimum_3d_center_step_p95_reduction_fraction"]),
|
||||
"minimum_3d_yaw_reduction": reductions["cuboid_yaw_step_p95_fraction"]
|
||||
>= float(acceptance["minimum_3d_yaw_step_p95_reduction_fraction"]),
|
||||
"minimum_semantic_unsupported_change_reduction": reductions[
|
||||
"semantic_unsupported_change_fraction"
|
||||
]
|
||||
>= float(acceptance["minimum_semantic_unsupported_change_reduction_fraction"]),
|
||||
"maximum_camera_frame_processing_p95": float(
|
||||
worker_report["metrics"]["latency_ms"]["temporal_2d_3d_ms"]["p95"]
|
||||
)
|
||||
<= float(acceptance["maximum_camera_frame_processing_p95_ms"]),
|
||||
"maximum_semantic_frame_processing_p95": float(temporal["semantic"]["processing_ms"]["p95"])
|
||||
<= float(acceptance["maximum_semantic_frame_processing_p95_ms"]),
|
||||
"maximum_track_states": int(temporal["tracking_2d_3d"]["peak_track_states"])
|
||||
<= int(acceptance["maximum_track_states_observed"]),
|
||||
"maximum_rss_growth": float(worker_report["metrics"]["runtime_telemetry"]["rss_growth_mib"])
|
||||
<= float(acceptance["maximum_rss_growth_mib"]),
|
||||
}
|
||||
quality = {
|
||||
"schema_version": "missioncore.e23-inline-quality/v1",
|
||||
"baseline": baseline,
|
||||
"stabilized": stabilized,
|
||||
"reductions": reductions,
|
||||
"checks": quality_checks,
|
||||
}
|
||||
if not all(quality_checks.values()):
|
||||
failed = [name for name, accepted in quality_checks.items() if not accepted]
|
||||
raise SessionIntegrityError(f"E23 inline temporal quality failed: {', '.join(failed)}")
|
||||
|
||||
box_offsets = [0]
|
||||
centers: list[list[float]] = []
|
||||
half_sizes: list[list[float]] = []
|
||||
quaternions: list[list[float]] = []
|
||||
colors: list[list[int]] = []
|
||||
for row in fusion_rows:
|
||||
for item in row["objects"]:
|
||||
if not str(item.get("cuboid_status", "")).startswith("accepted-"):
|
||||
continue
|
||||
centers.append(item["cuboid_center_map"])
|
||||
half_sizes.append(item["cuboid_half_size"])
|
||||
quaternions.append(item["cuboid_quaternion_xyzw"])
|
||||
colors.append(_cuboid_color(item))
|
||||
box_offsets.append(len(centers))
|
||||
|
||||
worker_identity = worker_document["identity"]
|
||||
configuration = {
|
||||
"pipeline": "e23-inline-temporal-envelope-visual-projection/v1",
|
||||
"profile_sha256": profile_sha256,
|
||||
"profile": profile,
|
||||
"worker_result_id": worker_document["result_id"],
|
||||
"source_report": {
|
||||
"schema_version": source_report["schema_version"],
|
||||
"session_id": source_report["session_id"],
|
||||
"speed": source_report["source"]["speed"],
|
||||
},
|
||||
"semantic_mask_materialization": {
|
||||
"mode": "inline-reconstruction-from-immutable-reference-exact-sha256",
|
||||
"reference_result_id": reference.result_id,
|
||||
"matched_masks": len(semantic_rows),
|
||||
},
|
||||
"quality": quality,
|
||||
}
|
||||
selection = {
|
||||
"frame_count": frame_count,
|
||||
"source_start_frame_index": 0,
|
||||
"source_end_frame_index": frame_count - 1,
|
||||
"timeline_start_seconds": float(frame_times[0]) / 1_000_000_000,
|
||||
"timeline_end_seconds": float(frame_times[-1]) / 1_000_000_000,
|
||||
"timeline_sha256": hashlib.sha256(frame_times.tobytes()).hexdigest(),
|
||||
}
|
||||
identity = {
|
||||
"schema_version": "missioncore.e10-integrated-perception-identity/v1",
|
||||
"job_id": lab_job.job_id,
|
||||
"input_sha256": lab_job.input_sha256,
|
||||
"session_id": lab_session_id,
|
||||
"source_id": lab_job.source_id,
|
||||
"lidar_pack_id": lab_pack.name,
|
||||
"selection": selection,
|
||||
"configuration": configuration,
|
||||
"models": worker_identity["models"],
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e10-integrated-perception-{identity_sha256}"
|
||||
destination = results_root / result_id
|
||||
if destination.exists():
|
||||
existing = _read_object(destination / "result.json", destination)
|
||||
if existing.get("identity") != identity:
|
||||
raise SessionIntegrityError("E23 LAB visual result id collides")
|
||||
return destination, quality
|
||||
|
||||
staging = _staging_directory(results_root, result_id)
|
||||
try:
|
||||
semantic_path = staging / "semantic-frames.jsonl"
|
||||
fusion_path = staging / "fusion-frames.jsonl"
|
||||
world_path = staging / "world-state.jsonl"
|
||||
arrays_path = staging / "transient-perception.npz"
|
||||
gpu_path = staging / "gpu-telemetry.jsonl"
|
||||
report_path = staging / "run-report.json"
|
||||
_write_jsonl(semantic_path, semantic_rows)
|
||||
_write_jsonl(fusion_path, fusion_rows)
|
||||
_write_jsonl(world_path, world_rows)
|
||||
np.savez_compressed(
|
||||
arrays_path,
|
||||
frame_times_ns=frame_times.astype(np.int64, copy=False),
|
||||
semantic_frame_indices=reference_indices.astype(np.int64, copy=False),
|
||||
semantic_masks=stabilized_masks.astype(np.uint8, copy=False),
|
||||
support_offsets=np.zeros(frame_count + 1, dtype=np.int64),
|
||||
support_points=np.empty((0, 3), dtype=np.float32),
|
||||
support_colors=np.empty((0, 3), dtype=np.uint8),
|
||||
box_offsets=np.asarray(box_offsets, dtype=np.int64),
|
||||
box_centers=np.asarray(centers, dtype=np.float32).reshape((-1, 3)),
|
||||
box_half_sizes=np.asarray(half_sizes, dtype=np.float32).reshape((-1, 3)),
|
||||
box_quaternions=np.asarray(quaternions, dtype=np.float32).reshape((-1, 4)),
|
||||
box_colors=np.asarray(colors, dtype=np.uint8).reshape((-1, 4)),
|
||||
)
|
||||
shutil.copyfile(worker_root / "gpu-telemetry.jsonl", gpu_path)
|
||||
report = {
|
||||
"schema_version": "missioncore.e10-integrated-perception-report/v1",
|
||||
"result_id": result_id,
|
||||
"created_at_utc": worker_report["created_at_utc"],
|
||||
"state": "accepted",
|
||||
"ground_truth": False,
|
||||
"identity": identity,
|
||||
"acceptance": {
|
||||
"accepted": True,
|
||||
"navigation_or_safety_accepted": False,
|
||||
"checks": quality_checks,
|
||||
},
|
||||
"metrics": {
|
||||
**worker_report["metrics"],
|
||||
"quality": quality,
|
||||
"visual_projection": {
|
||||
"frames": frame_count,
|
||||
"semantic_masks": len(semantic_rows),
|
||||
"detector_replacement_frames": dropped_indices,
|
||||
"accepted_cuboids": len(centers),
|
||||
},
|
||||
},
|
||||
"runtime": worker_report.get("runtime", {}),
|
||||
"limitations": [
|
||||
"This is the accepted E23 recorded 1x inline worker gate, not a physical K1 run.",
|
||||
"Latest-wins detector replacements are explicit empty visual frames.",
|
||||
"Semantic pixels are reconstructed only after exact inline SHA-256 matches.",
|
||||
"LiDAR support points remain in the immutable source scene and are not duplicated.",
|
||||
"Navigation and safety authority remain disabled.",
|
||||
],
|
||||
}
|
||||
write_json_atomic(report_path, report)
|
||||
artifacts = [
|
||||
_artifact_descriptor(
|
||||
"e10-semantic-frames",
|
||||
semantic_path,
|
||||
"missioncore.e10-semantic-frame/v1",
|
||||
),
|
||||
_artifact_descriptor(
|
||||
"e10-fusion-frames",
|
||||
fusion_path,
|
||||
"missioncore.e10-fusion-frame/v1",
|
||||
),
|
||||
_artifact_descriptor(
|
||||
"e10-world-state",
|
||||
world_path,
|
||||
"missioncore.live-perception-world-state/v1",
|
||||
),
|
||||
_artifact_descriptor("e10-transient-perception", arrays_path, None),
|
||||
_artifact_descriptor("worker-gpu-telemetry", gpu_path, None),
|
||||
_artifact_descriptor(
|
||||
"e10-run-report",
|
||||
report_path,
|
||||
"missioncore.e10-integrated-perception-report/v1",
|
||||
),
|
||||
]
|
||||
write_json_atomic(
|
||||
staging / "result.json",
|
||||
{
|
||||
"schema_version": "missioncore.e10-integrated-perception-result/v1",
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": worker_report["created_at_utc"],
|
||||
"ground_truth": False,
|
||||
"publication_scope": "recorded-integrated-realtime-qualification-only",
|
||||
"acceptance_state": "accepted",
|
||||
"frames_processed": frame_count,
|
||||
"artifacts": artifacts,
|
||||
},
|
||||
)
|
||||
_publish_directory(staging, destination)
|
||||
finally:
|
||||
_remove_staging(staging)
|
||||
return destination, quality
|
||||
|
||||
|
||||
def _dropped_world_row(frame_index: int, session_seconds: float) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "missioncore.live-perception-world-state/v1",
|
||||
|
|
@ -799,6 +1316,13 @@ def _cuboid_color(item: dict[str, Any]) -> list[int]:
|
|||
return [64 + digest[0] % 176, 64 + digest[1] % 176, 64 + digest[2] % 176, 88]
|
||||
|
||||
|
||||
def _fraction_reduction(baseline: int | float, stabilized: int | float) -> float:
|
||||
baseline_value = float(baseline)
|
||||
if baseline_value <= 0:
|
||||
return 0.0
|
||||
return (baseline_value - float(stabilized)) / baseline_value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from k1link.compute import (
|
|||
prepare_camera_compute_job,
|
||||
publish_e21_lab_instance,
|
||||
publish_e22_lab_instance,
|
||||
publish_e23_lab_instance,
|
||||
publish_integrated_lab_instance,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.analyze import (
|
||||
|
|
@ -497,6 +498,89 @@ def publish_e22_lab(
|
|||
)
|
||||
|
||||
|
||||
@lab_app.command("publish-e23")
|
||||
def publish_e23_lab(
|
||||
reference: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
help="Accepted zero-based E10 result containing exact source masks.",
|
||||
),
|
||||
],
|
||||
worker_result: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--worker-result",
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
help="Accepted E23 inline-temporal worker result.",
|
||||
),
|
||||
],
|
||||
source_report: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--source-report",
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
help="Exact E23 1x replay source report.",
|
||||
),
|
||||
],
|
||||
profile: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
exists=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
help="Bounded E23 inline-temporal profile.",
|
||||
),
|
||||
],
|
||||
session_id: Annotated[
|
||||
str,
|
||||
typer.Option("--session-id", help="New immutable LAB session id."),
|
||||
],
|
||||
lab_id: Annotated[
|
||||
str,
|
||||
typer.Option("--lab-id", help="LAB marker, for example 'LAB E23.2'."),
|
||||
],
|
||||
display_name: Annotated[
|
||||
str,
|
||||
typer.Option("--display-name", help="Operator-facing saved-session title."),
|
||||
],
|
||||
) -> None:
|
||||
"""Publish an accepted inline-temporal 1x run as an immutable LAB replay."""
|
||||
|
||||
repository_root = Path(__file__).resolve().parents[4]
|
||||
try:
|
||||
published = publish_e23_lab_instance(
|
||||
repository_root=repository_root,
|
||||
reference_result_root=reference,
|
||||
worker_result_root=worker_result,
|
||||
source_report_path=source_report,
|
||||
profile_path=profile,
|
||||
lab_session_id=session_id,
|
||||
lab_id=lab_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
|
||||
console.print(f"[red]E23 LAB publication failed:[/red] {exc}")
|
||||
raise typer.Exit(code=2) from exc
|
||||
console.print(
|
||||
"[green]E23 LAB instance published.[/green] "
|
||||
f"session={published.binding.session_id}; "
|
||||
f"source={published.binding.source_session_id}; "
|
||||
f"result={published.binding.result_id}; "
|
||||
"speed=1.0; source_payloads_mutated=false"
|
||||
)
|
||||
|
||||
|
||||
@app.command("serve")
|
||||
def serve_console(
|
||||
port: Annotated[
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ def test_e15_worker_package_is_minimal_hash_addressed_projection(tmp_path: Path)
|
|||
manifest = module.validate_worker_package(package)
|
||||
|
||||
assert package.name == f"e15-worker-package-{manifest['identity_sha256']}"
|
||||
assert manifest["identity"]["classification"] == (
|
||||
"minimal-live-worker-import-projection"
|
||||
)
|
||||
assert len(manifest["artifacts"]) == 11
|
||||
assert manifest["identity"]["classification"] == ("minimal-live-worker-import-projection")
|
||||
assert len(manifest["artifacts"]) == 12
|
||||
assert (package / "k1link" / "compute" / "inline_temporal.py").is_file()
|
||||
assert not (package / "k1link" / "device_plugins" / "xgrids_k1" / "mqtt").exists()
|
||||
assert "observation" not in (
|
||||
package / "k1link" / "device_plugins" / "xgrids_k1" / "__init__.py"
|
||||
).read_text()
|
||||
assert (
|
||||
"observation"
|
||||
not in (package / "k1link" / "device_plugins" / "xgrids_k1" / "__init__.py").read_text()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.inline_temporal import (
|
||||
StreamingSemanticStabilizer,
|
||||
TemporalStabilizer,
|
||||
read_inline_profile,
|
||||
stabilize_world_state,
|
||||
)
|
||||
|
||||
|
||||
def _profile() -> dict[str, object]:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
profile, digest = read_inline_profile(
|
||||
root / "experiments" / "perception" / "e23_inline_temporal_profile.json"
|
||||
)
|
||||
assert len(digest) == 64
|
||||
return profile
|
||||
|
||||
|
||||
def _object(track_id: int, box: list[float]) -> dict[str, object]:
|
||||
return {
|
||||
"association_group": "vehicle",
|
||||
"bbox_xyxy": box,
|
||||
"candidate_projected_points": 20,
|
||||
"clustered_points": 14,
|
||||
"completion_fraction": 0.9,
|
||||
"cuboid_center_map": [10.0, 2.0, 0.8],
|
||||
"cuboid_half_size": [2.25, 0.925, 0.775],
|
||||
"cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||
"cuboid_status": "accepted-class-prior-amodal-v1",
|
||||
"distance_median_m": 10.2,
|
||||
"distance_p10_m": 9.8,
|
||||
"distance_smoothed_m": 10.1,
|
||||
"geometry": "class-prior-completed-from-visible-lidar-support",
|
||||
"ground_rejected_points": 0,
|
||||
"ground_z_map": 0.0,
|
||||
"label": "car",
|
||||
"observed_cuboid_center_map": [10.0, 2.0, 0.8],
|
||||
"observed_cuboid_half_size": [2.25, 0.925, 0.775],
|
||||
"observed_cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||
"orientation_source": "support-pca",
|
||||
"pre_ground_clustered_points": 14,
|
||||
"score": 0.9,
|
||||
"semantic_compatible_points": 18,
|
||||
"support_coverage_fraction": 0.8,
|
||||
"support_ground_z_map": None,
|
||||
"temporal_status": "confirmed",
|
||||
"track_id": track_id,
|
||||
}
|
||||
|
||||
|
||||
def test_e23_profile_pins_inline_stage_and_has_no_control_authority() -> None:
|
||||
profile = _profile()
|
||||
|
||||
assert profile["stage"] == "warm-worker-after-fusion-before-result-publication"
|
||||
assert profile["bounds"]["maximum_track_states"] == 128
|
||||
assert profile["bounds"]["semantic_history_masks"] == 1
|
||||
assert profile["authority"] == {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def test_e23_inline_tracking_is_bounded_and_updates_world_state() -> None:
|
||||
stabilizer = TemporalStabilizer(_profile())
|
||||
first = stabilizer.update(
|
||||
frame_index=0,
|
||||
session_seconds=0.0,
|
||||
objects=[_object(10, [100.0, 100.0, 200.0, 200.0])],
|
||||
)
|
||||
second = stabilizer.update(
|
||||
frame_index=1,
|
||||
session_seconds=0.1,
|
||||
objects=[_object(11, [102.0, 100.0, 202.0, 200.0])],
|
||||
)
|
||||
world = stabilize_world_state(
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"track_id": 11,
|
||||
"velocity_status": "observed",
|
||||
}
|
||||
],
|
||||
"delivery": {},
|
||||
},
|
||||
second,
|
||||
{},
|
||||
)
|
||||
|
||||
assert first[0]["track_id"] == 10
|
||||
assert second[0]["track_id"] == 10
|
||||
assert second[0]["cuboid_status"] == "accepted-temporally-stabilized-e23-v1"
|
||||
assert world["delivery"]["temporal_stability"] == "e23-inline-bounded"
|
||||
assert world["objects"][0]["track_id"] == 10
|
||||
assert stabilizer.snapshot()["peak_track_states"] <= 128
|
||||
|
||||
|
||||
def test_e23_semantic_state_keeps_only_one_mask_and_rejects_islands() -> None:
|
||||
stabilizer = StreamingSemanticStabilizer(_profile())
|
||||
first = np.zeros((600, 800), dtype=np.uint8)
|
||||
second = first.copy()
|
||||
second[20, 20] = 4
|
||||
second[100:110, 100:110] = 7
|
||||
|
||||
stabilizer.update(first)
|
||||
output = stabilizer.update(second)
|
||||
snapshot = stabilizer.snapshot()
|
||||
|
||||
assert output[20, 20] == 0
|
||||
assert np.all(output[102:108, 102:108] == 7)
|
||||
assert snapshot["history_masks"] == 1
|
||||
assert snapshot["frames"] == 2
|
||||
assert snapshot["unsupported_change_reduction_fraction"] > 0
|
||||
|
||||
|
||||
def test_e23_profile_rejects_non_object_json(tmp_path: Path) -> None:
|
||||
profile_path = tmp_path / "profile.json"
|
||||
profile_path.write_text("[]", encoding="utf-8")
|
||||
|
||||
with pytest.raises(RuntimeError, match="profile is invalid"):
|
||||
read_inline_profile(profile_path)
|
||||
Loading…
Reference in New Issue