From c4c2392c79641583ce868e5cc80904b7a22f847b Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 28 Aug 2026 11:22:00 +0300 Subject: [PATCH] feat(perception): integrate vegetation policy review --- .../src/core/laboratory/vegetationShadow.ts | 90 ++++- .../laboratory/M49TgsFullShadowEvidence.tsx | 22 +- .../laboratory/M4ReplayThreatVisual.tsx | 4 +- .../laboratory/VegetationShadowResult.tsx | 127 ++++-- .../test/m49TgsFullShadow.test.mjs | 4 +- .../test/vegetationShadow.test.mjs | 133 +++++-- ...ab-v1-vegetation-integrated-shadow-v1.json | 68 ++++ .../Invoke-M49TgsIntegratedGraphShadow.ps1 | 205 +++++++++- .../run_vegetation_integrated_load.py | 280 ++++++++++++++ ...ld_vegetation_integrated_graph_evidence.py | 366 ++++++++++++++++++ ...1_vegetation_integrated_worker_artifact.py | 183 +++++++++ .../laboratory/vegetation_policy_review.py | 281 ++++++++++++++ .../laboratory/vegetation_policy_video.py | 206 ++++++++++ .../laboratory/vegetation_shadow_lab.py | 157 +++++++- src/k1link/web/vegetation_shadow_lab_api.py | 27 +- ...est_lab_v1_vegetation_integrated_shadow.py | 243 ++++++++++++ tests/test_vegetation_shadow_lab.py | 97 ++++- 17 files changed, 2388 insertions(+), 105 deletions(-) create mode 100644 config/perception/lab-v1-vegetation-integrated-shadow-v1.json create mode 100644 experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py create mode 100644 experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py create mode 100644 scripts/build_lab_v1_vegetation_integrated_worker_artifact.py create mode 100644 src/k1link/laboratory/vegetation_policy_review.py create mode 100644 src/k1link/laboratory/vegetation_policy_video.py create mode 100644 tests/test_lab_v1_vegetation_integrated_shadow.py diff --git a/apps/control-station/src/core/laboratory/vegetationShadow.ts b/apps/control-station/src/core/laboratory/vegetationShadow.ts index f4d3ff5..c6d4c6e 100644 --- a/apps/control-station/src/core/laboratory/vegetationShadow.ts +++ b/apps/control-station/src/core/laboratory/vegetationShadow.ts @@ -55,7 +55,9 @@ export interface VegetationVideoSemanticClass { classId: number; label: string; colorRgb: readonly [number, number, number]; - disposition: "prediction" | "undefined"; + disposition: "labeled" | "ambiguous" | "prediction" | "undefined"; + materialClass: string | null; + evidenceState: string | null; } export interface VegetationRouteVideo { @@ -67,8 +69,12 @@ export interface VegetationRouteVideo { height: 600; centerCropXyxy: readonly [100, 0, 700, 600]; outsideCropState: "undefined"; + viewKind: "fine-semantic-prediction" | "coarse-material-policy-review"; + linkedTgsResultId: string | null; taxonomy: readonly VegetationVideoSemanticClass[]; aggregatePredictionPixels: readonly number[]; + policyPresets: Readonly>>> | null; + fusionMode: "synchronised-multilayer-review" | null; } export interface VegetationShadowResult { @@ -257,6 +263,9 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { "vegetation.route_video.m47_reference_graph_result_id", ); const baseM4ResultId = textValue(row.base_m4_result_id, "vegetation.route_video.base_m4_result_id"); + const viewKind = row.view_kind === undefined + ? "fine-semantic-prediction" + : textValue(row.view_kind, "vegetation.route_video.view_kind"); if ( !/^lab-v1-ravnoves-video-ddrnet-[a-f0-9]{64}$/.test(workerResultId) || !/^m47-reference-graph-lab-[a-f0-9]{64}$/.test(m47ReferenceGraphResultId) @@ -264,6 +273,15 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { ) { throw new VegetationShadowContractError("vegetation.route_video: identity invalid."); } + if (viewKind !== "fine-semantic-prediction" && viewKind !== "coarse-material-policy-review") { + throw new VegetationShadowContractError("vegetation.route_video: view kind invalid."); + } + const linkedTgsResultId = viewKind === "coarse-material-policy-review" + ? textValue(row.linked_tgs_result_id, "vegetation.route_video.linked_tgs_result_id") + : null; + if (linkedTgsResultId && !/^m49-tgs-full-shadow-[a-f0-9]{64}$/.test(linkedTgsResultId)) { + throw new VegetationShadowContractError("vegetation.route_video: TGS identity invalid."); + } exact(row.frame_count, 4489, "vegetation.route_video.frame_count"); exact(row.width, 800, "vegetation.route_video.width"); exact(row.height, 600, "vegetation.route_video.height"); @@ -279,11 +297,9 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { throw new VegetationShadowContractError("vegetation.route_video: crop contract changed."); } const taxonomy = objectValue(row.taxonomy, "vegetation.route_video.taxonomy"); - exact( - taxonomy.schema_version, - "missioncore.lab-v1-vegetation-taxonomy/v1", - "vegetation.route_video.taxonomy.schema", - ); + exact(taxonomy.schema_version, viewKind === "coarse-material-policy-review" + ? "missioncore.lab-v1-terrain-policy-taxonomy/v1" + : "missioncore.lab-v1-vegetation-taxonomy/v1", "vegetation.route_video.taxonomy.schema"); const classes = arrayValue(taxonomy.classes, "vegetation.route_video.taxonomy.classes") .map((value, expectedId): VegetationVideoSemanticClass => { const item = objectValue(value, `vegetation.route_video.taxonomy[${expectedId}]`); @@ -296,36 +312,78 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { if (color.length !== 3 || color.some((channel) => channel > 255)) { throw new VegetationShadowContractError("vegetation.route_video: taxonomy color invalid."); } - const disposition: VegetationVideoSemanticClass["disposition"] = expectedId === 0 - ? "undefined" - : "prediction"; - if (item.disposition !== disposition) { + const disposition = item.disposition; + if ( + disposition !== "labeled" + && disposition !== "ambiguous" + && disposition !== "prediction" + && disposition !== "undefined" + ) { throw new VegetationShadowContractError("vegetation.route_video: taxonomy disposition changed."); } + if ( + viewKind === "fine-semantic-prediction" + && disposition !== (expectedId === 0 ? "undefined" : "prediction") + ) { + throw new VegetationShadowContractError("vegetation.route_video: fine taxonomy disposition changed."); + } + const materialClass = item.material_class === null || item.material_class === undefined + ? null + : textValue(item.material_class, `vegetation.route_video.material[${expectedId}]`); + const evidenceState = item.evidence_state === null || item.evidence_state === undefined + ? null + : textValue(item.evidence_state, `vegetation.route_video.evidence[${expectedId}]`); return { classId, label: textValue(item.label, `vegetation.route_video.label[${expectedId}]`), colorRgb: color as unknown as readonly [number, number, number], disposition, + materialClass, + evidenceState, }; }); - if (classes.length !== 64) { - throw new VegetationShadowContractError("vegetation.route_video: taxonomy must contain 64 classes."); + const expectedClassCount = viewKind === "coarse-material-policy-review" ? 9 : 64; + if (classes.length !== expectedClassCount) { + throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed."); } const aggregatePredictionPixels = arrayValue( row.aggregate_prediction_pixels, "vegetation.route_video.aggregate_prediction_pixels", ).map((value, index) => integerValue(value, `vegetation.route_video.pixels[${index}]`)); - if (aggregatePredictionPixels.length !== 64) { + if (aggregatePredictionPixels.length !== expectedClassCount) { throw new VegetationShadowContractError("vegetation.route_video: class accounting changed."); } const maskArchive = objectValue(row.mask_archive, "vegetation.route_video.mask_archive"); - exact(maskArchive.path, "video/ddrnet-semantic-masks.zip", "vegetation.route_video.mask_archive.path"); + exact(maskArchive.path, viewKind === "coarse-material-policy-review" + ? "video/coarse-material-policy-masks.zip" + : "video/ddrnet-semantic-masks.zip", "vegetation.route_video.mask_archive.path"); const archiveSha256 = textValue(maskArchive.sha256, "vegetation.route_video.mask_archive.sha256"); if (!SHA256.test(archiveSha256)) { throw new VegetationShadowContractError("vegetation.route_video: archive digest invalid."); } integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length"); + let policyPresets: VegetationRouteVideo["policyPresets"] = null; + let fusionMode: VegetationRouteVideo["fusionMode"] = null; + if (viewKind === "coarse-material-policy-review") { + const policy = objectValue(row.policy, "vegetation.route_video.policy"); + const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets"); + policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => { + const rules = objectValue(rawRules, `vegetation.route_video.policy.${presetId}`); + return [presetId, Object.fromEntries(Object.entries(rules).map(([material, action]) => [ + material, + textValue(action, `vegetation.route_video.policy.${presetId}.${material}`), + ]))]; + })); + const fusion = objectValue(row.fusion, "vegetation.route_video.fusion"); + exact(fusion.pixel_raster_fusion, false, "vegetation.route_video.fusion.pixel_raster_fusion"); + exact(fusion.camera_semantic_temporal_filter, "none", "vegetation.route_video.fusion.camera_filter"); + exact( + fusion.mode, + "synchronised-multilayer-review", + "vegetation.route_video.fusion.mode", + ); + fusionMode = "synchronised-multilayer-review"; + } return { workerResultId, m47ReferenceGraphResultId, @@ -335,8 +393,12 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { height: 600, centerCropXyxy: [100, 0, 700, 600], outsideCropState: "undefined", + viewKind, + linkedTgsResultId, taxonomy: classes, aggregatePredictionPixels, + policyPresets, + fusionMode, }; } diff --git a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx index 11c1714..5b17dc0 100644 --- a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx +++ b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx @@ -22,6 +22,7 @@ import { import { M4ReplayThreatVisual, type M4ReplayClassifiedSpatialFrame, + type M4ReplayThreatSemanticLayer, } from "./M4ReplayThreatVisual"; const CLASSES: readonly RecordedEvidenceSemanticClass[] = [ @@ -42,7 +43,15 @@ function message(error: unknown): string { : "Полный TGS spatial frame недоступен."; } -export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) { +export function M49TgsFullShadowEvidence({ + result, + semanticOverride, + evidenceLabel = "M49 · full TGS shadow", +}: { + result: M49TgsFullShadowResult; + semanticOverride?: M4ReplayThreatSemanticLayer; + evidenceLabel?: string; +}) { const [activeSequence, setActiveSequence] = useState(null); const [semantic, setSemantic] = useState(null); const [semanticError, setSemanticError] = useState(null); @@ -62,6 +71,7 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR const controller = new AbortController(); setSemantic(null); setSemanticError(null); + if (semanticOverride) return () => controller.abort(); void fetchE47SemanticSlamResult({ resultId: result.source.linkedSemanticResultId, signal: controller.signal, @@ -77,7 +87,7 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR if (!controller.signal.aborted) setSemanticError(message(caught)); }); return () => controller.abort(); - }, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]); + }, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId, semanticOverride]); useEffect(() => { const controller = new AbortController(); @@ -196,13 +206,13 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR <> - {semanticError ? ( + {!semanticOverride && semanticError ? (
Semantic overlay недоступен: {semanticError}
diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx index 616253b..33eb510 100644 --- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx +++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx @@ -378,12 +378,12 @@ export function M4ReplayThreatVisual({ ); }, [frame, metadata.timeline, showReferenceMediaLayers, showStaticObstacles]); const activeBoxes = useMemo( - () => classifiedSpatialLayer || !showReferenceMediaLayers ? [] : [ + () => !showReferenceMediaLayers ? [] : [ ...boxes(frame?.cameraProposals ?? []), ...staticObstacleBoxes, ...reviewAnchorBoxes, ], - [classifiedSpatialLayer, frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes], + [frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes], ); const semanticClasses = useMemo( () => semantic?.taxonomy.map((item) => ({ diff --git a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx index d5ece50..99d5687 100644 --- a/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/VegetationShadowResult.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; + import { LaboratoryEvidence, LaboratoryResultSummary, @@ -8,11 +10,16 @@ import { vegetationVideoMaskUrl, type VegetationShadowResult, } from "../../core/laboratory/vegetationShadow"; +import { + fetchM49TgsFullShadowResult, + type M49TgsFullShadowResult, +} from "../../core/laboratory/m49TgsFullShadow"; import { M48MaskComparisonVisual, type M48MaskComparisonCase, } from "./M48FailureAtlasVisual"; import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual"; +import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence"; function decimal(value: number, digits = 1): string { return value.toLocaleString("ru-RU", { maximumFractionDigits: digits }); @@ -49,6 +56,75 @@ function comparisonCases(result: VegetationShadowResult): readonly M48MaskCompar }); } +function VegetationRouteEvidence({ result }: { result: VegetationShadowResult }) { + const route = result.routeVideo!; + const [tgs, setTgs] = useState(null); + const [tgsError, setTgsError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + setTgs(null); + setTgsError(null); + if (!route.linkedTgsResultId) return () => controller.abort(); + void fetchM49TgsFullShadowResult(route.linkedTgsResultId, { + signal: controller.signal, + }).then((next) => { + if (controller.signal.aborted) return; + if (next.source.linkedVisualResultId !== route.baseM4ResultId) { + throw new Error("TGS и camera timeline имеют разные source identities."); + } + setTgs(next); + }).catch((caught: unknown) => { + if (!controller.signal.aborted) { + setTgsError(caught instanceof Error ? caught.message : "Sealed TGS недоступен."); + } + }); + return () => controller.abort(); + }, [route.baseM4ResultId, route.linkedTgsResultId]); + + const semantic = { + resultId: route.workerResultId, + spatialResultId: null, + taxonomy: route.taxonomy, + maskUrl: (sequence: number) => vegetationVideoMaskUrl(result.resultId, sequence), + label: route.viewKind === "coarse-material-policy-review" + ? "Coarse material evidence · recorded video" + : "DDRNet vegetation prediction · recorded video", + maskAriaLabel: route.viewKind === "coarse-material-policy-review" + ? "Coarse material policy evidence" + : "DDRNet vegetation prediction", + } as const; + + if (route.linkedTgsResultId && tgs) { + return ( + + ); + } + if (route.linkedTgsResultId && !tgsError) { + return
Открываем sealed TGS и coarse material timeline…
; + } + return ( + <> + + {tgsError ? ( +
+ TGS слой недоступен: {tgsError} +
+ ) : null} + + ); +} + export function VegetationShadowResultView({ rigLabel, result, @@ -68,10 +144,14 @@ export function VegetationShadowResultView({ - vegetationVideoMaskUrl(result.resultId, sequence), - label: "DDRNet vegetation prediction · recorded video", - maskAriaLabel: "DDRNet vegetation prediction", - }} - /> + ) : null} )} result={( )} diff --git a/apps/control-station/test/m49TgsFullShadow.test.mjs b/apps/control-station/test/m49TgsFullShadow.test.mjs index a66345b..36351c6 100644 --- a/apps/control-station/test/m49TgsFullShadow.test.mjs +++ b/apps/control-station/test/m49TgsFullShadow.test.mjs @@ -108,7 +108,9 @@ test("M4.9T5 viewer prefers autonomous chunks and keeps a sealed legacy fallback assert.doesNotMatch(source, /centersXyM\.map\(/); assert.match(source, /fetchE47SemanticSlamResult/); assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/); - assert.match(source, /semantic=\{semantic \? \{/); + assert.match(source, /semantic=\{semanticOverride \?\? \(semantic \? \{/); + assert.match(source, /semanticOverride/); + assert.doesNotMatch(visual, /classifiedSpatialLayer \|\| !showReferenceMediaLayers \? \[\]/); assert.match( visual, /classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/, diff --git a/apps/control-station/test/vegetationShadow.test.mjs b/apps/control-station/test/vegetationShadow.test.mjs index bd59e33..44262c5 100644 --- a/apps/control-station/test/vegetationShadow.test.mjs +++ b/apps/control-station/test/vegetationShadow.test.mjs @@ -104,45 +104,89 @@ function routeVideo() { }; } +function coarseRouteVideo() { + return { + ...routeVideo(), + view_kind: "coarse-material-policy-review", + linked_tgs_result_id: `m49-tgs-full-shadow-${"2".repeat(64)}`, + taxonomy: { + schema_version: "missioncore.lab-v1-terrain-policy-taxonomy/v1", + classes: Array.from({ length: 9 }, (_, classId) => ({ + class_id: classId, + label: `policy-${classId}`, + color_rgb: [classId, classId, classId], + disposition: classId === 0 ? "ambiguous" : "prediction", + material_class: classId === 0 ? null : "grass", + evidence_state: classId === 0 ? "UNOBSERVED" : "SUPPORTED_GROUND", + })), + }, + aggregate_prediction_pixels: Array(9).fill(0), + mask_archive: { + path: "video/coarse-material-policy-masks.zip", + sha256: "8".repeat(64), + byte_length: 2048, + }, + policy: { + presets: { + urban: { grass: "NO_GO" }, + rural: { grass: "HIGH_COST" }, + offroad: { grass: "HIGH_COST" }, + }, + }, + fusion: { + mode: "synchronised-multilayer-review", + pixel_raster_fusion: false, + camera_semantic_temporal_filter: "none", + }, + }; +} + +function labPayload(route = routeVideo()) { + return { + schema_version: "missioncore.lab-v1-vegetation-shadow/v1", + result_id: resultId, + created_at_utc: "2026-08-27T20:00:00Z", + status: "visual-shadow-ready-policy-not-authorized", + ground_truth: false, + identity: { selected_candidate: "ddrnet" }, + metrics: { + candidates: { + ddrnet: candidate("ddrnet", 0.64), + ppliteseg: candidate("ppliteseg", 0.61), + }, + }, + decision: { + selected_candidate: "ddrnet", + visual_shadow_ready: true, + mission_policy_ready_for_configuration: true, + navigation_accepted: false, + production_accepted: false, + }, + limitations: ["shadow only"], + authority: { + commands_enabled: false, + navigation_or_safety_accepted: false, + actuation_accepted: false, + camera_semantics_can_clear_rigid_geometry: false, + }, + catalogs: { + goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)), + ravnoves: [], + }, + route_video: route, + access: "read-only", + }; +} + test("vegetation LAB keeps autonomous assets and fail-closed authority", async () => { let requestedUrl = ""; const result = await fetchVegetationShadowResult(resultId, { fetcher: async (url) => { requestedUrl = String(url); - return new Response(JSON.stringify({ - schema_version: "missioncore.lab-v1-vegetation-shadow/v1", - result_id: resultId, - created_at_utc: "2026-08-27T20:00:00Z", - status: "visual-shadow-ready-policy-not-authorized", - ground_truth: false, - identity: { selected_candidate: "ddrnet" }, - metrics: { - candidates: { - ddrnet: candidate("ddrnet", 0.64), - ppliteseg: candidate("ppliteseg", 0.61), - }, - }, - decision: { - selected_candidate: "ddrnet", - visual_shadow_ready: true, - mission_policy_ready_for_configuration: true, - navigation_accepted: false, - production_accepted: false, - }, - limitations: ["shadow only"], - authority: { - commands_enabled: false, - navigation_or_safety_accepted: false, - actuation_accepted: false, - camera_semantics_can_clear_rigid_geometry: false, - }, - catalogs: { - goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)), - ravnoves: [], - }, - route_video: routeVideo(), - access: "read-only", - }), { status: 200, headers: { "Content-Type": "application/json" } }); + return new Response(JSON.stringify(labPayload()), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); }, }); assert.equal( @@ -154,6 +198,8 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async ( assert.equal(result.routeCases.length, 0); assert.equal(result.validationCases.length, 12); assert.equal(result.routeVideo.frameCount, 4489); + assert.equal(result.routeVideo.viewKind, "fine-semantic-prediction"); + assert.equal(result.routeVideo.linkedTgsResultId, null); assert.equal(result.routeVideo.taxonomy[0].disposition, "undefined"); assert.equal(result.validationCases[0].focus.className, "high_grass"); assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//); @@ -165,6 +211,21 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async ( }); }); +test("vegetation LAB parses coarse material policy and sealed TGS binding", async () => { + const result = await fetchVegetationShadowResult(resultId, { + fetcher: async () => new Response(JSON.stringify(labPayload(coarseRouteVideo())), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + }); + assert.equal(result.routeVideo.viewKind, "coarse-material-policy-review"); + assert.match(result.routeVideo.linkedTgsResultId, /^m49-tgs-full-shadow-/); + assert.equal(result.routeVideo.taxonomy.length, 9); + assert.equal(result.routeVideo.taxonomy[0].evidenceState, "UNOBSERVED"); + assert.equal(result.routeVideo.policyPresets.urban.grass, "NO_GO"); + assert.equal(result.routeVideo.fusionMode, "synchronised-multilayer-review"); +}); + test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => { const resultSource = await readFile( new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url), @@ -172,8 +233,10 @@ test("vegetation LAB reuses the admitted M4.8 and M4.7 instruments", async () => ); assert.match(resultSource, /M48MaskComparisonVisual/); assert.match(resultSource, /M4ReplayThreatVisual/); + assert.match(resultSource, /M49TgsFullShadowEvidence/); + assert.match(resultSource, /semanticOverride/); assert.equal(resultSource.match(/ $null Assert-LastExitCode "pinned runtime image inspection" $os = Get-CimInstance Win32_OperatingSystem $freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB -if ($freeMemoryGiB -lt 24.0) { - throw ("M49 integrated shadow requires 24 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB) +$requiredMemoryGiB = if ($VegetationLoadGate) { 32.0 } else { 24.0 } +if ($freeMemoryGiB -lt $requiredMemoryGiB) { + throw ( + "M49 integrated shadow requires {0:N0} GiB free memory; observed {1:N2} GiB" -f + $requiredMemoryGiB, $freeMemoryGiB + ) } $canonicalBefore = Get-Container "ndc-mission-core-triton" if (-not $canonicalBefore.State.Running -or $canonicalBefore.State.Health.Status -cne "healthy") { @@ -225,9 +274,12 @@ $compileName = "ndc-mission-core-m49-integrated-compile-$RunId" $tritonName = "ndc-mission-core-m49-integrated-triton-$RunId" $graphName = "ndc-mission-core-m49-integrated-graph-$RunId" $tgsName = "ndc-mission-core-m49-integrated-tgs-$RunId" +$vegetationName = "ndc-mission-core-m49-integrated-vegetation-$RunId" $analyzeName = "ndc-mission-core-m49-integrated-analyze-$RunId" $evidenceName = "ndc-mission-core-m49-integrated-evidence-$RunId" +$vegetationEvidenceName = "ndc-mission-core-m49-integrated-vegetation-evidence-$RunId" $containers = @($prepareName, $compileName, $tritonName, $graphName, $tgsName, $analyzeName, $evidenceName) +if ($VegetationLoadGate) { $containers += @($vegetationName, $vegetationEvidenceName) } foreach ($name in $containers) { if (& docker ps -a --format "{{.Names}}" --filter "name=^/$name$") { throw "M49 integrated container name already exists: $name" @@ -236,6 +288,24 @@ foreach ($name in $containers) { $started = [DateTimeOffset]::UtcNow try { + if ($VegetationLoadGate) { + $vegetationFrames = Join-Path $runOutput "vegetation\input-frames" + $null = New-Item -ItemType Directory -Path $vegetationFrames + & ffmpeg -hide_banner -loglevel error -i $source.Video -map 0:v:0 -fps_mode passthrough ( + Join-Path $vegetationFrames "frame-%06d.png" + ) + Assert-LastExitCode "RAVNOVES full-video frame extraction" + $extractedFrames = @( + Get-ChildItem -LiteralPath $vegetationFrames -File -Filter "frame-*.png" | + Sort-Object Name + ) + if ( + $extractedFrames.Count -ne 4489 -or + $extractedFrames[0].Name -cne "frame-000001.png" -or + $extractedFrames[-1].Name -cne "frame-004489.png" + ) { throw "RAVNOVES full-video frame sequence changed" } + } + & docker run --rm --name $prepareName --network none --cpus 8 --memory 16g ` --entrypoint python3 ` --volume ((Convert-ToDockerPath $source.SourcePack) + ":/source/lidar-pack.npz:ro") ` @@ -332,24 +402,71 @@ try { $TravelImageTag /release/run_tgs_integrated_shadow.sh *> $null Assert-LastExitCode "M49 integrated TGS creation" + if ($VegetationLoadGate) { + $dockerVegetationDataset = Convert-ToDockerPath $vegetation.Dataset + $dockerVegetationCheckpoint = Convert-ToDockerPath $vegetation.Checkpoint + & docker create --name $vegetationName --network none --cpus 8 --memory 10g ` + --gpus all --read-only --security-opt "no-new-privileges:true" --cap-drop ALL ` + --pids-limit 512 --tmpfs "/tmp:rw,noexec,nosuid,size=2g" ` + -e "HOME=/tmp" ` + --entrypoint conda ` + --volume ($dockerRelease + ":/release:ro") ` + --volume ($dockerRun + ":/shared:rw") ` + --volume ($dockerVegetationDataset + ":/data/goose:ro") ` + --volume ($dockerVegetationCheckpoint + ":/models/candidate.pth:ro") ` + $VegetationImageTag run --no-capture-output --name goose python ` + /release/run_vegetation_integrated_load.py ` + --config /release/lab-v1-goose-vegetation-benchmark-v1.json ` + --policy /release/lab-v1-vegetation-mission-policy-v1.json ` + --provider-map /release/lab-v1-vegetation-provider-label-map-v1.json ` + --checkpoint /models/candidate.pth ` + --dataset-root /data/goose ` + --frames-root /shared/vegetation/input-frames ` + --source-rate-hz $rate ` + --minimum-effective-fps 11.209069 ` + --maximum-completion-p95-ms 125.0 ` + --shared-start-ready-file /shared/control/vegetation.ready ` + --shared-start-file /shared/control/start.signal ` + --frame-ledger /shared/vegetation/frames.jsonl ` + --output /shared/vegetation/result.json ` + --release-sha256 $ExpectedArtifactSha256 *> $null + Assert-LastExitCode "M49 integrated vegetation creation" + } + & docker start $graphName *> $null Assert-LastExitCode "M49 integrated graph start" & docker start $tgsName *> $null Assert-LastExitCode "M49 integrated TGS start" + if ($VegetationLoadGate) { + & docker start $vegetationName *> $null + Assert-LastExitCode "M49 integrated vegetation start" + } $graphReady = Join-Path $runOutput "control\graph.ready" $tgsReady = Join-Path $runOutput "control\tgs.ready" - Wait-SharedReady $graphReady $tgsReady $graphName $tgsName + $vegetationReady = if ($VegetationLoadGate) { + Join-Path $runOutput "control\vegetation.ready" + } else { "" } + Wait-SharedReady $graphReady $tgsReady $vegetationReady $graphName $tgsName $vegetationName [DateTimeOffset]::UtcNow.ToString("o") | Set-Content -LiteralPath ( Join-Path $runOutput "control\start.signal" ) -Encoding utf8 $telemetryPath = Join-Path $runOutput "container-telemetry.jsonl" + $m49TelemetryPath = if ($VegetationLoadGate) { + Join-Path $runOutput "m49-container-telemetry.jsonl" + } else { $telemetryPath } while ($true) { $graphState = Get-Container $graphName $tgsState = Get-Container $tgsName + $vegetationState = if ($VegetationLoadGate) { + Get-Container $vegetationName + } else { $null } $running = @() if ($graphState.State.Running) { $running += $graphName } if ($tgsState.State.Running) { $running += $tgsName } + if ($VegetationLoadGate -and $vegetationState.State.Running) { + $running += $vegetationName + } if ((Get-Container $tritonName).State.Running) { $running += $tritonName } if ($running.Count -gt 0) { $stats = @((& docker stats --no-stream --format "{{json .}}" @running)) @@ -362,10 +479,12 @@ try { "tgs" } elseif ($value.Name -ceq $tritonName) { "triton" + } elseif ($VegetationLoadGate -and $value.Name -ceq $vegetationName) { + "vegetation" } else { throw "Unknown M49 telemetry container" } - [ordered]@{ + $telemetryRow = [ordered]@{ observed_utc = [DateTimeOffset]::UtcNow.ToString("o") role = $role name = [string]$value.Name @@ -373,23 +492,46 @@ try { memory_usage = [string]$value.MemUsage memory_percent = [string]$value.MemPerc pids = [string]$value.PIDs - } | ConvertTo-Json -Compress | Out-File -LiteralPath $telemetryPath -Encoding utf8 -Append + } | ConvertTo-Json -Compress + $telemetryRow | Out-File -LiteralPath $telemetryPath -Encoding utf8 -Append + if ($VegetationLoadGate -and $role -cne "vegetation") { + $telemetryRow | Out-File -LiteralPath $m49TelemetryPath -Encoding utf8 -Append + } } } - if (-not $graphState.State.Running -and -not $tgsState.State.Running) { break } + $vegetationStopped = -not $VegetationLoadGate -or -not $vegetationState.State.Running + if ( + -not $graphState.State.Running -and + -not $tgsState.State.Running -and + $vegetationStopped + ) { break } Start-Sleep -Seconds 1 } $graphExit = [int](Get-Container $graphName).State.ExitCode $tgsExit = [int](Get-Container $tgsName).State.ExitCode + $vegetationExit = if ($VegetationLoadGate) { + [int](Get-Container $vegetationName).State.ExitCode + } else { 0 } $previousErrorAction = $ErrorActionPreference $ErrorActionPreference = "Continue" $graphLogs = & docker logs $graphName 2>&1 $tgsLogs = & docker logs $tgsName 2>&1 + $vegetationLogs = if ($VegetationLoadGate) { + & docker logs $vegetationName 2>&1 + } else { @() } $ErrorActionPreference = $previousErrorAction $graphLogs | Set-Content -LiteralPath (Join-Path $runOutput "graph.log") -Encoding utf8 $tgsLogs | Set-Content -LiteralPath (Join-Path $runOutput "tgs.log") -Encoding utf8 + if ($VegetationLoadGate) { + $vegetationLogs | Set-Content -LiteralPath ( + Join-Path $runOutput "vegetation.log" + ) -Encoding utf8 + } if ($graphExit -ne 0) { throw "M49 integrated graph failed with exit code $graphExit" } if ($tgsExit -ne 0) { throw "M49 integrated TGS failed with exit code $tgsExit" } + if ($vegetationExit -ne 0) { + throw "M49 integrated vegetation failed with exit code $vegetationExit" + } & docker run --rm --name $analyzeName --network none --cpus 8 --memory 16g ` --entrypoint python3 ` @@ -401,6 +543,12 @@ try { --output-root /shared/tgs/evidence Assert-LastExitCode "M49 integrated TGS evidence analysis" + $m49ResultPath = if ($VegetationLoadGate) { + "/shared/m49-result.json" + } else { "/shared/result.json" } + $dockerM49TelemetryPath = if ($VegetationLoadGate) { + "/shared/m49-container-telemetry.jsonl" + } else { "/shared/container-telemetry.jsonl" } & docker run --rm --name $evidenceName --network none --cpus 4 --memory 8g ` --entrypoint python3 ` --volume ($dockerRelease + ":/release:ro") ` @@ -411,11 +559,33 @@ try { --graph-frames /shared/graph/frames.jsonl ` --tgs-result /shared/tgs/evidence/result.json ` --tgs-timing /shared/tgs/tgs-full-timing.tsv ` - --telemetry /shared/container-telemetry.jsonl ` - --output /shared/result.json ` + --telemetry $dockerM49TelemetryPath ` + --output $m49ResultPath ` --release-sha256 $ExpectedArtifactSha256 Assert-LastExitCode "M49 integrated evidence gate" + + if ($VegetationLoadGate) { + & docker run --rm --name $vegetationEvidenceName --network none --cpus 4 --memory 8g ` + --entrypoint python3 ` + --volume ($dockerRelease + ":/release:ro") ` + --volume ($dockerRun + ":/shared:rw") ` + $ParityImageTag /release/build_vegetation_integrated_graph_evidence.py ` + --profile /release/lab-v1-vegetation-integrated-shadow-v1.json ` + --m49-result /shared/m49-result.json ` + --graph-frames /shared/graph/frames.jsonl ` + --tgs-timing /shared/tgs/tgs-full-timing.tsv ` + --vegetation-result /shared/vegetation/result.json ` + --vegetation-frames /shared/vegetation/frames.jsonl ` + --telemetry /shared/container-telemetry.jsonl ` + --output /shared/result.json ` + --release-sha256 $ExpectedArtifactSha256 + Assert-LastExitCode "M49 integrated vegetation evidence gate" + } } finally { + $vegetationFrames = Join-Path $runOutput "vegetation\input-frames" + if (Test-Path -LiteralPath $vegetationFrames -PathType Container) { + Remove-Item -LiteralPath $vegetationFrames -Recurse -Force + } foreach ($name in $containers) { Remove-ExactContainer $name } $canonicalAfter = Get-Container "ndc-mission-core-triton" if ( @@ -432,7 +602,11 @@ if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { } $result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json $summary = [ordered]@{ - schema_version = "missioncore.m49-tgs-integrated-graph-worker-summary/v1" + schema_version = if ($VegetationLoadGate) { + "missioncore.lab-v1-vegetation-integrated-worker-summary/v1" + } else { + "missioncore.m49-tgs-integrated-graph-worker-summary/v1" + } worker_id = "worker-006" run_id = $RunId code_revision = [string]$releaseDocument.code_revision @@ -443,6 +617,7 @@ $summary = [ordered]@{ free_memory_gib_before = [math]::Round($freeMemoryGiB, 6) result_id = [string]$result.result_id result_status = [string]$result.status + vegetation_load_gate = [bool]$VegetationLoadGate canonical_triton_id = $canonicalId canonical_triton_health = "healthy" gauss_or_playcanvas_action = "none" diff --git a/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py b/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py new file mode 100644 index 0000000..a60b54f --- /dev/null +++ b/experiments/perception/worker/lab_v1_vegetation_goose/run_vegetation_integrated_load.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""Run source-paced DDRNet beside the frozen M4 graph and TGS shadow.""" + +from __future__ import annotations + +import argparse +import json +import math +import platform +import statistics +import time +from pathlib import Path +from typing import Any + +import torch +from PIL import Image +from run_goose_vegetation_benchmark import ( + infer, + load_mapping, + load_model, + percentile, + preprocess, + read_json, + sha256, + stable_digest, + validate_contracts, +) + +SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v1" +FRAME_SCHEMA = "missioncore.lab-v1-vegetation-integrated-frame/v1" +FRAME_COUNT = 4_489 +AUTHORITY = { + "ground_truth": False, + "candidate_accepted": False, + "camera_semantics_can_clear_rigid_geometry": False, + "commands_enabled": False, + "actuation_allowed": False, + "navigation_or_safety_accepted": False, + "production_accepted": False, +} + + +class IntegratedLoadError(RuntimeError): + """The bounded integrated-load contract is incomplete or changed.""" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--config", type=Path, required=True) + parser.add_argument("--policy", type=Path, required=True) + parser.add_argument("--provider-map", type=Path, required=True) + parser.add_argument("--checkpoint", type=Path, required=True) + parser.add_argument("--dataset-root", type=Path, required=True) + parser.add_argument("--frames-root", type=Path, required=True) + parser.add_argument("--source-rate-hz", type=float, required=True) + parser.add_argument("--minimum-effective-fps", type=float, required=True) + parser.add_argument("--maximum-completion-p95-ms", type=float, required=True) + parser.add_argument("--shared-start-ready-file", type=Path, required=True) + parser.add_argument("--shared-start-file", type=Path, required=True) + parser.add_argument("--shared-start-timeout-seconds", type=float, default=600.0) + parser.add_argument("--frame-ledger", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--release-sha256", required=True) + return parser.parse_args() + + +def wait_for_shared_start(ready_file: Path, start_file: Path, timeout_seconds: float) -> None: + if ready_file.exists(): + raise IntegratedLoadError("shared-start ready file already exists") + ready_file.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + ready_file.write_text("ready\n", encoding="utf-8") + deadline = time.monotonic() + timeout_seconds + while not start_file.is_file(): + if time.monotonic() >= deadline: + raise IntegratedLoadError("shared-start barrier timed out") + time.sleep(0.01) + + +def distribution(values: list[float]) -> dict[str, float]: + return { + "mean": round(statistics.fmean(values), 6), + "p50": round(percentile(values, 0.50), 6), + "p95": round(percentile(values, 0.95), 6), + "p99": round(percentile(values, 0.99), 6), + "maximum": round(max(values), 6), + } + + +def exact_frames(root: Path) -> list[Path]: + if root.is_symlink() or not root.is_dir(): + raise IntegratedLoadError("RAVNOVES frame root is unavailable") + frames = sorted(root.glob("frame-*.png")) + expected = [f"frame-{sequence + 1:06d}.png" for sequence in range(FRAME_COUNT)] + if len(frames) != FRAME_COUNT or [frame.name for frame in frames] != expected: + raise IntegratedLoadError("RAVNOVES full-video frame sequence changed") + return frames + + +def validate_sha256(value: str, label: str) -> None: + if len(value) != 64 or any(character not in "0123456789abcdef" for character in value): + raise IntegratedLoadError(f"{label} SHA-256 is invalid") + + +def run() -> int: + args = parse_args() + if not torch.cuda.is_available(): + raise IntegratedLoadError("CUDA is required for Worker 006 qualification") + if ( + not math.isfinite(args.source_rate_hz) + or args.source_rate_hz <= 0 + or args.minimum_effective_fps <= 0 + or args.maximum_completion_p95_ms <= 0 + or args.shared_start_timeout_seconds <= 0 + ): + raise IntegratedLoadError("integrated-load thresholds must be positive and finite") + validate_sha256(args.release_sha256, "release") + if args.output.exists() or args.frame_ledger.exists(): + raise IntegratedLoadError("integrated-load output already exists") + + config = read_json(args.config, "benchmark config") + policy = read_json(args.policy, "mission policy") + provider_map = read_json(args.provider_map, "provider map") + candidate = validate_contracts(config, policy, provider_map, "ddrnet") + if args.checkpoint.is_symlink() or not args.checkpoint.is_file(): + raise IntegratedLoadError("DDRNet checkpoint is unavailable") + if args.checkpoint.stat().st_size != candidate["checkpoint_size_bytes"]: + raise IntegratedLoadError("DDRNet checkpoint size changed") + checkpoint_sha256 = sha256(args.checkpoint) + if checkpoint_sha256 != candidate["checkpoint_sha256"]: + raise IntegratedLoadError("DDRNet checkpoint digest changed") + mapping_path = args.dataset_root / config["dataset"]["mapping_relative_path"] + load_mapping(mapping_path, config["dataset"]["mapping_sha256"]) + frames = exact_frames(args.frames_root) + + torch.cuda.empty_cache() + model, model_name, architecture_failures = load_model("ddrnet", args.checkpoint) + with Image.open(frames[0]) as image: + warmup_tensor, _ = preprocess(image.convert("RGB")) + warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)] + torch.cuda.reset_peak_memory_stats() + wait_for_shared_start( + args.shared_start_ready_file, + args.shared_start_file, + args.shared_start_timeout_seconds, + ) + + interval_ns = 1_000_000_000.0 / args.source_rate_hz + start_ns = time.monotonic_ns() + started_utc_ns = time.time_ns() + completion_ages_ms: list[float] = [] + stage_latencies_ms: list[float] = [] + inference_latencies_ms: list[float] = [] + late_deadline_count = 0 + args.frame_ledger.parent.mkdir(parents=True, exist_ok=True) + with args.frame_ledger.open("x", encoding="utf-8") as ledger: + for sequence, frame in enumerate(frames): + scheduled_ns = start_ns + round(sequence * interval_ns) + remaining_ns = scheduled_ns - time.monotonic_ns() + if remaining_ns > 0: + time.sleep(remaining_ns / 1_000_000_000.0) + admitted_ns = time.monotonic_ns() + with Image.open(frame) as image: + source = image.convert("RGB") + if source.size != ( + config["ravnoves"]["expected_width"], + config["ravnoves"]["expected_height"], + ): + raise IntegratedLoadError("RAVNOVES video frame dimensions changed") + tensor, _ = preprocess(source) + _, inference_ms = infer(model, tensor) + completed_ns = time.monotonic_ns() + completion_age_ms = (completed_ns - scheduled_ns) / 1_000_000.0 + stage_ms = (completed_ns - admitted_ns) / 1_000_000.0 + completion_ages_ms.append(completion_age_ms) + stage_latencies_ms.append(stage_ms) + inference_latencies_ms.append(inference_ms) + if sequence + 1 < FRAME_COUNT and completed_ns > start_ns + round( + (sequence + 1) * interval_ns + ): + late_deadline_count += 1 + row = { + "schema_version": FRAME_SCHEMA, + "sequence": sequence, + "frame_name": frame.name, + "scheduled_monotonic_ns": scheduled_ns, + "admitted_monotonic_ns": admitted_ns, + "completed_monotonic_ns": completed_ns, + "completion_age_ms": round(completion_age_ms, 6), + "stage_ms": round(stage_ms, 6), + "inference_ms": round(inference_ms, 6), + } + ledger.write(json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n") + if sequence % 64 == 0: + ledger.flush() + + completed_ns = time.monotonic_ns() + wall_seconds = (completed_ns - start_ns) / 1_000_000_000.0 + effective_fps = FRAME_COUNT / wall_seconds + completion = distribution(completion_ages_ms) + checks = { + "all_frames_accounted": len(completion_ages_ms) == FRAME_COUNT, + "minimum_effective_fps": effective_fps >= args.minimum_effective_fps, + "maximum_completion_p95_ms": completion["p95"] + <= args.maximum_completion_p95_ms, + "zero_capacity_drops": len(completion_ages_ms) == FRAME_COUNT, + "authority_remains_false": all(value is False for value in AUTHORITY.values()), + } + result: dict[str, Any] = { + "schema_version": SCHEMA, + "worker_id": "worker-006", + "source": { + "source_id": config["ravnoves"]["source_id"], + "frame_count": FRAME_COUNT, + "requested_source_rate_hz": args.source_rate_hz, + "raw_fisheye_immutable": True, + "ground_truth_available": False, + }, + "candidate": { + "candidate_id": candidate["candidate_id"], + "candidate_key": "ddrnet", + "loaded_model_name": model_name, + "architecture_probe_failures": architecture_failures, + "checkpoint_size_bytes": args.checkpoint.stat().st_size, + "checkpoint_sha256": checkpoint_sha256, + }, + "execution": { + "run_mode": "source-paced-integrated-shadow/v1", + "started_utc_ns": started_utc_ns, + "wall_seconds": round(wall_seconds, 6), + "effective_fps": round(effective_fps, 6), + "frame_count": FRAME_COUNT, + "capacity_drop_count": 0, + "deadline_miss_count": late_deadline_count, + "frame_ledger": { + "path": args.frame_ledger.name, + "rows": FRAME_COUNT, + "sha256": sha256(args.frame_ledger), + }, + }, + "timing": { + "prewarm_inference_count": len(warmup_latencies_ms), + "prewarm_latency_ms_first": round(warmup_latencies_ms[0], 6), + "prewarm_latency_ms_last": round(warmup_latencies_ms[-1], 6), + "completion_age_ms": completion, + "stage_ms": distribution(stage_latencies_ms), + "inference_ms": distribution(inference_latencies_ms), + }, + "resource": { + "gpu_name": torch.cuda.get_device_name(0), + "peak_allocated_vram_bytes": int(torch.cuda.max_memory_allocated()), + "peak_reserved_vram_bytes": int(torch.cuda.max_memory_reserved()), + "torch_version": torch.__version__, + "cuda_runtime_version": torch.version.cuda, + "python_version": platform.python_version(), + }, + "identity": { + "release_sha256": args.release_sha256, + "config_sha256": sha256(args.config), + "policy_sha256": sha256(args.policy), + "provider_map_sha256": sha256(args.provider_map), + "runner_sha256": sha256(Path(__file__)), + }, + "predeclared_thresholds": { + "minimum_effective_fps": args.minimum_effective_fps, + "maximum_completion_p95_ms": args.maximum_completion_p95_ms, + "capacity_drop_count_max": 0, + }, + "checks": checks, + "integrated_load_gate_passed": all(checks.values()), + "authority": AUTHORITY, + } + result["result_id"] = f"lab-v1-vegetation-integrated-{stable_digest(result)}" + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps({"result_id": result["result_id"], "passed": all(checks.values())})) + return 0 if all(checks.values()) else 2 + + +if __name__ == "__main__": + raise SystemExit(run()) diff --git a/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py b/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py new file mode 100644 index 0000000..0808088 --- /dev/null +++ b/experiments/perception/worker/m49_t3_travel/build_vegetation_integrated_graph_evidence.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Seal the synchronized RF-DETR, TGS and DDRNet Worker 006 load gate.""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import math +import re +from collections import defaultdict +from pathlib import Path +from typing import Any + +import numpy as np + +PROFILE_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-profile/v1" +M49_SCHEMA = "missioncore.m49-tgs-integrated-graph-shadow-result/v1" +VEGETATION_SCHEMA = "missioncore.lab-v1-vegetation-integrated-load/v1" +RESULT_SCHEMA = "missioncore.lab-v1-vegetation-integrated-shadow-result/v1" +FRAME_COUNT = 4_489 + + +class VegetationIntegratedError(RuntimeError): + """The synchronized three-layer load evidence is incomplete.""" + + +def canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def load_json(path: Path, label: str) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8-sig")) + except (OSError, json.JSONDecodeError) as exc: + raise VegetationIntegratedError(f"{label} is unreadable") from exc + if not isinstance(value, dict): + raise VegetationIntegratedError(f"{label} is not an object") + return value + + +def distribution(values: list[float]) -> dict[str, float]: + if not values: + raise VegetationIntegratedError("timing distribution is empty") + array = np.asarray(values, dtype=np.float64) + return { + "mean": round(float(array.mean()), 6), + "p50": round(float(np.percentile(array, 50)), 6), + "p95": round(float(np.percentile(array, 95)), 6), + "p99": round(float(np.percentile(array, 99)), 6), + "maximum": round(float(array.max()), 6), + } + + +def graph_completion_ages(path: Path) -> list[float]: + values: list[float] = [] + with path.open("r", encoding="utf-8") as stream: + for expected, line in enumerate(stream): + row = json.loads(line) + if row.get("source_envelope", {}).get("sequence") != expected: + raise VegetationIntegratedError("graph frame sequence changed") + age = row.get("completion_age_ns") + if not isinstance(age, int) or age < 0: + raise VegetationIntegratedError("graph completion age is invalid") + values.append(age / 1_000_000.0) + if len(values) != FRAME_COUNT: + raise VegetationIntegratedError("graph frame ledger is incomplete") + return values + + +def tgs_completion_ages(path: Path) -> list[float]: + values: list[float] = [] + with path.open("r", encoding="utf-8", newline="") as stream: + for expected, row in enumerate(csv.DictReader(stream, delimiter="\t")): + if int(row["timeline_frame_index"]) != expected: + raise VegetationIntegratedError("TGS timing sequence changed") + age = float(row["completion_age_ms"]) + if not math.isfinite(age) or age < 0: + raise VegetationIntegratedError("TGS completion age is invalid") + values.append(age) + if len(values) != FRAME_COUNT: + raise VegetationIntegratedError("TGS timing ledger is incomplete") + return values + + +def vegetation_completion_ages(path: Path) -> list[float]: + values: list[float] = [] + with path.open("r", encoding="utf-8") as stream: + for expected, line in enumerate(stream): + row = json.loads(line) + if row.get("schema_version") != "missioncore.lab-v1-vegetation-integrated-frame/v1": + raise VegetationIntegratedError("vegetation frame schema changed") + if row.get("sequence") != expected: + raise VegetationIntegratedError("vegetation frame sequence changed") + age = row.get("completion_age_ms") + if not isinstance(age, (int, float)) or not math.isfinite(age) or age < 0: + raise VegetationIntegratedError("vegetation completion age is invalid") + values.append(float(age)) + if len(values) != FRAME_COUNT: + raise VegetationIntegratedError("vegetation frame ledger is incomplete") + return values + + +_SIZE = re.compile(r"^\s*([0-9.]+)\s*([kmgt]?i?b)\s*$", re.IGNORECASE) + + +def size_mib(value: str) -> float: + match = _SIZE.fullmatch(value) + if match is None: + raise VegetationIntegratedError("container memory telemetry is invalid") + number = float(match.group(1)) + scale = { + "b": 1.0 / (1024.0 * 1024.0), + "kb": 1.0 / 1024.0, + "kib": 1.0 / 1024.0, + "mb": 1.0, + "mib": 1.0, + "gb": 1024.0, + "gib": 1024.0, + "tb": 1024.0 * 1024.0, + "tib": 1024.0 * 1024.0, + }[match.group(2).lower()] + return number * scale + + +def host_telemetry(path: Path) -> dict[str, object]: + roles = ("graph", "tgs", "triton", "vegetation") + samples: dict[str, list[dict[str, float]]] = defaultdict(list) + with path.open("r", encoding="utf-8-sig") as stream: + for line in stream: + row = json.loads(line) + role = row.get("role") + if role not in roles: + raise VegetationIntegratedError("container telemetry role changed") + cpu = row.get("cpu_percent") + memory = row.get("memory_usage") + memory_percent = row.get("memory_percent") + if not all(isinstance(value, str) for value in (cpu, memory, memory_percent)): + raise VegetationIntegratedError("container telemetry row is incomplete") + assert isinstance(cpu, str) and isinstance(memory, str) + assert isinstance(memory_percent, str) + samples[role].append( + { + "cpu_percent": float(cpu.rstrip("%")), + "memory_used_mib": size_mib(memory.split("/", 1)[0].strip()), + "memory_percent": float(memory_percent.rstrip("%")), + } + ) + if any(not samples[role] for role in roles): + raise VegetationIntegratedError("container telemetry does not cover every runtime role") + return { + role: { + "sample_count": len(samples[role]), + "cpu_percent": distribution([row["cpu_percent"] for row in samples[role]]), + "memory_used_mib": distribution( + [row["memory_used_mib"] for row in samples[role]] + ), + "memory_percent": distribution( + [row["memory_percent"] for row in samples[role]] + ), + } + for role in roles + } + + +def build( + *, + profile_path: Path, + m49_result_path: Path, + graph_frames_path: Path, + tgs_timing_path: Path, + vegetation_result_path: Path, + vegetation_frames_path: Path, + telemetry_path: Path, + output_path: Path, + release_sha256: str, +) -> dict[str, object]: + if output_path.exists(): + raise VegetationIntegratedError("integrated vegetation result already exists") + if len(release_sha256) != 64 or any( + character not in "0123456789abcdef" for character in release_sha256 + ): + raise VegetationIntegratedError("release SHA-256 is invalid") + profile = load_json(profile_path, "integrated vegetation profile") + m49 = load_json(m49_result_path, "M49 integrated result") + vegetation = load_json(vegetation_result_path, "vegetation load result") + if profile.get("schema_version") != PROFILE_SCHEMA: + raise VegetationIntegratedError("integrated vegetation profile schema changed") + if m49.get("schema_version") != M49_SCHEMA: + raise VegetationIntegratedError("M49 integrated result schema changed") + if vegetation.get("schema_version") != VEGETATION_SCHEMA: + raise VegetationIntegratedError("vegetation load result schema changed") + + graph_ages = graph_completion_ages(graph_frames_path) + tgs_ages = tgs_completion_ages(tgs_timing_path) + vegetation_ages = vegetation_completion_ages(vegetation_frames_path) + combined_ages = [ + max(graph, tgs, semantic) + for graph, tgs, semantic in zip( + graph_ages, tgs_ages, vegetation_ages, strict=True + ) + ] + combined = distribution(combined_ages) + telemetry = host_telemetry(telemetry_path) + acceptance = profile["acceptance"] + vegetation_execution = vegetation.get("execution", {}) + vegetation_timing = vegetation.get("timing", {}) + vegetation_identity = vegetation.get("identity", {}) + vegetation_candidate = vegetation.get("candidate", {}) + m49_performance = m49.get("performance", {}) + m49_accounting = m49.get("accounting", {}) + checks = { + "base_m49_runtime_passed": ( + m49.get("status") == "passed" + and m49.get("integrated_runtime_gate_passed") is True + and m49.get("identity", {}).get("profile_sha256") + == profile["stages"]["m49_graph_tgs"]["profile_sha256"] + ), + "vegetation_identity_frozen": ( + vegetation_candidate.get("candidate_key") == "ddrnet" + and vegetation_candidate.get("checkpoint_sha256") + == profile["stages"]["vegetation"]["checkpoint_sha256"] + and vegetation_identity.get("config_sha256") + == profile["stages"]["vegetation"]["config_sha256"] + and vegetation_identity.get("policy_sha256") + == profile["stages"]["vegetation"]["policy_sha256"] + and vegetation_identity.get("provider_map_sha256") + == profile["stages"]["vegetation"]["provider_map_sha256"] + ), + "requested_source_rate_preserved": ( + vegetation.get("source", {}).get("requested_source_rate_hz") + == profile["source"]["requested_source_rate_hz"] + ), + "exact_three_layer_sequence_join": len(combined_ages) == FRAME_COUNT, + "all_graph_frames_delivered": ( + m49_accounting.get("graph_admitted") == FRAME_COUNT + and m49_accounting.get("graph_delivered") == FRAME_COUNT + ), + "all_tgs_frames_accounted": m49_accounting.get("tgs_timeline_frames") + == FRAME_COUNT, + "all_vegetation_frames_accounted": vegetation_execution.get("frame_count") + == FRAME_COUNT, + "minimum_graph_world_state_fps": float( + m49_performance.get("effective_world_state_fps", 0.0) + ) + >= float(acceptance["minimum_graph_world_state_fps"]), + "minimum_vegetation_fps": float(vegetation_execution.get("effective_fps", 0.0)) + >= float(acceptance["minimum_vegetation_fps"]), + "maximum_vegetation_completion_p95_ms": float( + vegetation_timing.get("completion_age_ms", {}).get("p95", math.inf) + ) + <= float(acceptance["maximum_vegetation_completion_p95_ms"]), + "maximum_combined_output_age_p99_ms": combined["p99"] + <= float(acceptance["maximum_combined_output_age_p99_ms"]), + "zero_capacity_drops": ( + int(m49_accounting.get("tgs_capacity_drops", -1)) == 0 + and int(vegetation_execution.get("capacity_drop_count", -1)) == 0 + ), + "host_resource_telemetry_complete": all( + telemetry[role]["sample_count"] > 0 + for role in ("graph", "tgs", "triton", "vegetation") + ), + "authority_remains_false": ( + all(value is False for value in profile["authority"].values()) + and all(value is False for value in vegetation.get("authority", {}).values()) + ), + } + files = { + label: {"bytes": path.stat().st_size, "sha256": sha256_file(path)} + for label, path in ( + ("m49-result.json", m49_result_path), + ("graph-frames.jsonl", graph_frames_path), + ("tgs-timing.tsv", tgs_timing_path), + ("vegetation-result.json", vegetation_result_path), + ("vegetation-frames.jsonl", vegetation_frames_path), + ("container-telemetry.jsonl", telemetry_path), + ) + } + document: dict[str, object] = { + "schema_version": RESULT_SCHEMA, + "profile_id": profile["profile_id"], + "status": "passed" if all(checks.values()) else "failed", + "source": { + "source_id": profile["source"]["source_id"], + "requested_source_rate_hz": profile["source"]["requested_source_rate_hz"], + "joined_frame_count": len(combined_ages), + "ground_truth_available": False, + }, + "identity": { + "release_sha256": release_sha256, + "profile_sha256": sha256_file(profile_path), + "m49_result_id": m49.get("result_id"), + "vegetation_result_id": vegetation.get("result_id"), + }, + "performance": { + "graph_tgs": m49_performance, + "vegetation": { + "effective_fps": vegetation_execution.get("effective_fps"), + "completion_age_ms": vegetation_timing.get("completion_age_ms"), + "stage_ms": vegetation_timing.get("stage_ms"), + "inference_ms": vegetation_timing.get("inference_ms"), + "resource": vegetation.get("resource"), + }, + "three_layer_output_age_ms": combined, + "host_containers": telemetry, + }, + "accounting": { + "graph_frames": m49_accounting.get("graph_delivered"), + "tgs_frames": m49_accounting.get("tgs_timeline_frames"), + "vegetation_frames": vegetation_execution.get("frame_count"), + "capacity_drop_count": int(m49_accounting.get("tgs_capacity_drops", 0)) + + int(vegetation_execution.get("capacity_drop_count", 0)), + }, + "checks": checks, + "integrated_runtime_gate_passed": all(checks.values()), + "visual_quality_accepted": False, + "route_truth_available": False, + "production_accepted": False, + "authority": profile["authority"], + "files": files, + } + identity = hashlib.sha256(canonical_json(document)).hexdigest() + document["result_id"] = f"lab-v1-vegetation-integrated-shadow-{identity}" + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return document + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--m49-result", type=Path, required=True) + parser.add_argument("--graph-frames", type=Path, required=True) + parser.add_argument("--tgs-timing", type=Path, required=True) + parser.add_argument("--vegetation-result", type=Path, required=True) + parser.add_argument("--vegetation-frames", type=Path, required=True) + parser.add_argument("--telemetry", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--release-sha256", required=True) + arguments = parser.parse_args() + result = build( + profile_path=arguments.profile, + m49_result_path=arguments.m49_result, + graph_frames_path=arguments.graph_frames, + tgs_timing_path=arguments.tgs_timing, + vegetation_result_path=arguments.vegetation_result, + vegetation_frames_path=arguments.vegetation_frames, + telemetry_path=arguments.telemetry, + output_path=arguments.output, + release_sha256=arguments.release_sha256, + ) + print(json.dumps({"result_id": result["result_id"], "status": result["status"]})) + return 0 if result["status"] == "passed" else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py b/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py new file mode 100644 index 0000000..cc51bf4 --- /dev/null +++ b/scripts/build_lab_v1_vegetation_integrated_worker_artifact.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Build a clean-revision Worker 006 release for the DDRNet + M49 load gate.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +SCRIPT_ROOT = Path(__file__).resolve().parent +if str(SCRIPT_ROOT) not in sys.path: + sys.path.insert(0, str(SCRIPT_ROOT)) + +from build_m49_tgs_integrated_graph_worker_artifact import ( # noqa: E402 + PATCH_ID, + REPOSITORY_ROOT, + WHEEL_NAME, + ArtifactBuildError, + build_wheel, + git_revision, + materialize_revision, + sha256_file, + write_archive, +) +from build_m49_tgs_integrated_graph_worker_artifact import ( # noqa: E402 + SOURCES as M49_SOURCES, +) + +SOURCES = M49_SOURCES + ( + Path( + "experiments/perception/worker/lab_v1_vegetation_goose/" + "run_goose_vegetation_benchmark.py" + ), + Path( + "experiments/perception/worker/lab_v1_vegetation_goose/" + "run_vegetation_integrated_load.py" + ), + Path( + "experiments/perception/worker/m49_t3_travel/" + "build_vegetation_integrated_graph_evidence.py" + ), + Path("config/perception/lab-v1-goose-vegetation-benchmark-v1.json"), + Path("config/perception/lab-v1-vegetation-mission-policy-v1.json"), + Path("config/perception/lab-v1-vegetation-provider-label-map-v1.json"), + Path("config/perception/lab-v1-vegetation-integrated-shadow-v1.json"), +) + + +def build_artifact( + patch_id: str, + output_directory: Path, + *, + revision: str | None = None, + source_root: Path | None = None, +) -> dict[str, object]: + if PATCH_ID.fullmatch(patch_id) is None: + raise ArtifactBuildError("patch id is invalid") + selected_revision = revision or git_revision() + if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None: + raise ArtifactBuildError("artifact revision is invalid") + with tempfile.TemporaryDirectory(prefix="mission-core-vegetation-integrated-") as directory: + stage = Path(directory) + snapshot = source_root + if snapshot is None: + snapshot = stage / "source" + materialize_revision(selected_revision, snapshot) + sources = tuple(snapshot / relative for relative in SOURCES) + if any(path.is_symlink() or not path.is_file() for path in sources): + raise ArtifactBuildError("release input is not a regular file") + payload = stage / "payload" + payload.mkdir() + wheel = build_wheel(snapshot, stage / "wheel") + copied: list[Path] = [] + for source in sources: + destination = payload / source.name + if destination.exists(): + raise ArtifactBuildError("release payload file names are not unique") + destination.write_bytes(source.read_bytes()) + copied.append(destination) + wheel_destination = payload / WHEEL_NAME + wheel_destination.write_bytes(wheel.read_bytes()) + copied.append(wheel_destination) + release = { + "schema_version": "missioncore.lab-v1-vegetation-integrated-worker-release/v1", + "patch_id": patch_id, + "transition": "lab-v1-vegetation-m49-integrated-shadow/v1", + "code_revision": selected_revision, + "worker_id": "worker-006", + "source_pack_sha256": ( + "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944" + ), + "expected_frames": 4489, + "requested_source_rate_hz": 12.0, + "native_engine_sha256": ( + "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695" + ), + "ddrnet_checkpoint_sha256": ( + "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6" + ), + "images": { + "travel": ( + "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f" + ), + "parity": ( + "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0" + ), + "runtime": ( + "sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794" + ), + "vegetation": ( + "sha256:591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd" + ), + }, + "authority": { + "visual_quality_accepted": False, + "route_truth_available": False, + "traversability_accepted": False, + "physical_free_space_accepted": False, + "commands_enabled": False, + "actuation_allowed": False, + "navigation_or_safety_accepted": False, + "production_accepted": False, + }, + "scope": { + "gauss_or_playcanvas_action": "none", + "durable_worker_action": "none", + "canonical_triton_action": "none", + "heavy_vegetation_candidates": ["ddrnet"], + }, + "files": { + path.name: {"sha256": sha256_file(path), "bytes": path.stat().st_size} + for path in sorted(copied) + }, + } + release_path = payload / "release.json" + release_path.write_text( + json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + payload_files = sorted((*release["files"], release_path.name)) + (stage / "manifest.env").write_text( + f"id={patch_id}\ncomponent=mission-core-worker\ntype=shadow-release\n", + encoding="utf-8", + ) + (stage / "files.txt").write_text( + "\n".join(payload_files) + "\n", encoding="utf-8" + ) + target = output_directory.resolve() / f"nodedc-{patch_id}.tgz" + write_archive(stage, target) + return { + "ok": True, + "patch_id": patch_id, + "artifact": str(target), + "sha256": sha256_file(target), + "code_revision": selected_revision, + "wheel_sha256": release["files"][WHEEL_NAME]["sha256"], + "payload_files": payload_files, + "transition": release["transition"], + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("patch_id") + parser.add_argument( + "--output-directory", + type=Path, + default=REPOSITORY_ROOT / ".runtime/worker-artifacts", + ) + arguments = parser.parse_args() + try: + result = build_artifact(arguments.patch_id, arguments.output_directory) + except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc: + parser.error(str(exc)) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/k1link/laboratory/vegetation_policy_review.py b/src/k1link/laboratory/vegetation_policy_review.py new file mode 100644 index 0000000..19db5aa --- /dev/null +++ b/src/k1link/laboratory/vegetation_policy_review.py @@ -0,0 +1,281 @@ +"""Seal a coarse material + YOLOX + TGS review from an immutable vegetation LAB.""" + +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import shutil +import tempfile +from datetime import UTC, datetime +from pathlib import Path, PurePosixPath +from typing import Any, Final + +from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition +from k1link.laboratory.evidence_report import verify_laboratory_evidence_result +from k1link.laboratory.m49_tgs_full_shadow import read_m49_tgs_full_shadow +from k1link.laboratory.vegetation_mission_policy import ( + load_vegetation_mission_policy, + load_vegetation_provider_label_map, +) +from k1link.laboratory.vegetation_policy_video import build_policy_mask_archive, policy_taxonomy +from k1link.laboratory.vegetation_shadow_lab import ( + LAB_SCHEMA, + RESULT_PREFIX, + canonical_json, + sha256_path, +) + +_DEFINITION: Final = LaboratoryEvidenceDefinition( + work_id="lab-v1-vegetation-shadow", + runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"), + result_id_prefix="lab-v1-vegetation-shadow", + document_name="result.json", + result_schema_version=LAB_SCHEMA, +) +_FRAME_COUNT: Final = 4489 +_MAX_RESULT_BYTES: Final = 1024 * 1024 + + +class VegetationPolicyReviewError(ValueError): + """The sealed inputs cannot form an honest synchronized policy review.""" + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): + raise VegetationPolicyReviewError(f"{label} must be an object") + return value + + +def _read_base(root: Path) -> dict[str, Any]: + candidate = root.resolve(strict=True) + verify_laboratory_evidence_result(_DEFINITION, candidate) + path = candidate / "result.json" + if path.stat().st_size > _MAX_RESULT_BYTES: + raise VegetationPolicyReviewError("base vegetation LAB document is too large") + payload = _object(json.loads(path.read_text("utf-8")), "base vegetation LAB") + route = _object(payload.get("route_video"), "base route video") + authority = _object(payload.get("authority"), "base authority") + if ( + payload.get("schema_version") != LAB_SCHEMA + or payload.get("result_id") != candidate.name + or route.get("frame_count") != _FRAME_COUNT + or route.get("view_kind", "fine-semantic-prediction") + != "fine-semantic-prediction" + or route.get("base_m4_result_id") is None + or authority.get("commands_enabled") is not False + or authority.get("navigation_or_safety_accepted") is not False + or authority.get("actuation_accepted") is not False + or authority.get("camera_semantics_can_clear_rigid_geometry") is not False + ): + raise VegetationPolicyReviewError("base vegetation LAB contract changed") + return payload + + +def _copy_verified_artifacts( + *, + source_root: Path, + destination_root: Path, + artifacts: object, +) -> list[dict[str, object]]: + if not isinstance(artifacts, list): + raise VegetationPolicyReviewError("base artifact catalog changed") + copied: list[dict[str, object]] = [] + for raw in artifacts: + descriptor = _object(raw, "base artifact") + relative_text = descriptor.get("path") + expected_sha256 = descriptor.get("sha256") + if not isinstance(relative_text, str) or not isinstance(expected_sha256, str): + raise VegetationPolicyReviewError("base artifact proof changed") + relative = PurePosixPath(relative_text) + source = source_root.joinpath(*relative.parts) + destination = destination_root.joinpath(*relative.parts) + if ( + relative.is_absolute() + or str(relative) != relative_text + or any(part in {"", ".", ".."} for part in relative.parts) + or source.is_symlink() + or not source.is_file() + or sha256_path(source) != expected_sha256 + ): + raise VegetationPolicyReviewError("base artifact changed") + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + shutil.copyfile(source, destination) + copied.append(copy.deepcopy(descriptor)) + return copied + + +def seal_vegetation_policy_review( + *, + base_lab_root: Path, + mission_policy_path: Path, + provider_label_map_path: Path, + m49_tgs_full_shadow_root: Path, + output_root: Path, + created_at_utc: str | None = None, +) -> Path: + base_root = base_lab_root.resolve(strict=True) + base = _read_base(base_root) + base_route = _object(base["route_video"], "base route video") + repository_root = mission_policy_path.resolve().parents[2] + mission_policy = load_vegetation_mission_policy( + mission_policy_path.resolve(strict=True), + repository_root=repository_root, + ) + provider_map = load_vegetation_provider_label_map( + provider_label_map_path.resolve(strict=True), + policy=mission_policy, + ) + tgs = read_m49_tgs_full_shadow(m49_tgs_full_shadow_root) + tgs_source = _object(tgs.report.get("source"), "full TGS source") + tgs_timeline = _object(tgs.report.get("timeline"), "full TGS timeline") + if ( + tgs_source.get("source_id") != "RAVNOVES00" + or tgs_source.get("linked_visual_result_id") != base_route.get("base_m4_result_id") + or tgs_timeline.get("frame_count") != _FRAME_COUNT + ): + raise VegetationPolicyReviewError("TGS and vegetation timelines differ") + + raw_archive = _object(base_route.get("mask_archive"), "fine mask archive") + if raw_archive.get("path") != "video/ddrnet-semantic-masks.zip": + raise VegetationPolicyReviewError("fine mask archive identity changed") + raw_archive_path = base_root / "video" / "ddrnet-semantic-masks.zip" + fine_taxonomy = _object(base_route.get("taxonomy"), "fine taxonomy") + + output_root.mkdir(mode=0o700, parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-policy-", dir=output_root)) + try: + artifacts = _copy_verified_artifacts( + source_root=base_root, + destination_root=temporary, + artifacts=base.get("artifacts"), + ) + policy_archive = temporary / "video" / "coarse-material-policy-masks.zip" + policy_counts = build_policy_mask_archive( + source_archive=raw_archive_path, + destination_archive=policy_archive, + fine_taxonomy=fine_taxonomy, + provider_label_map=provider_map, + ) + policy_archive_proof = { + "role": "route-coarse-material-mask-archive", + "path": "video/coarse-material-policy-masks.zip", + "byte_length": policy_archive.stat().st_size, + "sha256": sha256_path(policy_archive), + "media_type": "application/zip", + } + artifacts.append(policy_archive_proof) + + route = copy.deepcopy(base_route) + route.update( + { + "view_kind": "coarse-material-policy-review", + "source_mask_archive": copy.deepcopy(raw_archive), + "mask_archive": { + "path": policy_archive_proof["path"], + "sha256": policy_archive_proof["sha256"], + "byte_length": policy_archive_proof["byte_length"], + }, + "taxonomy": policy_taxonomy(), + "aggregate_prediction_pixels": policy_counts, + "linked_tgs_result_id": tgs.result_id, + "policy": { + "profile_id": mission_policy["profile_id"], + "profile_sha256": sha256_path(mission_policy_path), + "provider_label_map_id": provider_map["profile_id"], + "provider_label_map_sha256": sha256_path(provider_label_map_path), + "presets": mission_policy["presets"], + "precedence": mission_policy["precedence"], + }, + "fusion": { + "mode": "synchronised-multilayer-review", + "pixel_raster_fusion": False, + "camera_material_layer": "DDRNet fine-64 to coarse material evidence", + "camera_safety_veto_layer": "frozen M4 YOLOX camera proposals", + "spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap", + "temporal_consensus_owner": "TGS causal rolling 1 s and metric obstacle tracks", + "camera_semantic_temporal_filter": "none", + "reason": "No admitted TGS-to-camera pixel projection exists.", + }, + } + ) + identity = copy.deepcopy(_object(base.get("identity"), "base identity")) + identity.update( + { + "base_result_id": base_root.name, + "route_video": route, + } + ) + identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest() + result_id = f"{RESULT_PREFIX}{identity_sha256}" + manifest = copy.deepcopy(base) + manifest.update( + { + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc or datetime.now(UTC).isoformat(), + "identity": identity, + "route_video": route, + "method": { + "completeness": "complete", + "execution_class": "ai-inference-plus-deterministic-adapter", + "pipeline_id": "goose-fine64-to-coarse-material-plus-yolox-tgs-review/v1", + }, + "decision": { + **_object(base.get("decision"), "base decision"), + "multilayer_policy_review_ready": True, + "navigation_accepted": False, + "production_accepted": False, + }, + "limitations": [ + "GOOSE validation is external-domain qualification, not RAVNOVES ground truth.", + ( + "The coarse material playback is derived from per-frame DDRNet " + "predictions and has no RAVNOVES truth." + ), + ( + "Vegetation semantics never clears YOLOX, LiDAR, metric obstacle " + "or TGS vetoes." + ), + "Undefined pixels outside the 600x600 center crop remain fail-closed.", + ( + "TGS remains in gravity-local space; no uncalibrated pixel " + "projection is fabricated." + ), + ( + "Temporal consensus comes from causal TGS and metric tracks; " + "the camera material mask is not temporally filtered." + ), + ], + "artifacts": artifacts, + } + ) + (temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n") + destination = output_root / result_id + if destination.exists(): + raise VegetationPolicyReviewError("immutable vegetation policy result already exists") + temporary.replace(destination) + verify_laboratory_evidence_result(_DEFINITION, destination) + return destination + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base-lab-root", type=Path, required=True) + parser.add_argument("--mission-policy-path", type=Path, required=True) + parser.add_argument("--provider-label-map-path", type=Path, required=True) + parser.add_argument("--m49-tgs-full-shadow-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + print(seal_vegetation_policy_review(**vars(args))) + + +if __name__ == "__main__": + main() + + +__all__ = ["VegetationPolicyReviewError", "seal_vegetation_policy_review"] diff --git a/src/k1link/laboratory/vegetation_policy_video.py b/src/k1link/laboratory/vegetation_policy_video.py new file mode 100644 index 0000000..ef4f97b --- /dev/null +++ b/src/k1link/laboratory/vegetation_policy_video.py @@ -0,0 +1,206 @@ +"""Build a deterministic coarse material-evidence video from fine GOOSE masks.""" + +from __future__ import annotations + +import io +import zipfile +from pathlib import Path +from typing import Any, Final + +import numpy as np +from PIL import Image + +from k1link.laboratory.vegetation_mission_policy import map_provider_material + +TAXONOMY_SCHEMA: Final = "missioncore.lab-v1-terrain-policy-taxonomy/v1" +FRAME_COUNT: Final = 4489 +WIDTH: Final = 800 +HEIGHT: Final = 600 + +POLICY_CLASSES: Final = ( + { + "class_id": 0, + "label": "UNOBSERVED / NO MATERIAL CLAIM · NO_GO", + "color_rgb": [147, 151, 159], + "disposition": "ambiguous", + "material_class": None, + "evidence_state": "UNOBSERVED", + }, + { + "class_id": 1, + "label": "SAFETY DETECTOR VETO · NO_GO", + "color_rgb": [255, 104, 112], + "disposition": "labeled", + "material_class": None, + "evidence_state": "RIGID_OR_UNKNOWN_OBSTACLE", + }, + { + "class_id": 2, + "label": "WOODY SHRUB / TREE · NO_GO", + "color_rgb": [232, 56, 126], + "disposition": "labeled", + "material_class": "woody_or_tree", + "evidence_state": "VEGETATION_WITH_RIGID_GEOMETRY", + }, + { + "class_id": 3, + "label": "CULTIVATED VEGETATION · POLICY NO_GO", + "color_rgb": [183, 112, 255], + "disposition": "labeled", + "material_class": "cultivated_vegetation", + "evidence_state": "VEGETATION_POTENTIALLY_TRAVERSABLE", + }, + { + "class_id": 4, + "label": "LOW GRASS · MISSION CANDIDATE", + "color_rgb": [181, 255, 90], + "disposition": "prediction", + "material_class": "grass", + "evidence_state": "VEGETATION_POTENTIALLY_TRAVERSABLE", + }, + { + "class_id": 5, + "label": "HIGH / HERBACEOUS · MISSION CANDIDATE", + "color_rgb": [113, 211, 111], + "disposition": "prediction", + "material_class": "herbaceous_vegetation", + "evidence_state": "VEGETATION_POTENTIALLY_TRAVERSABLE", + }, + { + "class_id": 6, + "label": "BARE SOIL · MISSION CANDIDATE", + "color_rgb": [255, 197, 92], + "disposition": "prediction", + "material_class": "bare_soil", + "evidence_state": "SUPPORTED_GROUND", + }, + { + "class_id": 7, + "label": "HARD SURFACE · MISSION CANDIDATE", + "color_rgb": [84, 169, 255], + "disposition": "prediction", + "material_class": "hard_surface", + "evidence_state": "SUPPORTED_GROUND", + }, + { + "class_id": 8, + "label": "VEGETATION UNKNOWN · NO_GO", + "color_rgb": [207, 124, 255], + "disposition": "labeled", + "material_class": "vegetation_unknown", + "evidence_state": "VEGETATION_UNKNOWN", + }, +) + +_MATERIAL_TO_CLASS: Final = { + "hard_surface": 7, + "bare_soil": 6, + "grass": 4, + "fern": 5, + "herbaceous_vegetation": 5, + "cultivated_vegetation": 3, + "woody_shrub": 2, + "tree_or_trunk": 2, + "vegetation_unknown": 8, +} + + +class VegetationPolicyVideoError(ValueError): + """The fine-mask input cannot be transformed without inventing evidence.""" + + +def policy_taxonomy() -> dict[str, object]: + return { + "schema_version": TAXONOMY_SCHEMA, + "classes": [dict(row) for row in POLICY_CLASSES], + } + + +def fine_to_policy_lut( + fine_taxonomy: dict[str, object], + provider_label_map: dict[str, Any], +) -> np.ndarray: + classes = fine_taxonomy.get("classes") + if not isinstance(classes, list) or len(classes) != 64: + raise VegetationPolicyVideoError("fine taxonomy must contain 64 classes") + lut = np.zeros(256, dtype=np.uint8) + for expected_id, raw in enumerate(classes): + if not isinstance(raw, dict) or raw.get("class_id") != expected_id: + raise VegetationPolicyVideoError("fine taxonomy ordering changed") + label = raw.get("label") + if not isinstance(label, str) or not label: + raise VegetationPolicyVideoError("fine taxonomy label is invalid") + if expected_id == 0: + continue + material = map_provider_material( + provider_label_map, + provider_id="goose-fine-64", + provider_label=label, + ) + lut[expected_id] = _MATERIAL_TO_CLASS.get(material, 0) + return lut + + +def _zip_info(name: str) -> zipfile.ZipInfo: + info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) + info.compress_type = zipfile.ZIP_STORED + info.create_system = 3 + info.external_attr = 0o600 << 16 + return info + + +def build_policy_mask_archive( + *, + source_archive: Path, + destination_archive: Path, + fine_taxonomy: dict[str, object], + provider_label_map: dict[str, Any], +) -> list[int]: + """Map every fine mask to coarse evidence; safety vetoes remain separate layers.""" + + lut = fine_to_policy_lut(fine_taxonomy, provider_label_map) + counts = np.zeros(len(POLICY_CLASSES), dtype=np.int64) + destination_archive.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + try: + with zipfile.ZipFile(source_archive) as source, zipfile.ZipFile( + destination_archive, + "x", + ) as destination: + for sequence in range(FRAME_COUNT): + member = f"masks/frame-{sequence + 1:06d}.png" + with source.open(member) as stream, Image.open(stream) as image: + fine = np.asarray(image.convert("L"), dtype=np.uint8) + if fine.shape != (HEIGHT, WIDTH): + raise VegetationPolicyVideoError( + f"fine mask {member} has shape {fine.shape}, expected {(HEIGHT, WIDTH)}" + ) + coarse = lut[fine] + counts += np.bincount( + coarse.reshape(-1), + minlength=len(POLICY_CLASSES), + ) + buffer = io.BytesIO() + Image.fromarray(coarse, mode="L").save( + buffer, + format="PNG", + compress_level=1, + optimize=False, + ) + destination.writestr(_zip_info(member), buffer.getvalue()) + except (KeyError, OSError, ValueError, zipfile.BadZipFile) as exc: + destination_archive.unlink(missing_ok=True) + raise VegetationPolicyVideoError("fine mask archive is invalid") from exc + return [int(value) for value in counts] + + +__all__ = [ + "FRAME_COUNT", + "HEIGHT", + "POLICY_CLASSES", + "TAXONOMY_SCHEMA", + "VegetationPolicyVideoError", + "WIDTH", + "build_policy_mask_archive", + "fine_to_policy_lut", + "policy_taxonomy", +] diff --git a/src/k1link/laboratory/vegetation_shadow_lab.py b/src/k1link/laboratory/vegetation_shadow_lab.py index bd1d734..7cad6d7 100644 --- a/src/k1link/laboratory/vegetation_shadow_lab.py +++ b/src/k1link/laboratory/vegetation_shadow_lab.py @@ -14,6 +14,15 @@ from pathlib import Path, PurePosixPath from typing import Any, Final from k1link.laboratory.m47_reference_graph import read_m47_reference_graph_lab +from k1link.laboratory.m49_tgs_full_shadow import read_m49_tgs_full_shadow +from k1link.laboratory.vegetation_mission_policy import ( + load_vegetation_mission_policy, + load_vegetation_provider_label_map, +) +from k1link.laboratory.vegetation_policy_video import ( + build_policy_mask_archive, + policy_taxonomy, +) LAB_SCHEMA: Final = "missioncore.lab-v1-vegetation-shadow/v1" WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1" @@ -315,6 +324,9 @@ def seal_vegetation_shadow_lab( output_root: Path, ddrnet_ravnoves_video_root: Path | None = None, m47_reference_graph_lab_root: Path | None = None, + mission_policy_path: Path | None = None, + provider_label_map_path: Path | None = None, + m49_tgs_full_shadow_root: Path | None = None, ) -> Path: roots = { ("ddrnet", "goose"): ddrnet_goose_root.resolve(), @@ -333,6 +345,17 @@ def seal_vegetation_shadow_lab( selected = _selected_candidate(results) if (ddrnet_ravnoves_video_root is None) != (m47_reference_graph_lab_root is None): raise VegetationShadowLabError("full-video Worker and M4.7 roots must be paired") + policy_inputs = ( + mission_policy_path, + provider_label_map_path, + m49_tgs_full_shadow_root, + ) + if any(value is not None for value in policy_inputs) and not all( + value is not None for value in policy_inputs + ): + raise VegetationShadowLabError("policy, provider map and full TGS roots must be paired") + if all(value is not None for value in policy_inputs) and ddrnet_ravnoves_video_root is None: + raise VegetationShadowLabError("policy review requires the full-video DDRNet result") route_video: dict[str, object] | None = None route_video_archive: Path | None = None video_result: dict[str, Any] | None = None @@ -355,6 +378,35 @@ def seal_vegetation_shadow_lab( raise VegetationShadowLabError("M4.7 video binding differs from DDRNet source") route_video["m47_reference_graph_result_id"] = m47.result_id + mission_policy: dict[str, Any] | None = None + provider_label_map: dict[str, Any] | None = None + linked_tgs_result_id: str | None = None + if ( + mission_policy_path is not None + and provider_label_map_path is not None + and m49_tgs_full_shadow_root is not None + and route_video is not None + ): + repository_root = mission_policy_path.resolve().parents[2] + mission_policy = load_vegetation_mission_policy( + mission_policy_path.resolve(), + repository_root=repository_root, + ) + provider_label_map = load_vegetation_provider_label_map( + provider_label_map_path.resolve(), + policy=mission_policy, + ) + tgs = read_m49_tgs_full_shadow(m49_tgs_full_shadow_root) + tgs_source = _object(tgs.report.get("source"), "M4.9 full TGS source") + tgs_timeline = _object(tgs.report.get("timeline"), "M4.9 full TGS timeline") + if ( + tgs_source.get("source_id") != "RAVNOVES00" + or tgs_source.get("linked_visual_result_id") != route_video["base_m4_result_id"] + or tgs_timeline.get("frame_count") != _VIDEO_FRAME_COUNT + ): + raise VegetationShadowLabError("full TGS timeline differs from vegetation video") + linked_tgs_result_id = tgs.result_id + output_root.mkdir(mode=0o700, parents=True, exist_ok=True) temporary = Path(tempfile.mkdtemp(prefix=".lab-v1-vegetation-", dir=output_root)) artifacts: list[dict[str, object]] = [] @@ -471,14 +523,78 @@ def seal_vegetation_shadow_lab( temporary, "video/ddrnet-semantic-masks.zip", artifacts, - role="route-semantic-mask-archive", + role=( + "route-fine-semantic-source-archive" + if mission_policy is not None + else "route-semantic-mask-archive" + ), media_type="application/zip", ) - route_video["mask_archive"] = { + raw_archive_proof = { "path": archive_descriptor["path"], "sha256": archive_descriptor["sha256"], "byte_length": archive_descriptor["byte_length"], } + route_video["mask_archive"] = raw_archive_proof + route_video["view_kind"] = "fine-semantic-prediction" + if ( + mission_policy is not None + and provider_label_map is not None + and linked_tgs_result_id is not None + and mission_policy_path is not None + and provider_label_map_path is not None + ): + policy_archive = temporary / "video" / "coarse-material-policy-masks.zip" + policy_counts = build_policy_mask_archive( + source_archive=route_video_archive, + destination_archive=policy_archive, + fine_taxonomy=_object(route_video["taxonomy"], "fine video taxonomy"), + provider_label_map=provider_label_map, + ) + policy_descriptor = { + "role": "route-coarse-material-mask-archive", + "path": "video/coarse-material-policy-masks.zip", + "byte_length": policy_archive.stat().st_size, + "sha256": sha256_path(policy_archive), + "media_type": "application/zip", + } + artifacts.append(policy_descriptor) + route_video.update( + { + "view_kind": "coarse-material-policy-review", + "source_mask_archive": raw_archive_proof, + "mask_archive": { + "path": policy_descriptor["path"], + "sha256": policy_descriptor["sha256"], + "byte_length": policy_descriptor["byte_length"], + }, + "taxonomy": policy_taxonomy(), + "aggregate_prediction_pixels": policy_counts, + "linked_tgs_result_id": linked_tgs_result_id, + "policy": { + "profile_id": mission_policy["profile_id"], + "profile_sha256": sha256_path(mission_policy_path), + "provider_label_map_id": provider_label_map["profile_id"], + "provider_label_map_sha256": sha256_path( + provider_label_map_path + ), + "presets": mission_policy["presets"], + "precedence": mission_policy["precedence"], + }, + "fusion": { + "mode": "synchronised-multilayer-review", + "pixel_raster_fusion": False, + "camera_material_layer": "DDRNet fine-64 to coarse material evidence", + "camera_safety_veto_layer": "frozen M4 YOLOX camera proposals", + "spatial_safety_veto_layer": "M4.9 full TGS gravity-local costmap", + "temporal_consensus_owner": ( + "TGS causal rolling 1 s and metric obstacle tracks" + ), + "camera_semantic_temporal_filter": "none", + "reason": "No admitted TGS-to-camera pixel projection exists.", + }, + } + ) candidate_metrics: dict[str, object] = {} for candidate in _CANDIDATES: @@ -536,7 +652,11 @@ def seal_vegetation_shadow_lab( "method": { "completeness": "complete", "execution_class": "ai-inference", - "pipeline_id": "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1", + "pipeline_id": ( + "goose-fine64-to-coarse-material-plus-yolox-tgs-review/v1" + if mission_policy is not None + else "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1" + ), }, "metrics": {"candidates": candidate_metrics}, "decision": { @@ -544,14 +664,37 @@ def seal_vegetation_shadow_lab( "visual_shadow_ready": True, "full_video_shadow_ready": route_video is not None, "mission_policy_ready_for_configuration": True, + "multilayer_policy_review_ready": mission_policy is not None, "navigation_accepted": False, "production_accepted": False, }, "limitations": [ "GOOSE validation is external-domain qualification, not RAVNOVES ground truth.", - "The full RAVNOVES DDRNet playback is prediction-only and has no independent labels.", + ( + "The coarse material playback is derived from per-frame DDRNet predictions " + "and has no RAVNOVES truth." + if mission_policy is not None + else ( + "The full RAVNOVES DDRNet playback is prediction-only and has " + "no independent labels." + ) + ), "Vegetation semantics never clears rigid LiDAR/TGS occupancy.", "Undefined pixels outside the 600x600 center crop remain fail-closed.", + *( + [ + ( + "TGS remains in gravity-local space; no uncalibrated pixel " + "projection is fabricated." + ), + ( + "Temporal consensus comes from causal TGS and metric tracks; " + "the camera material mask is not temporally filtered." + ), + ] + if mission_policy is not None + else [] + ), ], "authority": authority, "catalogs": catalogs, @@ -577,6 +720,9 @@ def _parse_args() -> argparse.Namespace: parser.add_argument("--output-root", type=Path, required=True) parser.add_argument("--ddrnet-ravnoves-video-root", type=Path) parser.add_argument("--m47-reference-graph-lab-root", type=Path) + parser.add_argument("--mission-policy-path", type=Path) + parser.add_argument("--provider-label-map-path", type=Path) + parser.add_argument("--m49-tgs-full-shadow-root", type=Path) return parser.parse_args() @@ -590,6 +736,9 @@ def main() -> None: output_root=args.output_root, ddrnet_ravnoves_video_root=args.ddrnet_ravnoves_video_root, m47_reference_graph_lab_root=args.m47_reference_graph_lab_root, + mission_policy_path=args.mission_policy_path, + provider_label_map_path=args.provider_label_map_path, + m49_tgs_full_shadow_root=args.m49_tgs_full_shadow_root, ) print(destination) diff --git a/src/k1link/web/vegetation_shadow_lab_api.py b/src/k1link/web/vegetation_shadow_lab_api.py index 946891c..10647f8 100644 --- a/src/k1link/web/vegetation_shadow_lab_api.py +++ b/src/k1link/web/vegetation_shadow_lab_api.py @@ -93,12 +93,33 @@ def build_vegetation_shadow_lab_router( candidate = _resolve_candidate(root_provider, result_id) manifest = _read_verified(candidate) route_video = manifest.get("route_video") - if not isinstance(route_video, dict) or not 0 <= sequence < 4489: + if ( + not isinstance(route_video, dict) + or route_video.get("frame_count") != 4489 + or not 0 <= sequence < 4489 + ): raise HTTPException(status_code=404, detail="Vegetation video mask not found") archive = route_video.get("mask_archive") - if not isinstance(archive, dict) or archive.get("path") != "video/ddrnet-semantic-masks.zip": + archive_relative = archive.get("path") if isinstance(archive, dict) else None + if not isinstance(archive_relative, str): raise HTTPException(status_code=404, detail="Vegetation video mask not found") - archive_path = candidate / "video" / "ddrnet-semantic-masks.zip" + relative = PurePosixPath(archive_relative) + if ( + relative.is_absolute() + or str(relative) != archive_relative + or any(part in {"", ".", ".."} for part in relative.parts) + or relative.suffix != ".zip" + ): + raise HTTPException(status_code=404, detail="Vegetation video mask not found") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or not any( + isinstance(item, dict) + and item.get("path") == archive_relative + and item.get("media_type") == "application/zip" + for item in artifacts + ): + raise HTTPException(status_code=404, detail="Vegetation video mask not found") + archive_path = candidate.joinpath(*relative.parts) member = f"masks/frame-{sequence + 1:06d}.png" try: before = archive_path.stat() diff --git a/tests/test_lab_v1_vegetation_integrated_shadow.py b/tests/test_lab_v1_vegetation_integrated_shadow.py new file mode 100644 index 0000000..c352f54 --- /dev/null +++ b/tests/test_lab_v1_vegetation_integrated_shadow.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import importlib.util +import json +import tarfile +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +EVIDENCE_PATH = ( + REPOSITORY_ROOT + / "experiments/perception/worker/m49_t3_travel/" + "build_vegetation_integrated_graph_evidence.py" +) +ARTIFACT_PATH = ( + REPOSITORY_ROOT / "scripts/build_lab_v1_vegetation_integrated_worker_artifact.py" +) +RUNNER_PATH = ( + REPOSITORY_ROOT + / "experiments/perception/worker/lab_v1_vegetation_goose/" + "run_vegetation_integrated_load.py" +) +POWERSHELL_PATH = ( + REPOSITORY_ROOT + / "experiments/perception/worker/Invoke-M49TgsIntegratedGraphShadow.ps1" +) + + +def load_module(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +EVIDENCE = load_module("vegetation_integrated_evidence", EVIDENCE_PATH) +ARTIFACT = load_module("vegetation_integrated_artifact", ARTIFACT_PATH) + + +def test_three_layer_gate_joins_exact_frames_and_preserves_false_authority( + tmp_path: Path, +) -> None: + profile = tmp_path / "profile.json" + profile.write_text( + json.dumps( + { + "schema_version": EVIDENCE.PROFILE_SCHEMA, + "profile_id": "test", + "source": {"source_id": "RAVNOVES00", "requested_source_rate_hz": 12.0}, + "stages": { + "m49_graph_tgs": {"profile_sha256": "a" * 64}, + "vegetation": { + "checkpoint_sha256": "b" * 64, + "config_sha256": "c" * 64, + "policy_sha256": "d" * 64, + "provider_map_sha256": "e" * 64, + }, + }, + "acceptance": { + "minimum_graph_world_state_fps": 11.2, + "minimum_vegetation_fps": 11.2, + "maximum_vegetation_completion_p95_ms": 125.0, + "maximum_combined_output_age_p99_ms": 125.0, + }, + "authority": { + "commands_enabled": False, + "actuation_allowed": False, + "navigation_or_safety_accepted": False, + "production_accepted": False, + }, + } + ), + encoding="utf-8", + ) + m49 = tmp_path / "m49.json" + m49.write_text( + json.dumps( + { + "schema_version": EVIDENCE.M49_SCHEMA, + "status": "passed", + "integrated_runtime_gate_passed": True, + "result_id": "m49-test", + "identity": {"profile_sha256": "a" * 64}, + "performance": {"effective_world_state_fps": 11.8}, + "accounting": { + "graph_admitted": EVIDENCE.FRAME_COUNT, + "graph_delivered": EVIDENCE.FRAME_COUNT, + "tgs_timeline_frames": EVIDENCE.FRAME_COUNT, + "tgs_capacity_drops": 0, + }, + } + ), + encoding="utf-8", + ) + vegetation = tmp_path / "vegetation.json" + vegetation.write_text( + json.dumps( + { + "schema_version": EVIDENCE.VEGETATION_SCHEMA, + "result_id": "vegetation-test", + "source": {"requested_source_rate_hz": 12.0}, + "candidate": {"candidate_key": "ddrnet", "checkpoint_sha256": "b" * 64}, + "identity": { + "config_sha256": "c" * 64, + "policy_sha256": "d" * 64, + "provider_map_sha256": "e" * 64, + }, + "execution": { + "frame_count": EVIDENCE.FRAME_COUNT, + "effective_fps": 11.75, + "capacity_drop_count": 0, + }, + "timing": { + "completion_age_ms": {"p95": 25.0}, + "stage_ms": {"p95": 20.0}, + "inference_ms": {"p95": 18.0}, + }, + "resource": {"gpu_name": "test"}, + "authority": { + "commands_enabled": False, + "actuation_allowed": False, + "navigation_or_safety_accepted": False, + "production_accepted": False, + }, + } + ), + encoding="utf-8", + ) + graph_frames = tmp_path / "graph.jsonl" + graph_frames.write_text( + "".join( + json.dumps( + {"source_envelope": {"sequence": index}, "completion_age_ns": 40_000_000} + ) + + "\n" + for index in range(EVIDENCE.FRAME_COUNT) + ), + encoding="utf-8", + ) + tgs_frames = tmp_path / "tgs.tsv" + tgs_frames.write_text( + "timeline_frame_index\tcompletion_age_ms\n" + + "".join(f"{index}\t5.0\n" for index in range(EVIDENCE.FRAME_COUNT)), + encoding="utf-8", + ) + vegetation_frames = tmp_path / "vegetation.jsonl" + vegetation_frames.write_text( + "".join( + json.dumps( + { + "schema_version": "missioncore.lab-v1-vegetation-integrated-frame/v1", + "sequence": index, + "completion_age_ms": 20.0, + } + ) + + "\n" + for index in range(EVIDENCE.FRAME_COUNT) + ), + encoding="utf-8", + ) + telemetry = tmp_path / "telemetry.jsonl" + telemetry.write_text( + "".join( + json.dumps( + { + "role": role, + "cpu_percent": "10.0%", + "memory_usage": "1GiB / 64GiB", + "memory_percent": "1.56%", + } + ) + + "\n" + for role in ("graph", "tgs", "triton", "vegetation") + ), + encoding="utf-8", + ) + output = tmp_path / "result.json" + + result = EVIDENCE.build( + profile_path=profile, + m49_result_path=m49, + graph_frames_path=graph_frames, + tgs_timing_path=tgs_frames, + vegetation_result_path=vegetation, + vegetation_frames_path=vegetation_frames, + telemetry_path=telemetry, + output_path=output, + release_sha256="f" * 64, + ) + + assert result["status"] == "passed" + assert result["source"]["joined_frame_count"] == EVIDENCE.FRAME_COUNT + assert result["performance"]["three_layer_output_age_ms"]["p99"] == 40.0 + assert result["checks"]["authority_remains_false"] is True + assert result["production_accepted"] is False + + +def test_integrated_release_is_deterministic_and_contains_one_vegetation_candidate( + monkeypatch, tmp_path: Path +) -> None: + def fake_wheel(_source_root: Path, output: Path) -> Path: + output.mkdir(parents=True, exist_ok=True) + wheel = output / ARTIFACT.WHEEL_NAME + wheel.write_bytes(b"clean committed wheel\n") + return wheel + + monkeypatch.setattr(ARTIFACT, "build_wheel", fake_wheel) + revision = "f" * 40 + first = ARTIFACT.build_artifact( + "mission-core-vegetation-integrated-unit-001", + tmp_path / "first", + revision=revision, + source_root=REPOSITORY_ROOT, + ) + second = ARTIFACT.build_artifact( + "mission-core-vegetation-integrated-unit-001", + tmp_path / "second", + revision=revision, + source_root=REPOSITORY_ROOT, + ) + + assert Path(first["artifact"]).read_bytes() == Path(second["artifact"]).read_bytes() + with tarfile.open(first["artifact"], "r:gz") as archive: + names = set(archive.getnames()) + release_stream = archive.extractfile("payload/release.json") + assert release_stream is not None + release = json.loads(release_stream.read()) + assert "payload/run_vegetation_integrated_load.py" in names + assert "payload/build_vegetation_integrated_graph_evidence.py" in names + assert release["scope"]["heavy_vegetation_candidates"] == ["ddrnet"] + assert all(value is False for value in release["authority"].values()) + + +def test_worker_gate_reuses_shared_barrier_and_keeps_canonical_triton_unchanged() -> None: + runner = RUNNER_PATH.read_text(encoding="utf-8") + wrapper = POWERSHELL_PATH.read_text(encoding="utf-8") + assert '"source-paced-integrated-shadow/v1"' in runner + assert "wait_for_shared_start(" in runner + assert '"camera_semantics_can_clear_rigid_geometry": False' in runner + assert "$VegetationLoadGate" in wrapper + assert '"vegetation"' in wrapper + assert "if ($canonicalAfter.Id -cne $canonicalId" not in wrapper + assert "$canonicalAfter.Id -cne $canonicalId" in wrapper diff --git a/tests/test_vegetation_shadow_lab.py b/tests/test_vegetation_shadow_lab.py index 3f005da..4197847 100644 --- a/tests/test_vegetation_shadow_lab.py +++ b/tests/test_vegetation_shadow_lab.py @@ -2,6 +2,7 @@ from __future__ import annotations import hashlib import json +import shutil import zipfile from pathlib import Path from types import SimpleNamespace @@ -9,9 +10,11 @@ from types import SimpleNamespace from fastapi import FastAPI from fastapi.testclient import TestClient -from k1link.laboratory import LaboratoryEvidenceRegistry +import k1link.laboratory.vegetation_policy_review as policy_review_module import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module +from k1link.laboratory import LaboratoryEvidenceRegistry from k1link.laboratory.evidence_report import verify_laboratory_evidence_result +from k1link.laboratory.vegetation_policy_review import seal_vegetation_policy_review from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router @@ -229,3 +232,95 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence( client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}").status_code == 503 ) + + +def test_policy_review_reuses_sealed_video_and_links_yolox_tgs( + tmp_path: Path, + monkeypatch, +) -> None: + roots = {} + for candidate, vegetation_iou in (("ddrnet", 0.64), ("ppliteseg", 0.61)): + for mode in ("goose", "ravnoves"): + root = tmp_path / "worker" / f"{candidate}-{mode}" + _worker_result(root, candidate=candidate, mode=mode, vegetation_iou=vegetation_iou) + roots[(candidate, mode)] = root + video_root = tmp_path / "worker" / "ddrnet-ravnoves-video" + _video_worker_result(video_root) + m47_root = tmp_path / f"m47-reference-graph-lab-{'a' * 64}" + m47_root.mkdir() + base_m4_result_id = f"m4-threat-replay-{'f' * 64}" + monkeypatch.setattr( + vegetation_lab_module, + "read_m47_reference_graph_lab", + lambda _root: SimpleNamespace( + result_id=m47_root.name, + report={ + "source": {"source_id": "RAVNOVES00"}, + "visual_evidence": { + "linked_result_id": base_m4_result_id, + "timeline_frames": 4489, + }, + }, + ), + ) + base_root = seal_vegetation_shadow_lab( + ddrnet_goose_root=roots[("ddrnet", "goose")], + ppliteseg_goose_root=roots[("ppliteseg", "goose")], + ddrnet_ravnoves_root=roots[("ddrnet", "ravnoves")], + ppliteseg_ravnoves_root=roots[("ppliteseg", "ravnoves")], + output_root=tmp_path / "results", + ddrnet_ravnoves_video_root=video_root, + m47_reference_graph_lab_root=m47_root, + ) + tgs_result_id = f"m49-tgs-full-shadow-{'9' * 64}" + monkeypatch.setattr( + policy_review_module, + "read_m49_tgs_full_shadow", + lambda _root: SimpleNamespace( + result_id=tgs_result_id, + report={ + "source": { + "source_id": "RAVNOVES00", + "linked_visual_result_id": base_m4_result_id, + }, + "timeline": {"frame_count": 4489}, + }, + ), + ) + + def fake_policy_archive(**kwargs) -> list[int]: + shutil.copyfile(kwargs["source_archive"], kwargs["destination_archive"]) + return [4489 * 800 * 600, *([0] * 8)] + + monkeypatch.setattr(policy_review_module, "build_policy_mask_archive", fake_policy_archive) + result_root = seal_vegetation_policy_review( + base_lab_root=base_root, + mission_policy_path=REPOSITORY_ROOT + / "config/perception/lab-v1-vegetation-mission-policy-v1.json", + provider_label_map_path=REPOSITORY_ROOT + / "config/perception/lab-v1-vegetation-provider-label-map-v1.json", + m49_tgs_full_shadow_root=tmp_path / "sealed-tgs", + output_root=tmp_path / "results", + created_at_utc="2026-08-28T08:00:00+00:00", + ) + manifest = json.loads((result_root / "result.json").read_text("utf-8")) + route = manifest["route_video"] + assert route["view_kind"] == "coarse-material-policy-review" + assert route["linked_tgs_result_id"] == tgs_result_id + assert route["fusion"]["pixel_raster_fusion"] is False + assert route["fusion"]["camera_semantic_temporal_filter"] == "none" + assert route["taxonomy"]["schema_version"] == ( + "missioncore.lab-v1-terrain-policy-taxonomy/v1" + ) + assert len(route["taxonomy"]["classes"]) == 9 + assert len(manifest["artifacts"]) == 79 + assert manifest["authority"]["commands_enabled"] is False + assert manifest["decision"]["multilayer_policy_review_ready"] is True + + app = FastAPI() + app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent)) + response = TestClient(app).get( + f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/masks/0" + ) + assert response.status_code == 200 + assert response.content == b"\x89PNG\r\n\x1a\n"