feat(perception): add autonomous vegetation shadow lab

This commit is contained in:
DCCONSTRUCTIONS
2026-08-27 23:26:35 +03:00
parent 7594c71dd1
commit 57204b3b0b
26 changed files with 2494 additions and 5 deletions
@@ -48,8 +48,10 @@ import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
import { fetchVegetationShadowResult } from "./vegetationShadow";
export type AdvancedLaboratoryWorkId =
| "lab-v1-vegetation-shadow"
| "m48-object-centric-quality"
| "m48-small-static-passage-regression"
| "m48-static-occupancy-qualification"
@@ -100,6 +102,7 @@ export interface AdvancedLaboratoryIndexItem {
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"lab-v1-vegetation-shadow",
"m48-object-centric-quality",
"m48-small-static-passage-regression",
"m48-static-occupancy-qualification",
@@ -145,6 +148,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow",
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
"m48-static-occupancy-qualification": "m48-static-occupancy-qualification",
@@ -197,6 +201,7 @@ export function isAdvancedLaboratoryWorkId(
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
vegetationShadow: null,
m47Graph: null,
m48: null,
m48SmallStatic: null,
@@ -330,7 +335,8 @@ export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "m48-object-centric-quality" ? results.m48 !== null
return workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null
: workId === "m48-object-centric-quality" ? results.m48 !== null
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
: workId === "m48r3-static-occupancy-shadow" ? results.m48r3StaticOccupancy !== null
@@ -387,7 +393,10 @@ export async function fetchAdvancedLaboratoryResult(
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "m48-object-centric-quality") {
if (workId === "lab-v1-vegetation-shadow") {
if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана.");
results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal });
} else if (workId === "m48-object-centric-quality") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8 lifecycle evidence identity не выбрана.");
results.m48 = await fetchM48LifecycleResult(resultId, { fetcher, signal });
} else if (workId === "m48-small-static-passage-regression") {
@@ -42,8 +42,10 @@ import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
import type { M49TgsFailClosedResult } from "./m49TgsFailClosed";
import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
import type { VegetationShadowResult } from "./vegetationShadow";
export interface AdvancedLaboratoryResults {
vegetationShadow: VegetationShadowResult | null;
m47Graph: M47ReferenceGraphLabResult | null;
m48: M48AdvancedResult | null;
m48SmallStatic: M48SmallStaticRegressionResult | null;
@@ -967,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
const e39 = settledCatalogValue(settled[7]);
const e40 = settledCatalogValue(settled[8]);
return {
vegetationShadow: null,
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
m48r3StaticOccupancy: null,
m48s: null, m48t: null, m49Tgs: null, m49TgsFull: null, m4Threat: null,
@@ -0,0 +1,278 @@
import type { LaboratoryFetch } from "./advancedResults";
const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const CANDIDATES = ["ddrnet", "ppliteseg"] as const;
const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const;
const VALIDATION_MODES = ["source", "truth", "ddrnet", "ppliteseg"] as const;
export type VegetationCandidateKey = typeof CANDIDATES[number];
export type VegetationRouteMode = typeof ROUTE_MODES[number];
export type VegetationValidationMode = typeof VALIDATION_MODES[number];
export interface VegetationCandidateMetrics {
candidate: VegetationCandidateKey;
loadedModelName: string;
checkpointSha256: string;
meanIouPercent: number;
publishedMeanIouPercent: number;
vegetationMeanIouPercent: number;
validationLatencyP95Ms: number;
validationThroughputFps: number;
shadowLatencyP95Ms: number;
shadowThroughputFps: number;
shadowPrewarmLatencyMs: number;
peakReservedVramBytes: number;
gpuName: string;
}
export interface VegetationVisualCase {
caseId: string;
sourceKind: "goose" | "ravnoves";
width: number;
height: number;
centerCropXyxy: readonly [number, number, number, number];
outsideCropState: "undefined" | "not-applicable";
assets: Readonly<Record<string, string>>;
}
export interface VegetationShadowResult {
resultId: string;
createdAtUtc: string;
status: "visual-shadow-ready-policy-not-authorized";
selectedCandidate: VegetationCandidateKey;
candidates: readonly VegetationCandidateMetrics[];
routeCases: readonly VegetationVisualCase[];
validationCases: readonly VegetationVisualCase[];
limitations: readonly string[];
visualShadowReady: true;
missionPolicyReadyForConfiguration: true;
authority: {
commandsEnabled: false;
navigationOrSafetyAccepted: false;
actuationAccepted: false;
cameraSemanticsCanClearRigidGeometry: false;
};
}
export class VegetationShadowContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new VegetationShadowContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function arrayValue(value: unknown, label: string): readonly unknown[] {
if (!Array.isArray(value)) throw new VegetationShadowContractError(`${label}: ожидался массив.`);
return value;
}
function textValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new VegetationShadowContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new VegetationShadowContractError(`${label}: ожидалось число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new VegetationShadowContractError(`${label}: ожидалось неотрицательное целое.`);
}
return parsed;
}
function exact(value: unknown, expected: unknown, label: string): void {
if (value !== expected) {
throw new VegetationShadowContractError(`${label}: контракт изменён.`);
}
}
function candidateKey(value: unknown, label: string): VegetationCandidateKey {
if (value !== "ddrnet" && value !== "ppliteseg") {
throw new VegetationShadowContractError(`${label}: неизвестная модель.`);
}
return value;
}
function candidateMetricsValue(
value: unknown,
candidate: VegetationCandidateKey,
): VegetationCandidateMetrics {
const row = objectValue(value, `vegetation.metrics.${candidate}`);
const validation = objectValue(row.validation_metrics, `${candidate}.validation_metrics`);
const validationTiming = objectValue(row.validation_timing, `${candidate}.validation_timing`);
const shadowTiming = objectValue(row.shadow_timing, `${candidate}.shadow_timing`);
const resource = objectValue(row.resource, `${candidate}.resource`);
const checkpointSha256 = textValue(row.checkpoint_sha256, `${candidate}.checkpoint_sha256`);
if (!SHA256.test(checkpointSha256)) {
throw new VegetationShadowContractError(`${candidate}.checkpoint_sha256: digest invalid.`);
}
return {
candidate,
loadedModelName: textValue(row.loaded_model_name, `${candidate}.loaded_model_name`),
checkpointSha256,
meanIouPercent: numberValue(validation.mean_iou_percent, `${candidate}.mean_iou_percent`),
publishedMeanIouPercent: numberValue(
validation.published_mean_iou_percent,
`${candidate}.published_mean_iou_percent`,
),
vegetationMeanIouPercent: numberValue(validation.vegetation_mean_iou, `${candidate}.vegetation_mean_iou`) * 100,
validationLatencyP95Ms: numberValue(validationTiming.latency_ms_p95, `${candidate}.validation_latency_p95`),
validationThroughputFps: numberValue(
validationTiming.throughput_fps_from_mean_inference,
`${candidate}.validation_throughput`,
),
shadowLatencyP95Ms: numberValue(shadowTiming.latency_ms_p95, `${candidate}.shadow_latency_p95`),
shadowThroughputFps: numberValue(
shadowTiming.throughput_fps_from_mean_inference,
`${candidate}.shadow_throughput`,
),
shadowPrewarmLatencyMs: numberValue(
shadowTiming.prewarm_latency_ms,
`${candidate}.shadow_prewarm_latency`,
),
peakReservedVramBytes: integerValue(resource.peak_reserved_vram_bytes, `${candidate}.vram`),
gpuName: textValue(resource.gpu_name, `${candidate}.gpu_name`),
};
}
function visualCaseValue(
value: unknown,
resultId: string,
expectedKind: "goose" | "ravnoves",
): VegetationVisualCase {
const row = objectValue(value, `vegetation.${expectedKind}.case`);
exact(row.source_kind, expectedKind, "vegetation.case.source_kind");
const caseId = textValue(row.case_id, "vegetation.case.case_id");
const crop = arrayValue(row.center_crop_xyxy, "vegetation.case.center_crop_xyxy")
.map((item, index) => integerValue(item, `vegetation.case.crop[${index}]`));
if (crop.length !== 4) {
throw new VegetationShadowContractError("vegetation.case.center_crop_xyxy: размер изменён.");
}
const assets = objectValue(row.assets, "vegetation.case.assets");
const projected: Record<string, string> = {};
for (const [key, raw] of Object.entries(assets)) {
const descriptor = objectValue(raw, `vegetation.case.assets.${key}`);
const path = textValue(descriptor.path, `vegetation.case.assets.${key}.path`);
const sha256 = textValue(descriptor.sha256, `vegetation.case.assets.${key}.sha256`);
if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) {
throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`);
}
projected[key] = `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/assets/${path
.split("/")
.map(encodeURIComponent)
.join("/")}`;
}
const expectedAssets = expectedKind === "goose"
? VALIDATION_MODES
: ROUTE_MODES;
if (expectedAssets.some((key) => !projected[key])) {
throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`);
}
const outsideCropState = row.outside_crop_state;
if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") {
throw new VegetationShadowContractError("vegetation.case.outside_crop_state: контракт изменён.");
}
return {
caseId,
sourceKind: expectedKind,
width: integerValue(row.width, "vegetation.case.width"),
height: integerValue(row.height, "vegetation.case.height"),
centerCropXyxy: crop as unknown as readonly [number, number, number, number],
outsideCropState,
assets: projected,
};
}
function parseResult(value: unknown, resultId: string): VegetationShadowResult {
const payload = objectValue(value, "Vegetation LAB");
exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema");
exact(payload.result_id, resultId, "vegetation.result_id");
exact(payload.status, "visual-shadow-ready-policy-not-authorized", "vegetation.status");
exact(payload.ground_truth, false, "vegetation.ground_truth");
exact(payload.access, "read-only", "vegetation.access");
const identity = objectValue(payload.identity, "vegetation.identity");
const metrics = objectValue(payload.metrics, "vegetation.metrics");
const candidates = objectValue(metrics.candidates, "vegetation.metrics.candidates");
const decision = objectValue(payload.decision, "vegetation.decision");
const authority = objectValue(payload.authority, "vegetation.authority");
const catalogs = objectValue(payload.catalogs, "vegetation.catalogs");
const selectedCandidate = candidateKey(identity.selected_candidate, "vegetation.selected_candidate");
exact(decision.selected_candidate, selectedCandidate, "vegetation.decision.selected_candidate");
exact(decision.visual_shadow_ready, true, "vegetation.decision.visual_shadow_ready");
exact(
decision.mission_policy_ready_for_configuration,
true,
"vegetation.decision.mission_policy_ready_for_configuration",
);
exact(decision.navigation_accepted, false, "vegetation.decision.navigation_accepted");
exact(decision.production_accepted, false, "vegetation.decision.production_accepted");
exact(authority.commands_enabled, false, "vegetation.authority.commands_enabled");
exact(
authority.navigation_or_safety_accepted,
false,
"vegetation.authority.navigation_or_safety_accepted",
);
exact(authority.actuation_accepted, false, "vegetation.authority.actuation_accepted");
exact(
authority.camera_semantics_can_clear_rigid_geometry,
false,
"vegetation.authority.camera_semantics_can_clear_rigid_geometry",
);
const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves")
.map((item) => visualCaseValue(item, resultId, "ravnoves"));
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
.map((item) => visualCaseValue(item, resultId, "goose"));
if (routeCases.length !== 12 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 + 12 случаев.");
}
return {
resultId,
createdAtUtc: textValue(payload.created_at_utc, "vegetation.created_at_utc"),
status: "visual-shadow-ready-policy-not-authorized",
selectedCandidate,
candidates: CANDIDATES.map((candidate) => candidateMetricsValue(candidates[candidate], candidate)),
routeCases,
validationCases,
limitations: arrayValue(payload.limitations, "vegetation.limitations")
.map((item, index) => textValue(item, `vegetation.limitations[${index}]`)),
visualShadowReady: true,
missionPolicyReadyForConfiguration: true,
authority: {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
actuationAccepted: false,
cameraSemanticsCanClearRigidGeometry: false,
},
};
}
export async function fetchVegetationShadowResult(
resultId: string,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationShadowResult> {
if (!RESULT_ID.test(resultId)) {
throw new VegetationShadowContractError("Vegetation LAB identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`);
}
return parseResult(await response.json(), resultId);
}
@@ -50,6 +50,7 @@ import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
import { VegetationShadowResultView } from "./VegetationShadowResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -92,6 +93,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) {
return <VegetationShadowResultView rigLabel={rigLabel} result={results.vegetationShadow} />;
}
if (workId === "m48-object-centric-quality" && results.m48) {
return <M48ObjectCentricQualityResultView rigLabel={rigLabel} result={results.m48} />;
}
@@ -0,0 +1,135 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
import {
VegetationRouteVisual,
VegetationValidationVisual,
} from "./VegetationShadowVisual";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
export function VegetationShadowResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: VegetationShadowResult;
}) {
const selected = result.candidates.find(
(candidate) => candidate.candidate === result.selectedCandidate,
)!;
const alternative = result.candidates.find(
(candidate) => candidate.candidate !== result.selectedCandidate,
)!;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB V1 · растительность и mission-policy пресеты"
description="Две готовые fine-64 модели GOOSE проверены на полном validation split и перенесены в автономный визуальный shadow по RAVNOVES00. Интерфейс читает sealed-кадры локально и не зависит от доступности Worker 006."
status="Визуальный shadow готов · navigation authority OFF"
statusTone="warning"
facts={[
{ label: "Источник", value: `${rigLabel} RIGHT · 12 raw KB4 кадров + GOOSE validation 962` },
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
{ label: "Policy", value: "Urban / rural / off-road · mission-configurable" },
{ label: "Authority", value: "SHADOW ONLY · commands OFF · geometry stays authoritative" },
]}
brief={{
question: "Можно ли взять готовую сегментацию растительности, увидеть её на нашем маршруте и сразу проверить разные правила миссии?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime: сначала 962 размеченных GOOSE-кадра, затем 12 детерминированных кадров RAVNOVES00. Для каждого кадра запечатаны source, обе семантики и три policy-проекции.",
principalResult: `${selected.loadedModelName} выбран по vegetation IoU ${decimal(selected.vegetationMeanIouPercent, 2)}% при shadow p95 ${decimal(selected.shadowLatencyP95Ms, 2)} ms. Визуальный результат доступен локально без Worker.`,
limitation: "RAVNOVES00 не размечен по fine-64, поэтому это перенос и визуальная проверка, а не доказательство точности или безопасности. Камерная семантика не может очищать жёсткую LiDAR/TGS occupancy.",
}}
method={{
completeness: "complete",
executionClass: "ai-inference",
pipelineId: "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1",
components: result.candidates.map((candidate) => ({
kind: "model" as const,
name: candidate.loadedModelName,
version: candidate.candidate,
role: candidate.candidate === result.selectedCandidate ? "selected policy provider" : "comparison candidate",
identitySha256: candidate.checkpointSha256,
})),
}}
/>
)}
evidence={(
<>
<LaboratoryEvidence
eyebrow="RAVNOVES00 · AUTONOMOUS VISUAL SHADOW"
title="Источник, обе модели и три правила миссии на одинаковых кадрах"
kind="diagnostic-model"
resizable
>
<VegetationRouteVisual result={result} />
</LaboratoryEvidence>
<LaboratoryEvidence
eyebrow="GOOSE · EXTERNAL VALIDATION EVIDENCE"
title="Независимая разметка: truth против DDRNet и PPLiteSeg"
kind="diagnostic-model"
resizable
>
<VegetationValidationVisual result={result} />
</LaboratoryEvidence>
</>
)}
result={(
<LaboratoryResultSummary
title="Готовые веса дают рабочую точку старта, но ещё не право ехать"
status={`${selected.loadedModelName} выбран для shadow`}
statusTone="warning"
metrics={[
{
label: "GOOSE mIoU",
value: `${decimal(selected.meanIouPercent, 2)}% / ${decimal(alternative.meanIouPercent, 2)}%`,
hint: `${selected.candidate} / ${alternative.candidate} · полный validation split`,
},
{
label: "Vegetation IoU",
value: `${decimal(selected.vegetationMeanIouPercent, 2)}% / ${decimal(alternative.vegetationMeanIouPercent, 2)}%`,
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
},
{
label: "RAVNOVES p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
hint: "чистый inference · одна тяжёлая модель за раз",
},
{
label: "Cold prewarm",
value: `${decimal(selected.shadowPrewarmLatencyMs, 1)} / ${decimal(alternative.shadowPrewarmLatencyMs, 1)} ms`,
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
},
{
label: "RAVNOVES throughput",
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
hint: "изолированный Worker 006 · не realtime graph целиком",
},
{
label: "Peak VRAM",
value: `${decimal(selected.peakReservedVramBytes / 1024 ** 3, 2)} / ${decimal(alternative.peakReservedVramBytes / 1024 ** 3, 2)} GiB`,
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
},
{
label: "Autonomous evidence",
value: "12 route + 12 validation",
hint: "только текущий кадр загружается в viewer; Worker не требуется",
},
]}
conclusion={{
proved: "Обе официальные fine-64 модели запускаются на Worker 006, проходят полный GOOSE validation и дают воспроизводимые растительные маски на 12 фиксированных RAVNOVES00 кадрах. Urban/rural/off-road policy-проекции формируются без повторного inference.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, различение тонкой травы от толстого ствола во всех условиях, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить выбранную модель как shadow provider. Следующий критический блок — разметить небольшой hard-case island нашего офф-роуда: трава, папоротник, куст с толстыми стволами и дерево; затем калибровать policy без ослабления LiDAR/TGS fail-closed геометрии.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,157 @@
import { useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
VegetationRouteMode,
VegetationShadowResult,
VegetationValidationMode,
VegetationVisualCase,
} from "../../core/laboratory/vegetationShadow";
const ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
{ value: "urban", label: "URBAN" },
{ value: "rural", label: "RURAL" },
{ value: "offroad", label: "OFF-ROAD" },
] as const;
const VALIDATION_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
function VegetationScene({
item,
mode,
}: {
item: VegetationVisualCase;
mode: string;
}) {
const overlay = mode === "source" ? null : item.assets[mode];
return (
<div className="recorded-evidence-image-scene">
<img src={item.assets.source} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function VegetationRouteVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationRouteMode>("offroad");
const [expanded, setExpanded] = useState(false);
const item = result.routeCases[index] ?? null;
const selected = result.candidates.find(
(candidate) => candidate.candidate === result.selectedCandidate,
);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES00 vegetation policy shadow"
className="m48-atlas-visual"
mode={mode}
modes={ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.routeCases.length
) % result.routeCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (current + 1) % result.routeCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
<strong>RAVNOVES00 · {item.caseId} · {mode.toUpperCase()}</strong>
<small>
{selected?.loadedModelName ?? result.selectedCandidate} policy provider
{" · "}center crop 600×600
{" · "}outside crop UNKNOWN
</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
RAVNOVES vegetation shadow каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
export function VegetationValidationVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationValidationMode>("truth");
const [expanded, setExpanded] = useState(false);
const item = result.validationCases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="GOOSE validation vegetation comparison"
className="m48-atlas-visual"
mode={mode}
modes={VALIDATION_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.validationCases.length
) % result.validationCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (current + 1) % result.validationCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE VALIDATION</StatusBadge>
<strong>{item.caseId} · {mode.toUpperCase()}</strong>
<small>official fine-64 labels · fixed 512×512 preprocessing · visual sample</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
GOOSE validation каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
@@ -63,6 +63,13 @@ interface KnownWorkDefinition {
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"lab-v1-vegetation-shadow": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 vegetation shadow`,
experimentId: "lab-v1-vegetation-mission-policy",
experimentName: "GOOSE ready weights → RAVNOVES vegetation policy",
variantName: "LAB V1 · DDRNet vs PPLiteSeg · urban/rural/off-road presets",
},
"m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + prediction-free spatial evidence`,
@@ -18,6 +18,7 @@ function mergeResults(
next: AdvancedLaboratoryResults,
): AdvancedLaboratoryResults {
return {
vegetationShadow: next.vegetationShadow ?? current.vegetationShadow,
m47Graph: next.m47Graph ?? current.m47Graph,
m48: next.m48 ?? current.m48,
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
@@ -121,6 +122,7 @@ export function useAdvancedLaboratoryCatalog({
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
if (
[
"lab-v1-vegetation-shadow",
"m47-reference-graph-shadow",
"m48-object-centric-quality",
"m48-small-static-passage-regression",
@@ -0,0 +1,125 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchVegetationShadowResult;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ fetchVegetationShadowResult } = await server.ssrLoadModule(
"/src/core/laboratory/vegetationShadow.ts",
));
});
after(async () => {
await server?.close();
});
const resultId = `lab-v1-vegetation-shadow-${"a".repeat(64)}`;
function candidate(candidateKey, vegetationIou) {
return {
loaded_model_name: candidateKey === "ddrnet" ? "ddrnet_39" : "pp_lite_t_seg",
checkpoint_sha256: (candidateKey === "ddrnet" ? "b" : "c").repeat(64),
validation_metrics: {
mean_iou_percent: 44.2,
published_mean_iou_percent: 46.53,
vegetation_mean_iou: vegetationIou,
},
validation_timing: {
latency_ms_p95: 22.4,
throughput_fps_from_mean_inference: 48.1,
},
shadow_timing: {
prewarm_latency_ms: 612.4,
latency_ms_p95: 21.8,
throughput_fps_from_mean_inference: 49.2,
},
resource: {
peak_reserved_vram_bytes: 2_000_000_000,
gpu_name: "NVIDIA GeForce RTX 4090",
},
};
}
function visualCase(sourceKind, index) {
const caseId = `case-${index}`;
const keys = sourceKind === "goose"
? ["source", "truth", "ddrnet", "ppliteseg"]
: ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"];
return {
case_id: caseId,
source_kind: sourceKind,
width: sourceKind === "goose" ? 512 : 800,
height: sourceKind === "goose" ? 512 : 600,
center_crop_xyxy: sourceKind === "goose" ? [0, 0, 512, 512] : [100, 0, 700, 600],
outside_crop_state: sourceKind === "goose" ? "not-applicable" : "undefined",
assets: Object.fromEntries(keys.map((key) => [key, {
path: `visual/${sourceKind}/${caseId}/${key}.png`,
sha256: "d".repeat(64),
}])),
};
}
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: Array.from({ length: 12 }, (_, index) => visualCase("ravnoves", index)),
},
access: "read-only",
}), { status: 200, headers: { "Content-Type": "application/json" } });
},
});
assert.equal(
requestedUrl,
`/api/v1/laboratory/vegetation-shadow/${resultId}`,
);
assert.equal(result.selectedCandidate, "ddrnet");
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
assert.equal(result.routeCases.length, 12);
assert.equal(result.validationCases.length, 12);
assert.match(result.routeCases[0].assets.offroad, /\/assets\/visual\/ravnoves\//);
assert.deepEqual(result.authority, {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
actuationAccepted: false,
cameraSemanticsCanClearRigidGeometry: false,
});
});
@@ -0,0 +1,10 @@
{
"schema_version": "missioncore.laboratory-evidence-definition/v1",
"work_id": "lab-v1-vegetation-shadow",
"evidence": {
"runtime_relative_root": "lab-v1-vegetation/results",
"result_id_prefix": "lab-v1-vegetation-shadow",
"document_name": "result.json",
"schema_version": "missioncore.lab-v1-vegetation-shadow/v1"
}
}
+19
View File
@@ -190,6 +190,25 @@
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.m49-tgs-full-shadow-lab/v1"
}
},
{
"work_id": "lab-v1-vegetation-shadow",
"lifecycle": "experimental",
"isolation": "bounded-adapter",
"adapter_id": "experimental.lab-v1-vegetation-shadow/v1",
"input_roles": [
"ddrnet_goose_root",
"ppliteseg_goose_root",
"ddrnet_ravnoves_root",
"ppliteseg_ravnoves_root"
],
"contracts": {
"source": "missioncore.goose-ravnoves-vegetation-source-set/v1",
"provider": "missioncore.goose-fine64-ready-weight-provider/v1",
"graph": "missioncore.vegetation-mission-policy-shadow-graph/v1",
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.lab-v1-vegetation-shadow/v1"
}
}
],
"legacy_work_ids": [
+8 -1
View File
@@ -1,6 +1,6 @@
{
"schema_version": "missioncore.laboratory-value-review-registry/v1",
"reviewed_at_utc": "2026-08-26T08:34:34Z",
"reviewed_at_utc": "2026-08-27T20:20:14Z",
"entries": [
{
"catalog_id": "e28-local-surface",
@@ -281,6 +281,13 @@
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "lab-v1-vegetation-shadow",
"evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
}
]
}
@@ -0,0 +1,97 @@
{
"schema_version": "missioncore.lab-v1-goose-vegetation-benchmark/v1",
"lab_id": "LAB-V1",
"worker_id": "worker-006",
"runtime": {
"super_gradients_version": "3.2.0",
"super_gradients_revision": "54d062ecb1081944a672ce447cf3e96a36708ff9",
"python_version": "3.9",
"numpy_version": "1.23.0",
"cmake_version": "3.31.6",
"onnxsim_version": "0.4.36",
"opencv_python_version": "4.8.1.78",
"pytorch_version": "1.13.1",
"torchvision_version": "0.14.1",
"pytorch_cuda_version": "11.7",
"container_base": "nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04@sha256:ad6d59a3bbf3e82c1c849c9ac09cfc2a3e0bbb8655042fd899be6681b3fe2a85",
"miniconda_installer": "Miniconda3-py39_24.11.1-0-Linux-x86_64.sh",
"miniconda_installer_sha256": "3ea8373098d72140e08aac9217822b047ec094eb457e7f73945af7c6f68bf6f5"
},
"dataset": {
"dataset_id": "goose-2d-validation-visible-rgb",
"relative_root": "goose-2d/validation",
"mapping_relative_path": "goose_label_mapping.csv",
"mapping_sha256": "88ae319ba5a3877dd3ae0773f693a6a5fdc283934140de9dfaff029108aefd7f",
"image_glob": "images/val/**/*_windshield_vis.png",
"expected_pair_count": 962,
"input_size": [512, 512],
"preprocessing": [
"center-square-crop",
"nearest-neighbor-resize",
"rgb-to-tensor-0-1"
]
},
"candidates": {
"ddrnet": {
"candidate_id": "goose-ddrnet-class-512",
"model_names": ["ddrnet_39"],
"checkpoint_relative_path": "models/goose/ddrnet_class_512.pth",
"checkpoint_size_bytes": 259419077,
"checkpoint_sha256": "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6",
"published_validation_miou_percent": 46.53
},
"ppliteseg": {
"candidate_id": "goose-ppliteseg-class-512",
"model_names": [
"pp_lite_t_seg",
"pp_lite_t_seg50",
"pp_lite_t_seg75",
"pp_lite_b_seg",
"pp_lite_b_seg50",
"pp_lite_b_seg75"
],
"checkpoint_relative_path": "models/goose/ppliteseg_class_512.pth",
"checkpoint_size_bytes": 98208249,
"checkpoint_sha256": "6dd412c0c99115e359896c4cab43a8e6bce9e09b843e7fa885fe597b0a6121cd",
"published_validation_miou_percent": 45.09
}
},
"ravnoves": {
"source_id": "RAVNOVES00/right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
"source_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
"expected_width": 800,
"expected_height": 600,
"expected_frame_count": 4489,
"frame_indices": [0, 253, 512, 768, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4488],
"crop_contract": "center-600-square-to-512; outside-crop-is-undefined"
},
"vegetation_class_names": [
"leaves",
"forest",
"bush",
"moss",
"tree_crown",
"tree_trunk",
"crops",
"low_grass",
"high_grass",
"scenery_vegetation",
"hedge",
"tree_root"
],
"policy_action_colors": {
"ALLOW": "#22c55e",
"HIGH_COST": "#f59e0b",
"NO_GO": "#ef4444"
},
"invariants": {
"one_heavy_candidate_at_a_time": true,
"raw_fisheye_is_immutable": true,
"outside_center_crop_is_free": false,
"missing_or_unknown_is_free": false,
"camera_semantics_can_clear_rigid_geometry": false,
"navigation_authority": false,
"actuation_authority": false,
"canonical_triton_mutation_allowed": false
}
}
@@ -0,0 +1,230 @@
[CmdletBinding()]
param(
[ValidateSet("Build", "Probe", "Validate", "Ravnoves", "Status")]
[string]$Mode = "Status",
[ValidateSet("Ddrnet", "Ppliteseg")]
[string]$Candidate = "Ddrnet",
[string]$AssetRoot = "D:\NDC_MISSIONCORE\datasets\vegetation-v1\observed-2026-08-27",
[string]$ToolRoot = "D:\NDC_MISSIONCORE\datasets\tooling\lab-v1-vegetation-goose",
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\experiments\lab-v1-vegetation",
[string]$RavnovesVideo = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$datasetPrefix = "D:\NDC_MISSIONCORE\datasets\"
$runtimePrefix = "D:\NDC_MISSIONCORE\runtime\experiments\"
if (-not $AssetRoot.StartsWith($datasetPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "AssetRoot must stay under $datasetPrefix"
}
if (-not $ToolRoot.StartsWith($datasetPrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "ToolRoot must stay under $datasetPrefix"
}
if (-not $OutputRoot.StartsWith($runtimePrefix, [StringComparison]::OrdinalIgnoreCase)) {
throw "OutputRoot must stay under $runtimePrefix"
}
$image = "ndc/mission-core-lab-v1-goose:sg3.2.0-cu117-v1"
$canonicalContainer = "ndc-mission-core-triton"
$candidateKey = $Candidate.ToLowerInvariant()
$contextRoot = Join-Path $ToolRoot "context"
$configRoot = Join-Path $ToolRoot "config"
$benchmarkConfig = Join-Path $configRoot "lab-v1-goose-vegetation-benchmark-v1.json"
$policyConfig = Join-Path $configRoot "lab-v1-vegetation-mission-policy-v1.json"
$providerMapConfig = Join-Path $configRoot "lab-v1-vegetation-provider-label-map-v1.json"
$datasetRoot = Join-Path $AssetRoot "goose-2d\validation"
$checkpointRelative = if ($candidateKey -eq "ddrnet") {
"models\goose\ddrnet_class_512.pth"
} else {
"models\goose\ppliteseg_class_512.pth"
}
$checkpoint = Join-Path $AssetRoot $checkpointRelative
$expectedCheckpointSha256 = if ($candidateKey -eq "ddrnet") {
"b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6"
} else {
"6dd412c0c99115e359896c4cab43a8e6bce9e09b843e7fa885fe597b0a6121cd"
}
$expectedCheckpointBytes = if ($candidateKey -eq "ddrnet") { 259419077 } else { 98208249 }
$ravnovesSha256 = "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8"
$frameIndices = @(0, 253, 512, 768, 1024, 1536, 2048, 2560, 3072, 3584, 4096, 4488)
$dockerConfig = "D:\NDC_MISSIONCORE\datasets\state\lab-v1-vegetation\docker-config"
function Get-CanonicalTritonIdentity {
$identity = & docker inspect $canonicalContainer --format "{{.Id}}|{{.Config.Image}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{end}}"
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($identity)) {
throw "Canonical Triton is unavailable"
}
$parts = $identity.Split("|")
if ($parts.Count -ne 4 -or $parts[2] -ne "running" -or $parts[3] -ne "healthy") {
throw "Canonical Triton is not running and healthy: $identity"
}
return $identity
}
function Assert-FileIdentity {
param([string]$Path, [long]$ExpectedBytes, [string]$ExpectedSha256)
$file = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
if ($null -eq $file -or $file.Length -ne $ExpectedBytes) {
throw "File identity changed: $Path"
}
$actualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualSha256 -ne $ExpectedSha256) {
throw "File digest changed: $Path"
}
}
function Assert-RunnerInputs {
foreach ($path in @($benchmarkConfig, $policyConfig, $providerMapConfig)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Runner config is unavailable: $path"
}
}
if (-not (Test-Path -LiteralPath (Join-Path $contextRoot "Dockerfile") -PathType Leaf)) {
throw "Runner Dockerfile is unavailable"
}
if (-not (Test-Path -LiteralPath (Join-Path $contextRoot "run_goose_vegetation_benchmark.py") -PathType Leaf)) {
throw "Runner source is unavailable"
}
Assert-FileIdentity -Path $checkpoint -ExpectedBytes $expectedCheckpointBytes -ExpectedSha256 $expectedCheckpointSha256
}
function New-RunRoot {
param([string]$Kind)
$stamp = [DateTime]::UtcNow.ToString("yyyyMMddTHHmmssfffZ")
$path = Join-Path $OutputRoot ("{0}-{1}-{2}" -f $Kind, $candidateKey, $stamp)
New-Item -ItemType Directory -Path $path | Out-Null
return $path
}
function Invoke-IsolatedRun {
param(
[ValidateSet("goose", "ravnoves")][string]$RunMode,
[string]$RunRoot,
[int]$Limit,
[string]$FramesRoot = ""
)
$containerName = "ndc-lab-v1-goose-$candidateKey-$([Guid]::NewGuid().ToString('N').Substring(0, 10))"
$arguments = @(
"run", "--rm", "--name", $containerName,
"--gpus", "all",
"--network", "none",
"--read-only",
"--cap-drop", "ALL",
"--security-opt", "no-new-privileges",
"--memory", "10g",
"--cpus", "8",
"--pids-limit", "512",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=2g",
"--env", "HOME=/tmp",
"--mount", "type=bind,src=$datasetRoot,dst=/data/goose,readonly",
"--mount", "type=bind,src=$checkpoint,dst=/models/candidate.pth,readonly",
"--mount", "type=bind,src=$configRoot,dst=/config,readonly",
"--mount", "type=bind,src=$RunRoot,dst=/output",
$image,
"--mode", $RunMode,
"--candidate", $candidateKey,
"--config", "/config/lab-v1-goose-vegetation-benchmark-v1.json",
"--policy", "/config/lab-v1-vegetation-mission-policy-v1.json",
"--provider-map", "/config/lab-v1-vegetation-provider-label-map-v1.json",
"--checkpoint", "/models/candidate.pth",
"--dataset-root", "/data/goose",
"--output", "/output/result",
"--limit", $Limit.ToString(),
"--visual-count", "12"
)
if ($RunMode -eq "ravnoves") {
$arguments = @($arguments[0..($arguments.Count - 1)])
$arguments += @("--frames-root", "/input")
$mountIndex = [Array]::IndexOf($arguments, $image)
$head = @($arguments[0..($mountIndex - 1)])
$tail = @($arguments[$mountIndex..($arguments.Count - 1)])
$arguments = $head + @("--mount", "type=bind,src=$FramesRoot,dst=/input,readonly") + $tail
}
& docker @arguments
if ($LASTEXITCODE -ne 0) {
throw "LAB V1 container failed with exit code $LASTEXITCODE"
}
}
function Export-RavnovesFrames {
param([string]$Destination)
Assert-FileIdentity -Path $RavnovesVideo -ExpectedBytes (Get-Item -LiteralPath $RavnovesVideo).Length -ExpectedSha256 $ravnovesSha256
New-Item -ItemType Directory -Path $Destination | Out-Null
$expression = ($frameIndices | ForEach-Object { "eq(n\,$_ )" }) -join "+"
$temporaryPattern = Join-Path $Destination "selected-%03d.png"
& ffmpeg -hide_banner -loglevel error -i $RavnovesVideo -vf "select='$expression'" -fps_mode vfr $temporaryPattern
if ($LASTEXITCODE -ne 0) {
throw "RAVNOVES exact frame extraction failed"
}
$selected = @(Get-ChildItem -LiteralPath $Destination -Filter "selected-*.png" | Sort-Object Name)
if ($selected.Count -ne $frameIndices.Count) {
throw "RAVNOVES frame island changed: expected $($frameIndices.Count), got $($selected.Count)"
}
for ($index = 0; $index -lt $selected.Count; $index++) {
$target = Join-Path $Destination ("frame-{0:D6}.png" -f $frameIndices[$index])
Move-Item -LiteralPath $selected[$index].FullName -Destination $target
}
}
if ($Mode -eq "Status") {
$imageIdentity = & docker image inspect $image --format "{{.Id}}" 2>$null
[ordered]@{
schema_version = "missioncore.lab-v1-goose-runner-status/v1"
observed_at_utc = [DateTime]::UtcNow.ToString("o")
worker_id = "worker-006"
image = $image
image_id = if ($LASTEXITCODE -eq 0) { $imageIdentity } else { $null }
canonical_triton = Get-CanonicalTritonIdentity
asset_root = $AssetRoot
output_root = $OutputRoot
candidate = $candidateKey
} | ConvertTo-Json -Depth 6
exit 0
}
Assert-RunnerInputs
$canonicalBefore = Get-CanonicalTritonIdentity
try {
if ($Mode -eq "Build") {
if (-not (Test-Path -LiteralPath (Join-Path $dockerConfig "config.json") -PathType Leaf)) {
throw "Isolated Docker client configuration is unavailable"
}
$previousDockerConfig = $env:DOCKER_CONFIG
try {
$env:DOCKER_CONFIG = $dockerConfig
& docker build --pull=false --label "com.nodedc.component=mission-core-lab-v1-goose" --label "com.nodedc.authority=shadow-only" --tag $image $contextRoot
if ($LASTEXITCODE -ne 0) {
throw "LAB V1 image build failed with exit code $LASTEXITCODE"
}
}
finally {
$env:DOCKER_CONFIG = $previousDockerConfig
}
}
elseif ($Mode -eq "Probe") {
$runRoot = New-RunRoot -Kind "probe"
Invoke-IsolatedRun -RunMode "goose" -RunRoot $runRoot -Limit 8
}
elseif ($Mode -eq "Validate") {
$runRoot = New-RunRoot -Kind "validation"
Invoke-IsolatedRun -RunMode "goose" -RunRoot $runRoot -Limit 0
}
elseif ($Mode -eq "Ravnoves") {
$runRoot = New-RunRoot -Kind "ravnoves"
$framesRoot = Join-Path $runRoot "input-frames"
Export-RavnovesFrames -Destination $framesRoot
Invoke-IsolatedRun -RunMode "ravnoves" -RunRoot $runRoot -Limit 0 -FramesRoot $framesRoot
}
}
finally {
$canonicalAfter = Get-CanonicalTritonIdentity
if ($canonicalAfter -ne $canonicalBefore) {
throw "Canonical Triton identity changed during LAB V1 work"
}
}
@@ -0,0 +1,49 @@
FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04@sha256:ad6d59a3bbf3e82c1c849c9ac09cfc2a3e0bbb8655042fd899be6681b3fe2a85
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/opt/conda/bin:$PATH
ARG MINICONDA_INSTALLER=Miniconda3-py39_24.11.1-0-Linux-x86_64.sh
ARG MINICONDA_SHA256=3ea8373098d72140e08aac9217822b047ec094eb457e7f73945af7c6f68bf6f5
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
libglib2.0-0 \
libgl1 \
&& curl --fail --location --retry 5 \
--output /tmp/miniconda.sh \
"https://repo.anaconda.com/miniconda/${MINICONDA_INSTALLER}" \
&& echo "${MINICONDA_SHA256} /tmp/miniconda.sh" | sha256sum --check --strict \
&& bash /tmp/miniconda.sh -b -p /opt/conda \
&& rm -f /tmp/miniconda.sh \
&& rm -rf /var/lib/apt/lists/*
RUN conda create --yes --name goose python=3.9 pip \
&& conda install --yes --name goose --channel pytorch --channel nvidia \
pytorch=1.13.1 torchvision=0.14.1 pytorch-cuda=11.7
RUN conda run --name goose python -m pip install --no-cache-dir \
cmake==3.31.6 \
numpy==1.23.0 \
onnxsim==0.4.36 \
opencv-python==4.8.1.78 \
protobuf==3.20.3 \
pyparsing==2.4.5
RUN conda run --name goose python -m pip install --no-cache-dir \
super-gradients==3.2.0 \
torchmetrics==0.8.0
RUN conda clean --all --yes
WORKDIR /opt/mission-core/lab-v1
COPY run_goose_vegetation_benchmark.py /opt/mission-core/lab-v1/runner.py
ENTRYPOINT ["conda", "run", "--no-capture-output", "--name", "goose", "python", "/opt/mission-core/lab-v1/runner.py"]
@@ -0,0 +1,556 @@
"""Run isolated GOOSE vegetation qualification and RAVNOVES shadow inference."""
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import math
import os
import platform
import statistics
import time
from pathlib import Path
from typing import Any
import numpy as np
import torch
from PIL import Image
from super_gradients.training import models
SCHEMA = "missioncore.lab-v1-goose-vegetation-run/v1"
VISUAL_SCHEMA = "missioncore.lab-v1-goose-vegetation-visual-case/v1"
CLASS_COUNT = 64
MAX_CONFIG_BYTES = 1024 * 1024
MODEL_NAMES = {
"ddrnet": ("ddrnet_39",),
"ppliteseg": (
"pp_lite_t_seg",
"pp_lite_t_seg50",
"pp_lite_t_seg75",
"pp_lite_b_seg",
"pp_lite_b_seg50",
"pp_lite_b_seg75",
),
}
RESAMPLE_NEAREST = getattr(Image, "Resampling", Image).NEAREST
class RunnerError(RuntimeError):
"""The bounded runner input or output contract is invalid."""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--mode", choices=("goose", "ravnoves"), required=True)
parser.add_argument("--candidate", choices=tuple(MODEL_NAMES), required=True)
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)
parser.add_argument("--frames-root", type=Path)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--visual-count", type=int, default=12)
return parser.parse_args()
def read_json(path: Path, label: str) -> dict[str, Any]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_CONFIG_BYTES:
raise RunnerError(f"{label} is unavailable")
value = json.loads(path.read_text(encoding="utf-8-sig"))
if not isinstance(value, dict):
raise RunnerError(f"{label} must be an object")
return value
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def stable_digest(value: object) -> str:
encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def validate_contracts(
config: dict[str, Any], policy: dict[str, Any], provider_map: dict[str, Any], candidate: str
) -> dict[str, Any]:
if config.get("schema_version") != "missioncore.lab-v1-goose-vegetation-benchmark/v1":
raise RunnerError("benchmark configuration identity changed")
if policy.get("schema_version") != "missioncore.vegetation-mission-policy/v1":
raise RunnerError("mission policy identity changed")
if provider_map.get("schema_version") != "missioncore.vegetation-provider-label-map/v1":
raise RunnerError("provider map identity changed")
invariants = config.get("invariants")
true_invariants = {"one_heavy_candidate_at_a_time", "raw_fisheye_is_immutable"}
if not isinstance(invariants, dict) or any(
value is not False for key, value in invariants.items() if key not in true_invariants
):
raise RunnerError("benchmark fail-closed invariants changed")
if (
invariants.get("one_heavy_candidate_at_a_time") is not True
or invariants.get("raw_fisheye_is_immutable") is not True
):
raise RunnerError("benchmark isolation invariants changed")
candidates = config.get("candidates")
if not isinstance(candidates, dict) or not isinstance(candidates.get(candidate), dict):
raise RunnerError("candidate is not configured")
expected_models = candidates[candidate].get("model_names")
if expected_models != list(MODEL_NAMES[candidate]):
raise RunnerError("candidate architecture probe order changed")
return candidates[candidate]
def load_mapping(path: Path, expected_sha256: str) -> tuple[dict[int, str], np.ndarray]:
if sha256(path) != expected_sha256:
raise RunnerError("GOOSE label mapping digest changed")
names: dict[int, str] = {}
palette = np.zeros((CLASS_COUNT, 4), dtype=np.uint8)
with path.open(newline="", encoding="utf-8-sig") as stream:
for row in csv.DictReader(stream):
label_id = int(row["label_key"])
if label_id < 0 or label_id >= CLASS_COUNT:
raise RunnerError("GOOSE label id is outside the 64-class contract")
color = row["hex"].lstrip("#")
names[label_id] = row["class_name"]
palette[label_id] = (*bytes.fromhex(color), 190)
if set(names) != set(range(CLASS_COUNT)):
raise RunnerError("GOOSE mapping does not cover exactly 64 classes")
palette[0, 3] = 0
return names, palette
def center_crop(image: Image.Image) -> tuple[Image.Image, tuple[int, int, int, int]]:
side = min(image.width, image.height)
left = (image.width - side) // 2
top = (image.height - side) // 2
box = (left, top, left + side, top + side)
return image.crop(box), box
def preprocess(image: Image.Image) -> tuple[torch.Tensor, tuple[int, int, int, int]]:
cropped, crop_box = center_crop(image.convert("RGB"))
resized = cropped.resize((512, 512), resample=RESAMPLE_NEAREST)
array = np.asarray(resized, dtype=np.float32) / 255.0
tensor = torch.from_numpy(np.transpose(array, (2, 0, 1))).unsqueeze(0)
return tensor, crop_box
def preprocess_label(image: Image.Image) -> np.ndarray:
cropped, _ = center_crop(image.convert("L"))
return np.asarray(cropped.resize((512, 512), resample=RESAMPLE_NEAREST), dtype=np.uint8)
def find_goose_pairs(root: Path) -> list[tuple[Path, Path]]:
pairs: list[tuple[Path, Path]] = []
image_root = root / "images" / "val"
label_root = root / "labels" / "val"
for image_path in sorted(image_root.rglob("*_windshield_vis.png")):
stem = image_path.name.removesuffix("_windshield_vis.png")
relative_parent = image_path.parent.relative_to(image_root)
label_path = label_root / relative_parent / f"{stem}_labelids.png"
if label_path.is_file() and not label_path.is_symlink():
pairs.append((image_path, label_path))
return pairs
def visual_indices(count: int, visual_count: int) -> set[int]:
if count <= 0 or visual_count <= 0:
return set()
selected_count = min(count, visual_count)
if selected_count == 1:
return {0}
return {
round(index * (count - 1) / (selected_count - 1))
for index in range(selected_count)
}
def load_model(candidate: str, checkpoint: Path) -> tuple[torch.nn.Module, str, list[str]]:
failures: list[str] = []
for model_name in MODEL_NAMES[candidate]:
try:
model = models.get(
model_name=model_name,
num_classes=CLASS_COUNT,
checkpoint_path=str(checkpoint),
)
model.eval()
model.cuda()
return model, model_name, failures
except Exception as error: # noqa: BLE001 - each upstream architecture is a probe
failures.append(f"{model_name}: {type(error).__name__}: {str(error)[:240]}")
if torch.cuda.is_available():
torch.cuda.empty_cache()
raise RunnerError("checkpoint did not load: " + " | ".join(failures))
def logits_from_output(value: object) -> torch.Tensor:
if isinstance(value, torch.Tensor) and value.ndim == 4 and value.shape[1] == CLASS_COUNT:
return value
if isinstance(value, (list, tuple)):
for item in value:
try:
return logits_from_output(item)
except RunnerError:
continue
raise RunnerError("model output does not contain a 64-class raster")
def infer(model: torch.nn.Module, tensor: torch.Tensor) -> tuple[np.ndarray, float]:
tensor = tensor.cuda(non_blocking=True)
torch.cuda.synchronize()
started = time.perf_counter_ns()
with torch.inference_mode():
logits = logits_from_output(model(tensor))
prediction = torch.argmax(torch.sigmoid(logits), dim=1)
torch.cuda.synchronize()
elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000.0
return prediction[0].to(device="cpu", dtype=torch.uint8).numpy(), elapsed_ms
def update_confusion(confusion: np.ndarray, truth: np.ndarray, prediction: np.ndarray) -> None:
valid = (truth >= 0) & (truth < CLASS_COUNT)
indices = CLASS_COUNT * truth[valid].astype(np.int64) + prediction[valid].astype(np.int64)
confusion += np.bincount(indices, minlength=CLASS_COUNT**2).reshape(CLASS_COUNT, CLASS_COUNT)
def class_metrics(confusion: np.ndarray, names: dict[int, str]) -> list[dict[str, Any]]:
truth = confusion.sum(axis=1)
predicted = confusion.sum(axis=0)
intersection = np.diag(confusion)
union = truth + predicted - intersection
rows: list[dict[str, Any]] = []
for label_id in range(CLASS_COUNT):
rows.append(
{
"label_id": label_id,
"class_name": names[label_id],
"support_pixels": int(truth[label_id]),
"predicted_pixels": int(predicted[label_id]),
"intersection_pixels": int(intersection[label_id]),
"union_pixels": int(union[label_id]),
"iou": round(float(intersection[label_id] / union[label_id]), 8)
if union[label_id]
else None,
}
)
return rows
def hex_rgb(value: str) -> tuple[int, int, int]:
raw = bytes.fromhex(value.removeprefix("#"))
if len(raw) != 3:
raise RunnerError("policy action color must be RGB")
return raw[0], raw[1], raw[2]
def policy_palette(
names: dict[int, str], policy: dict[str, Any], provider_map: dict[str, Any], preset: str,
action_colors: dict[str, str]
) -> np.ndarray:
palette = np.zeros((CLASS_COUNT, 4), dtype=np.uint8)
labels = provider_map["providers"]["goose-fine-64"]["labels"]
rules = policy["presets"][preset]
for label_id, class_name in names.items():
material = labels.get(class_name)
if material is None:
continue
action = rules[material]
palette[label_id] = (*hex_rgb(action_colors[action]), 190)
return palette
def save_image(path: Path, value: Image.Image | np.ndarray, mode: str | None = None) -> str:
path.parent.mkdir(parents=True, exist_ok=True)
image = value if isinstance(value, Image.Image) else Image.fromarray(value, mode=mode)
image.save(path, format="PNG", optimize=True)
return sha256(path)
def expand_mask(
mask: np.ndarray,
original_size: tuple[int, int],
crop_box: tuple[int, int, int, int],
) -> np.ndarray:
left, top, right, bottom = crop_box
side = right - left
resized = Image.fromarray(mask, mode="L").resize((side, side), resample=RESAMPLE_NEAREST)
canvas = np.zeros((original_size[1], original_size[0]), dtype=np.uint8)
canvas[top:bottom, left:right] = np.asarray(resized, dtype=np.uint8)
return canvas
def write_visual_case(
output: Path,
case_id: str,
source: Image.Image,
prediction: np.ndarray,
semantic_palette: np.ndarray,
policy_palettes: dict[str, np.ndarray],
crop_box: tuple[int, int, int, int],
truth: np.ndarray | None = None,
preserve_source_size: bool = False,
) -> dict[str, Any]:
case_root = output / "cases" / case_id
if preserve_source_size:
source_image = source.convert("RGB")
prediction_image = expand_mask(prediction, source_image.size, crop_box)
truth_image = expand_mask(truth, source_image.size, crop_box) if truth is not None else None
else:
cropped, _ = center_crop(source.convert("RGB"))
source_image = cropped.resize((512, 512), resample=RESAMPLE_NEAREST)
prediction_image = prediction
truth_image = truth
files: dict[str, dict[str, str]] = {}
source_path = case_root / "source.png"
files["source"] = {
"relative_path": source_path.relative_to(output).as_posix(),
"sha256": save_image(source_path, source_image),
}
prediction_path = case_root / "prediction-labelids.png"
files["prediction_labelids"] = {
"relative_path": prediction_path.relative_to(output).as_posix(),
"sha256": save_image(prediction_path, prediction_image, "L"),
}
semantic_path = case_root / "prediction-semantic.png"
files["prediction_semantic"] = {
"relative_path": semantic_path.relative_to(output).as_posix(),
"sha256": save_image(semantic_path, semantic_palette[prediction_image], "RGBA"),
}
for preset, palette in policy_palettes.items():
policy_path = case_root / f"policy-{preset}.png"
files[f"policy_{preset}"] = {
"relative_path": policy_path.relative_to(output).as_posix(),
"sha256": save_image(policy_path, palette[prediction_image], "RGBA"),
}
if truth_image is not None:
truth_path = case_root / "truth-labelids.png"
files["truth_labelids"] = {
"relative_path": truth_path.relative_to(output).as_posix(),
"sha256": save_image(truth_path, truth_image, "L"),
}
truth_semantic_path = case_root / "truth-semantic.png"
files["truth_semantic"] = {
"relative_path": truth_semantic_path.relative_to(output).as_posix(),
"sha256": save_image(truth_semantic_path, semantic_palette[truth_image], "RGBA"),
}
return {
"schema_version": VISUAL_SCHEMA,
"case_id": case_id,
"source_width": source_image.width,
"source_height": source_image.height,
"center_crop_xyxy": list(crop_box),
"outside_crop_state": "undefined" if preserve_source_size else "not-applicable",
"files": files,
}
def percentile(values: list[float], fraction: float) -> float:
if not values:
return 0.0
ordered = sorted(values)
index = (len(ordered) - 1) * fraction
lower = math.floor(index)
upper = math.ceil(index)
if lower == upper:
return ordered[lower]
return ordered[lower] * (upper - index) + ordered[upper] * (index - lower)
def run() -> None:
args = parse_args()
if not torch.cuda.is_available():
raise RunnerError("CUDA is required for Worker 006 qualification")
if args.limit < 0 or args.visual_count < 0:
raise RunnerError("limit and visual-count must be non-negative")
config = read_json(args.config, "benchmark config")
policy = read_json(args.policy, "mission policy")
provider_map = read_json(args.provider_map, "provider map")
candidate_config = validate_contracts(config, policy, provider_map, args.candidate)
if args.checkpoint.is_symlink() or not args.checkpoint.is_file():
raise RunnerError("checkpoint is unavailable")
if args.checkpoint.stat().st_size != candidate_config["checkpoint_size_bytes"]:
raise RunnerError("checkpoint size changed")
checkpoint_sha256 = sha256(args.checkpoint)
if checkpoint_sha256 != candidate_config["checkpoint_sha256"]:
raise RunnerError("checkpoint digest changed")
dataset_config = config["dataset"]
mapping_root = args.dataset_root
if mapping_root is None:
raise RunnerError("dataset-root is required for the immutable mapping")
mapping_path = mapping_root / dataset_config["mapping_relative_path"]
names, semantic_palette = load_mapping(mapping_path, dataset_config["mapping_sha256"])
policy_palettes = {
preset: policy_palette(
names,
policy,
provider_map,
preset,
config["policy_action_colors"],
)
for preset in ("urban", "rural", "offroad")
}
if args.mode == "goose":
pairs = find_goose_pairs(mapping_root)
if len(pairs) != dataset_config["expected_pair_count"]:
raise RunnerError(f"GOOSE pair count changed: {len(pairs)}")
items: list[tuple[str, Path, Path | None]] = [
(image.stem.removesuffix("_windshield_vis"), image, label)
for image, label in pairs
]
else:
if args.frames_root is None or not args.frames_root.is_dir():
raise RunnerError("frames-root is required for RAVNOVES mode")
frames = sorted(args.frames_root.glob("frame-*.png"))
expected = {f"frame-{index:06d}" for index in config["ravnoves"]["frame_indices"]}
if {frame.stem for frame in frames} != expected:
raise RunnerError("RAVNOVES frame island identity changed")
items = [(frame.stem, frame, None) for frame in frames]
if args.limit:
items = items[: args.limit]
if not items:
raise RunnerError("no inputs were selected")
args.output.mkdir(parents=True, exist_ok=False)
torch.cuda.empty_cache()
model, model_name, architecture_failures = load_model(args.candidate, args.checkpoint)
warmup_source = Image.open(items[0][1]).convert("RGB")
warmup_tensor, _ = preprocess(warmup_source)
warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)]
torch.cuda.reset_peak_memory_stats()
selected_visuals = visual_indices(len(items), args.visual_count)
confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64)
latencies_ms: list[float] = []
visuals: list[dict[str, Any]] = []
for index, (case_id, source_path, label_path) in enumerate(items):
source = Image.open(source_path).convert("RGB")
tensor, crop_box = preprocess(source)
prediction, latency_ms = infer(model, tensor)
latencies_ms.append(latency_ms)
truth = preprocess_label(Image.open(label_path)) if label_path is not None else None
if truth is not None:
update_confusion(confusion, truth, prediction)
if index in selected_visuals:
visuals.append(
write_visual_case(
args.output,
case_id,
source,
prediction,
semantic_palette,
policy_palettes,
crop_box,
truth=truth,
preserve_source_size=args.mode == "ravnoves",
)
)
rows = class_metrics(confusion, names) if args.mode == "goose" else []
valid_ious = [row["iou"] for row in rows if row["iou"] is not None]
vegetation_names = set(config["vegetation_class_names"])
vegetation_rows = [row for row in rows if row["class_name"] in vegetation_names]
vegetation_ious = [row["iou"] for row in vegetation_rows if row["iou"] is not None]
timing = {
"prewarm_inference_count": len(warmup_latencies_ms),
"prewarm_latency_ms": round(warmup_latencies_ms[0], 4),
"prewarm_latency_ms_last": round(warmup_latencies_ms[-1], 4),
"sample_count": len(latencies_ms),
"latency_ms_p50": round(percentile(latencies_ms, 0.50), 4),
"latency_ms_p95": round(percentile(latencies_ms, 0.95), 4),
"latency_ms_mean": round(statistics.fmean(latencies_ms), 4),
"throughput_fps_from_mean_inference": round(1000.0 / statistics.fmean(latencies_ms), 4),
}
result: dict[str, Any] = {
"schema_version": SCHEMA,
"lab_id": config["lab_id"],
"worker_id": config["worker_id"],
"mode": args.mode,
"candidate": {
"candidate_id": candidate_config["candidate_id"],
"candidate_key": args.candidate,
"loaded_model_name": model_name,
"architecture_probe_failures": architecture_failures,
"checkpoint_size_bytes": args.checkpoint.stat().st_size,
"checkpoint_sha256": checkpoint_sha256,
},
"source": {
"source_id": dataset_config["dataset_id"]
if args.mode == "goose"
else config["ravnoves"]["source_id"],
"input_count": len(items),
"ground_truth_available": args.mode == "goose",
"mapping_sha256": dataset_config["mapping_sha256"],
},
"preprocessing": dataset_config["preprocessing"],
"metrics": {
"mean_iou": round(statistics.fmean(valid_ious), 8) if valid_ious else None,
"mean_iou_percent": round(statistics.fmean(valid_ious) * 100.0, 4)
if valid_ious
else None,
"published_mean_iou_percent": candidate_config["published_validation_miou_percent"],
"vegetation_mean_iou": round(statistics.fmean(vegetation_ious), 8)
if vegetation_ious
else None,
"vegetation_classes": vegetation_rows,
"all_classes": rows,
},
"timing": timing,
"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(),
"super_gradients_version": "3.2.0",
},
"visual_cases": visuals,
"authority": {
"navigation_accepted": False,
"safety_accepted": False,
"actuation_accepted": False,
"camera_semantics_can_clear_rigid_geometry": False,
},
}
identity_value = {
"schema_version": result["schema_version"],
"candidate": result["candidate"],
"source": result["source"],
"preprocessing": result["preprocessing"],
"metrics": result["metrics"],
"timing": result["timing"],
"resource": result["resource"],
"visual_cases": result["visual_cases"],
"authority": result["authority"],
"config_sha256": sha256(args.config),
"policy_sha256": sha256(args.policy),
"provider_map_sha256": sha256(args.provider_map),
}
result["result_id"] = f"lab-v1-{args.mode}-{args.candidate}-{stable_digest(identity_value)}"
result["provenance"] = {
"config_sha256": identity_value["config_sha256"],
"policy_sha256": identity_value["policy_sha256"],
"provider_map_sha256": identity_value["provider_map_sha256"],
"hostname": platform.node(),
"pid": os.getpid(),
}
result_path = args.output / "result.json"
result_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(json.dumps({"result_id": result["result_id"], "result_path": str(result_path)}))
if __name__ == "__main__":
run()
+23
View File
@@ -326,6 +326,7 @@ def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
"experimental.e47-semantic-slam-shadow/v1": _run_e47,
"experimental.m48s-fixed-class-detector/v1": _run_m48s_fixed_class_detector,
"experimental.m48t-risk-quality-temporal/v1": _run_m48t_risk_quality_temporal,
"experimental.lab-v1-vegetation-shadow/v1": _run_lab_v1_vegetation_shadow,
}
@@ -382,6 +383,28 @@ def _run_m48t_risk_quality_temporal(
)
def _run_lab_v1_vegetation_shadow(
request: LaboratoryRunRequest,
) -> LaboratoryAdapterResult:
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
result_root = seal_vegetation_shadow_lab(
ddrnet_goose_root=request.inputs["ddrnet_goose_root"],
ppliteseg_goose_root=request.inputs["ppliteseg_goose_root"],
ddrnet_ravnoves_root=request.inputs["ddrnet_ravnoves_root"],
ppliteseg_ravnoves_root=request.inputs["ppliteseg_ravnoves_root"],
output_root=request.output_root,
)
manifest = _object(
json.loads((result_root / "result.json").read_text(encoding="utf-8")),
"vegetation shadow result",
)
result_id = manifest.get("result_id")
if not isinstance(result_id, str):
raise LaboratoryExecutionError("vegetation shadow result_id is invalid")
return LaboratoryAdapterResult(result_root=result_root, result_id=result_id)
def _run_m48_small_static_passage_regression(
request: LaboratoryRunRequest,
) -> LaboratoryAdapterResult:
@@ -0,0 +1,381 @@
"""Seal GOOSE qualification and RAVNOVES vegetation shadow evidence for LAB."""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import tempfile
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
from typing import Any, Final
LAB_SCHEMA: Final = "missioncore.lab-v1-vegetation-shadow/v1"
WORKER_SCHEMA: Final = "missioncore.lab-v1-goose-vegetation-run/v1"
RESULT_PREFIX: Final = "lab-v1-vegetation-shadow-"
_CANDIDATES: Final = ("ddrnet", "ppliteseg")
_MODES: Final = ("goose", "ravnoves")
_IMAGE_KEYS: Final = (
"source",
"prediction_semantic",
"policy_urban",
"policy_rural",
"policy_offroad",
"truth_semantic",
)
class VegetationShadowLabError(ValueError):
"""Raised when Worker evidence cannot be sealed without changing its meaning."""
def canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def sha256_path(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 _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise VegetationShadowLabError(f"{label} must be an object")
return value
def _read_worker_result(root: Path, *, candidate: str, mode: str) -> dict[str, Any]:
if root.is_symlink() or not root.is_dir():
raise VegetationShadowLabError(f"{candidate}/{mode} result root is unavailable")
path = root / "result.json"
if path.is_symlink() or not path.is_file() or path.stat().st_size > 1024 * 1024:
raise VegetationShadowLabError(f"{candidate}/{mode} result document is unavailable")
try:
result = _object(json.loads(path.read_text("utf-8")), f"{candidate}/{mode} result")
except (json.JSONDecodeError, OSError) as exc:
raise VegetationShadowLabError(f"{candidate}/{mode} result is invalid") from exc
candidate_value = _object(result.get("candidate"), f"{candidate}/{mode} candidate")
authority = _object(result.get("authority"), f"{candidate}/{mode} authority")
if (
result.get("schema_version") != WORKER_SCHEMA
or result.get("mode") != mode
or candidate_value.get("candidate_key") != candidate
or authority.get("navigation_accepted") is not False
or authority.get("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 VegetationShadowLabError(f"{candidate}/{mode} contract changed")
return result
def _case_map(result: dict[str, Any], label: str) -> dict[str, dict[str, Any]]:
values = result.get("visual_cases")
if not isinstance(values, list) or len(values) != 12:
raise VegetationShadowLabError(f"{label} must contain exactly 12 visual cases")
rows: dict[str, dict[str, Any]] = {}
for raw in values:
row = _object(raw, f"{label} visual case")
case_id = row.get("case_id")
if not isinstance(case_id, str) or not case_id or case_id in rows:
raise VegetationShadowLabError(f"{label} case identity changed")
rows[case_id] = row
return rows
def _file_from_case(
root: Path,
case: dict[str, Any],
key: str,
*,
required: bool = True,
) -> tuple[Path, str] | None:
files = _object(case.get("files"), "visual case files")
raw = files.get(key)
if raw is None and not required:
return None
descriptor = _object(raw, f"visual case {key}")
relative = descriptor.get("relative_path")
expected_sha256 = descriptor.get("sha256")
if not isinstance(relative, str) or not isinstance(expected_sha256, str):
raise VegetationShadowLabError(f"visual case {key} proof is invalid")
posix = PurePosixPath(relative)
if posix.is_absolute() or str(posix) != relative or any(
part in {"", ".", ".."} for part in posix.parts
):
raise VegetationShadowLabError(f"visual case {key} path is invalid")
path = root.joinpath(*posix.parts)
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(root.resolve()):
raise VegetationShadowLabError(f"visual case {key} file is unavailable")
if sha256_path(path) != expected_sha256:
raise VegetationShadowLabError(f"visual case {key} digest changed")
return path, expected_sha256
def _copy_artifact(
source: Path,
destination_root: Path,
relative: str,
artifacts: list[dict[str, object]],
*,
role: str,
media_type: str,
) -> dict[str, object]:
destination = destination_root.joinpath(*PurePosixPath(relative).parts)
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
shutil.copyfile(source, destination)
digest = sha256_path(destination)
descriptor = {
"role": role,
"path": relative,
"byte_length": destination.stat().st_size,
"sha256": digest,
"media_type": media_type,
}
artifacts.append(descriptor)
return descriptor
def _selected_candidate(results: dict[tuple[str, str], dict[str, Any]]) -> str:
def rank(candidate: str) -> tuple[float, float]:
validation = results[(candidate, "goose")]
metrics = _object(validation.get("metrics"), f"{candidate} metrics")
timing = _object(validation.get("timing"), f"{candidate} timing")
vegetation_iou = metrics.get("vegetation_mean_iou")
p95_ms = timing.get("latency_ms_p95")
if not isinstance(vegetation_iou, (int, float)) or not isinstance(p95_ms, (int, float)):
raise VegetationShadowLabError(f"{candidate} qualification metrics are incomplete")
return float(vegetation_iou), -float(p95_ms)
return max(_CANDIDATES, key=rank)
def _validation_metric_summary(result: dict[str, Any], candidate: str) -> dict[str, object]:
metrics = _object(result.get("metrics"), f"{candidate} metrics")
return {
"mean_iou_percent": metrics.get("mean_iou_percent"),
"published_mean_iou_percent": metrics.get("published_mean_iou_percent"),
"vegetation_mean_iou": metrics.get("vegetation_mean_iou"),
}
def seal_vegetation_shadow_lab(
*,
ddrnet_goose_root: Path,
ppliteseg_goose_root: Path,
ddrnet_ravnoves_root: Path,
ppliteseg_ravnoves_root: Path,
output_root: Path,
) -> Path:
roots = {
("ddrnet", "goose"): ddrnet_goose_root.resolve(),
("ppliteseg", "goose"): ppliteseg_goose_root.resolve(),
("ddrnet", "ravnoves"): ddrnet_ravnoves_root.resolve(),
("ppliteseg", "ravnoves"): ppliteseg_ravnoves_root.resolve(),
}
results = {
key: _read_worker_result(root, candidate=key[0], mode=key[1])
for key, root in roots.items()
}
cases = {key: _case_map(result, f"{key[0]}/{key[1]}") for key, result in results.items()}
for mode in _MODES:
if cases[("ddrnet", mode)].keys() != cases[("ppliteseg", mode)].keys():
raise VegetationShadowLabError(f"{mode} candidate case islands differ")
selected = _selected_candidate(results)
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]] = []
catalogs: dict[str, list[dict[str, object]]] = {"goose": [], "ravnoves": []}
try:
for mode in _MODES:
for case_id in sorted(cases[("ddrnet", mode)]):
ddr_case = cases[("ddrnet", mode)][case_id]
pplite_case = cases[("ppliteseg", mode)][case_id]
row: dict[str, object] = {
"case_id": case_id,
"source_kind": mode,
"width": ddr_case.get("source_width"),
"height": ddr_case.get("source_height"),
"center_crop_xyxy": ddr_case.get("center_crop_xyxy"),
"outside_crop_state": ddr_case.get("outside_crop_state"),
"assets": {},
}
asset_map = _object(row["assets"], "sealed assets")
sources: list[tuple[str, str, dict[str, Any], str]] = [
("source", "ddrnet", ddr_case, "source"),
("ddrnet", "ddrnet", ddr_case, "prediction_semantic"),
("ppliteseg", "ppliteseg", pplite_case, "prediction_semantic"),
]
if mode == "goose":
sources.append(("truth", "ddrnet", ddr_case, "truth_semantic"))
else:
selected_case = cases[(selected, mode)][case_id]
sources.extend(
(preset, selected, selected_case, f"policy_{preset}")
for preset in ("urban", "rural", "offroad")
)
for asset_key, candidate, case, worker_key in sources:
resolved = _file_from_case(roots[(candidate, mode)], case, worker_key)
assert resolved is not None
source_path, _ = resolved
relative = f"visual/{mode}/{case_id}/{asset_key}.png"
descriptor = _copy_artifact(
source_path,
temporary,
relative,
artifacts,
role=f"visual-{mode}-{asset_key}",
media_type="image/png",
)
asset_map[asset_key] = {
"path": descriptor["path"],
"sha256": descriptor["sha256"],
}
catalogs[mode].append(row)
worker_proofs: dict[str, dict[str, object]] = {}
for candidate in _CANDIDATES:
for mode in _MODES:
source = roots[(candidate, mode)] / "result.json"
relative = f"worker/{candidate}-{mode}.json"
descriptor = _copy_artifact(
source,
temporary,
relative,
artifacts,
role="worker-result",
media_type="application/json",
)
worker_proofs[f"{candidate}_{mode}"] = {
"result_id": results[(candidate, mode)].get("result_id"),
"path": relative,
"sha256": descriptor["sha256"],
}
candidate_metrics: dict[str, object] = {}
for candidate in _CANDIDATES:
validation = results[(candidate, "goose")]
shadow = results[(candidate, "ravnoves")]
candidate_metrics[candidate] = {
"loaded_model_name": _object(validation["candidate"], "candidate").get(
"loaded_model_name"
),
"checkpoint_sha256": _object(validation["candidate"], "candidate").get(
"checkpoint_sha256"
),
# Detailed per-class rows remain immutable in worker_proofs. The
# top-level LAB manifest carries only the UI/index summary.
"validation_metrics": _validation_metric_summary(validation, candidate),
"validation_timing": validation.get("timing"),
"shadow_timing": shadow.get("timing"),
"resource": shadow.get("resource"),
}
authority = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"actuation_accepted": False,
"camera_semantics_can_clear_rigid_geometry": False,
}
identity = {
"lab_id": "lab-v1-vegetation-mission-policy",
"source": {
"validation_dataset": "GOOSE-2D-validation-visible-962",
"shadow_session": "RAVNOVES00",
"shadow_camera": "sensor.camera.right",
"shadow_frame_count": 12,
},
"selected_candidate": selected,
"candidate_metrics": candidate_metrics,
"worker_proofs": worker_proofs,
"visual_catalog_sha256": hashlib.sha256(canonical_json(catalogs)).hexdigest(),
"authority": authority,
}
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
result_id = f"{RESULT_PREFIX}{identity_sha256}"
manifest = {
"schema_version": LAB_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"created_at_utc": datetime.now(UTC).isoformat(),
"ground_truth": False,
"status": "visual-shadow-ready-policy-not-authorized",
"identity": identity,
"source": identity["source"],
"method": {
"completeness": "complete",
"execution_class": "ai-inference",
"pipeline_id": "goose-fine64-ready-weights-to-ravnoves-policy-shadow/v1",
},
"metrics": {"candidates": candidate_metrics},
"decision": {
"selected_candidate": selected,
"visual_shadow_ready": True,
"mission_policy_ready_for_configuration": True,
"navigation_accepted": False,
"production_accepted": False,
},
"limitations": [
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
"The RAVNOVES island is visual shadow evidence without independent labels.",
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
],
"authority": authority,
"catalogs": catalogs,
"artifacts": artifacts,
}
(temporary / "result.json").write_bytes(canonical_json(manifest) + b"\n")
destination = output_root / result_id
if destination.exists():
raise VegetationShadowLabError("immutable vegetation LAB result already exists")
temporary.replace(destination)
return destination
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ddrnet-goose-root", type=Path, required=True)
parser.add_argument("--ppliteseg-goose-root", type=Path, required=True)
parser.add_argument("--ddrnet-ravnoves-root", type=Path, required=True)
parser.add_argument("--ppliteseg-ravnoves-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
return parser.parse_args()
def main() -> None:
args = _parse_args()
destination = seal_vegetation_shadow_lab(
ddrnet_goose_root=args.ddrnet_goose_root,
ppliteseg_goose_root=args.ppliteseg_goose_root,
ddrnet_ravnoves_root=args.ddrnet_ravnoves_root,
ppliteseg_ravnoves_root=args.ppliteseg_ravnoves_root,
output_root=args.output_root,
)
print(destination)
if __name__ == "__main__":
main()
__all__ = [
"LAB_SCHEMA",
"RESULT_PREFIX",
"VegetationShadowLabError",
"seal_vegetation_shadow_lab",
]
+12
View File
@@ -138,6 +138,7 @@ from k1link.web.m49_physical_safety_playback_api import (
)
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
from k1link.web.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -1019,6 +1020,17 @@ app.include_router(
),
)
)
app.include_router(
build_vegetation_shadow_lab_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "lab-v1-vegetation"
/ "results"
),
)
)
app.include_router(
build_m49_physical_safety_playback_router(
root_provider=lambda: (
+166
View File
@@ -0,0 +1,166 @@
"""Read-only API for the autonomous vegetation policy shadow LAB."""
from __future__ import annotations
import copy
import json
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Any, Final
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
LaboratoryEvidenceReportError,
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA, RESULT_PREFIX
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_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,
)
def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(
prefix="/api/v1/laboratory/vegetation-shadow",
tags=["laboratory"],
)
@router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, result_id)
return {**copy.deepcopy(_read_verified(candidate)), "access": "read-only"}
@router.get("/{result_id}/assets/{asset_path:path}")
def get_asset(result_id: str, asset_path: str) -> FileResponse:
candidate = _resolve_candidate(root_provider, result_id)
manifest = _read_verified(candidate)
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
descriptor = next(
(
item
for item in artifacts
if isinstance(item, dict) and item.get("path") == asset_path
),
None,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
relative = PurePosixPath(asset_path)
path = candidate.joinpath(*relative.parts)
if (
relative.is_absolute()
or str(relative) != asset_path
or any(part in {"", ".", ".."} for part in relative.parts)
or path.is_symlink()
or not path.is_file()
or not path.resolve().is_relative_to(candidate)
):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
media_type = descriptor.get("media_type")
if not isinstance(media_type, str) or not media_type.startswith("image/"):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
return FileResponse(
path,
media_type=media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{descriptor.get("sha256", "")}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
def _configured_root(provider: RootProvider) -> Path | None:
candidate = provider()
if candidate is None:
return None
absolute = candidate.expanduser().absolute()
if absolute.is_symlink():
return None
try:
root = absolute.resolve(strict=True)
except OSError:
return None
return root if root.is_dir() else None
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
root = _configured_root(provider)
if root is None or RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
candidate = root / result_id
if candidate.is_symlink():
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
try:
resolved = candidate.resolve(strict=True)
except OSError:
raise HTTPException(status_code=404, detail="Vegetation LAB result not found") from None
if not resolved.is_dir() or not resolved.is_relative_to(root):
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
return resolved
def _read_verified(candidate: Path) -> dict[str, Any]:
try:
rows: list[tuple[str, int, int, int, int]] = []
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
stat = path.lstat()
rows.append((
path.relative_to(candidate).as_posix(),
stat.st_mode,
stat.st_size,
stat.st_mtime_ns,
stat.st_ctime_ns,
))
signature = tuple(rows)
except OSError:
raise HTTPException(
status_code=503,
detail="Vegetation LAB evidence failed verification",
) from None
return _read_verified_cached(str(candidate), signature)
@lru_cache(maxsize=16)
def _read_verified_cached(
candidate_text: str,
signature: tuple[tuple[str, int, int, int, int], ...],
) -> dict[str, Any]:
del signature
candidate = Path(candidate_text)
try:
verify_laboratory_evidence_result(_DEFINITION, candidate)
path = candidate / "result.json"
if path.stat().st_size > _MAX_DOCUMENT_BYTES:
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
payload = json.loads(path.read_text("utf-8"))
except (json.JSONDecodeError, OSError, LaboratoryEvidenceReportError):
raise HTTPException(
status_code=503,
detail="Vegetation LAB evidence failed verification",
) from None
if not isinstance(payload, dict):
raise HTTPException(status_code=503, detail="Vegetation LAB evidence is invalid")
return payload
__all__ = ["build_vegetation_shadow_lab_router"]
@@ -0,0 +1,80 @@
from __future__ import annotations
import json
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
CONFIG_PATH = (
REPOSITORY_ROOT
/ "config"
/ "perception"
/ "lab-v1-goose-vegetation-benchmark-v1.json"
)
RUNNER_PATH = (
REPOSITORY_ROOT
/ "experiments"
/ "perception"
/ "worker"
/ "lab_v1_vegetation_goose"
/ "run_goose_vegetation_benchmark.py"
)
POWERSHELL_PATH = (
REPOSITORY_ROOT
/ "experiments"
/ "perception"
/ "worker"
/ "Invoke-LabV1VegetationGooseBenchmark.ps1"
)
def test_benchmark_contract_is_bounded_and_fail_closed() -> None:
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
assert config["schema_version"] == "missioncore.lab-v1-goose-vegetation-benchmark/v1"
assert config["dataset"]["expected_pair_count"] == 962
assert config["dataset"]["image_glob"].endswith("*_windshield_vis.png")
assert config["dataset"]["preprocessing"] == [
"center-square-crop",
"nearest-neighbor-resize",
"rgb-to-tensor-0-1",
]
assert set(config["candidates"]) == {"ddrnet", "ppliteseg"}
assert all(
len(candidate["checkpoint_sha256"]) == 64
for candidate in config["candidates"].values()
)
assert config["ravnoves"]["expected_frame_count"] == 4489
assert len(config["ravnoves"]["frame_indices"]) == 12
assert config["invariants"] == {
"one_heavy_candidate_at_a_time": True,
"raw_fisheye_is_immutable": True,
"outside_center_crop_is_free": False,
"missing_or_unknown_is_free": False,
"camera_semantics_can_clear_rigid_geometry": False,
"navigation_authority": False,
"actuation_authority": False,
"canonical_triton_mutation_allowed": False,
}
def test_runner_uses_exact_visible_pairs_and_never_grants_authority() -> None:
source = RUNNER_PATH.read_text(encoding="utf-8")
assert 'rglob("*_windshield_vis.png")' in source
assert "resample=RESAMPLE_NEAREST" in source
assert '"navigation_accepted": False' in source
assert '"actuation_accepted": False' in source
assert '"camera_semantics_can_clear_rigid_geometry": False' in source
assert "torch.cuda.reset_peak_memory_stats()" in source
assert 'warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)]' in source
assert '"prewarm_inference_count": len(warmup_latencies_ms)' in source
assert '"prewarm_latency_ms": round(warmup_latencies_ms[0], 4)' in source
assert '"prewarm_latency_ms_last": round(warmup_latencies_ms[-1], 4)' in source
def test_worker_wrapper_is_isolated_from_canonical_triton() -> None:
source = POWERSHELL_PATH.read_text(encoding="utf-8")
assert '$canonicalContainer = "ndc-mission-core-triton"' in source
assert '"--network", "none"' in source
assert '"--read-only"' in source
assert '"--cap-drop", "ALL"' in source
assert '"--security-opt", "no-new-privileges"' in source
assert "if ($canonicalAfter -ne $canonicalBefore)" in source
+2 -1
View File
@@ -127,8 +127,9 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
repository_root / "config" / "laboratories"
)
assert len(registry.definitions) == 42
assert len(registry.definitions) == 43
assert {item.work_id for item in registry.definitions} >= {
"lab-v1-vegetation-shadow",
"e31-source-binding",
"e46j-raw-fisheye-realtime",
"e47-semantic-slam-shadow",
+4
View File
@@ -103,6 +103,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
"m49-tgs-full-shadow",
"lab-v1-vegetation-shadow",
}
by_work_id = {row.work_id: row for row in execution.definitions}
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
@@ -124,6 +125,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
assert by_work_id["m49-tgs-fail-closed-evidence"].isolation == "bounded-adapter"
assert by_work_id["m49-tgs-full-shadow"].lifecycle == "experimental"
assert by_work_id["m49-tgs-full-shadow"].isolation == "bounded-adapter"
assert by_work_id["lab-v1-vegetation-shadow"].lifecycle == "experimental"
assert by_work_id["lab-v1-vegetation-shadow"].isolation == "bounded-adapter"
assert all(
row.lifecycle == "canonical"
for row in execution.definitions
@@ -134,6 +137,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
"m49-tgs-full-shadow",
"lab-v1-vegetation-shadow",
}
)
assert len(execution.definitions) + len(execution.legacy_work_ids) == len(
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
root / "config" / "laboratory-value-review.json"
)
assert len(registry.entries) == 40
assert len(registry.entries) == 41
assert {entry.catalog_id for entry in registry.entries} >= {
"e28-local-surface",
"e46d-temporal-failure-audit",
@@ -93,4 +93,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
"m49-tgs-full-shadow",
"lab-v1-vegetation-shadow",
}
+133
View File
@@ -0,0 +1,133 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
from k1link.laboratory.vegetation_shadow_lab import seal_vegetation_shadow_lab
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: float) -> None:
root.mkdir(parents=True)
visual_cases = []
for index in range(12):
case_id = f"case-{index:02d}"
case_root = root / "cases" / case_id
case_root.mkdir(parents=True)
keys = ["source", "prediction_semantic", "policy_urban", "policy_rural", "policy_offroad"]
if mode == "goose":
keys.append("truth_semantic")
files = {}
for key in keys:
path = case_root / f"{key}.png"
path.write_bytes(b"\x89PNG\r\n\x1a\n" + f"{candidate}:{mode}:{case_id}:{key}".encode())
files[key] = {
"relative_path": path.relative_to(root).as_posix(),
"sha256": _sha256(path),
}
visual_cases.append(
{
"case_id": case_id,
"source_width": 800 if mode == "ravnoves" else 512,
"source_height": 600 if mode == "ravnoves" else 512,
"center_crop_xyxy": [100, 0, 700, 600] if mode == "ravnoves" else [0, 0, 512, 512],
"outside_crop_state": "undefined" if mode == "ravnoves" else "not-applicable",
"files": files,
}
)
payload = {
"schema_version": "missioncore.lab-v1-goose-vegetation-run/v1",
"result_id": f"lab-v1-{mode}-{candidate}-fixture",
"mode": mode,
"candidate": {
"candidate_key": candidate,
"loaded_model_name": "ddrnet_39" if candidate == "ddrnet" else "pp_lite_t_seg",
"checkpoint_sha256": ("a" if candidate == "ddrnet" else "b") * 64,
},
"metrics": {
"mean_iou_percent": 44.0 + vegetation_iou,
"published_mean_iou_percent": 46.53 if candidate == "ddrnet" else 45.09,
"vegetation_mean_iou": vegetation_iou,
},
"timing": {
"latency_ms_p95": 20.0 if candidate == "ddrnet" else 15.0,
"throughput_fps_from_mean_inference": 55.0,
},
"resource": {
"peak_reserved_vram_bytes": 2_000_000_000,
"gpu_name": "fixture RTX 4090",
},
"visual_cases": visual_cases,
"authority": {
"navigation_accepted": False,
"safety_accepted": False,
"actuation_accepted": False,
"camera_semantics_can_clear_rigid_geometry": False,
},
}
(root / "result.json").write_text(json.dumps(payload), encoding="utf-8")
def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path) -> 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
result_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",
)
manifest = json.loads((result_root / "result.json").read_text("utf-8"))
assert manifest["decision"]["selected_candidate"] == "ddrnet"
assert manifest["ground_truth"] is False
assert manifest["authority"]["commands_enabled"] is False
assert manifest["authority"]["navigation_or_safety_accepted"] is False
assert len(manifest["catalogs"]["ravnoves"]) == 12
assert len(manifest["catalogs"]["goose"]) == 12
assert len(manifest["artifacts"]) == 124
assert "all_classes" not in manifest["metrics"]["candidates"]["ddrnet"]["validation_metrics"]
assert (result_root / "result.json").stat().st_size <= 64 * 1024
registry = LaboratoryEvidenceRegistry.from_directory(REPOSITORY_ROOT / "config/laboratories")
definition = next(
row for row in registry.definitions if row.work_id == "lab-v1-vegetation-shadow"
)
proof = verify_laboratory_evidence_result(definition, result_root)
assert proof["result_id"] == result_root.name
assert proof["artifact_count"] == 124
app = FastAPI()
app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent))
client = TestClient(app)
response = client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}")
assert response.status_code == 200
assert response.json()["access"] == "read-only"
asset_path = manifest["catalogs"]["ravnoves"][0]["assets"]["offroad"]["path"]
asset = client.get(
f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/assets/{asset_path}"
)
assert asset.status_code == 200
assert asset.headers["cache-control"].endswith("immutable")
(result_root / asset_path).write_bytes(b"tampered")
assert (
client.get(f"/api/v1/laboratory/vegetation-shadow/{result_root.name}").status_code
== 503
)