fix(perception): reuse M4.8 for vegetation evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 00:13:58 +03:00
parent 57204b3b0b
commit 67e6ae98e2
12 changed files with 482 additions and 226 deletions
@@ -4,11 +4,25 @@ 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;
const VALIDATION_ASSETS = [
"source",
"truth",
"ddrnet",
"ppliteseg",
"ddrnet_error",
"ppliteseg_error",
] as const;
export type VegetationCandidateKey = typeof CANDIDATES[number];
export type VegetationRouteMode = typeof ROUTE_MODES[number];
export type VegetationValidationMode = typeof VALIDATION_MODES[number];
export interface VegetationVisualFocus {
className: string;
labelId: number;
truthPixels: number;
truthFraction: number;
stratumRank: number;
}
export interface VegetationCandidateMetrics {
candidate: VegetationCandidateKey;
@@ -33,6 +47,7 @@ export interface VegetationVisualCase {
height: number;
centerCropXyxy: readonly [number, number, number, number];
outsideCropState: "undefined" | "not-applicable";
focus: VegetationVisualFocus | null;
assets: Readonly<Record<string, string>>;
}
@@ -174,7 +189,7 @@ function visualCaseValue(
.join("/")}`;
}
const expectedAssets = expectedKind === "goose"
? VALIDATION_MODES
? VALIDATION_ASSETS
: ROUTE_MODES;
if (expectedAssets.some((key) => !projected[key])) {
throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`);
@@ -183,6 +198,23 @@ function visualCaseValue(
if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") {
throw new VegetationShadowContractError("vegetation.case.outside_crop_state: контракт изменён.");
}
let focus: VegetationVisualFocus | null = null;
if (expectedKind === "goose") {
const rawFocus = objectValue(row.focus, "vegetation.case.focus");
const truthFraction = numberValue(rawFocus.truth_fraction, "vegetation.case.focus.truth_fraction");
if (truthFraction <= 0 || truthFraction > 1) {
throw new VegetationShadowContractError("vegetation.case.focus.truth_fraction: диапазон изменён.");
}
focus = {
className: textValue(rawFocus.class_name, "vegetation.case.focus.class_name"),
labelId: integerValue(rawFocus.label_id, "vegetation.case.focus.label_id"),
truthPixels: integerValue(rawFocus.truth_pixels, "vegetation.case.focus.truth_pixels"),
truthFraction,
stratumRank: integerValue(rawFocus.stratum_rank, "vegetation.case.focus.stratum_rank"),
};
} else if (row.focus !== null) {
throw new VegetationShadowContractError("vegetation.case.focus: RAVNOVES focus отсутствует.");
}
return {
caseId,
sourceKind: expectedKind,
@@ -190,6 +222,7 @@ function visualCaseValue(
height: integerValue(row.height, "vegetation.case.height"),
centerCropXyxy: crop as unknown as readonly [number, number, number, number],
outsideCropState,
focus,
assets: projected,
};
}
@@ -233,8 +266,8 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
.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 случаев.");
if (routeCases.length !== 0 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
}
return {
resultId,
@@ -28,6 +28,119 @@ const ATLAS_MODES = [
] as const;
type AtlasMode = typeof ATLAS_MODES[number]["value"];
const MASK_COMPARISON_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "prediction", label: "PREDICTION" },
{ value: "error", label: "ERROR" },
] as const;
const MASK_COMPARISON_CANDIDATES = [
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
type MaskComparisonMode = typeof MASK_COMPARISON_MODES[number]["value"];
type MaskComparisonCandidate = typeof MASK_COMPARISON_CANDIDATES[number]["value"];
export interface M48MaskComparisonCase {
caseId: string;
title: string;
context?: string;
sourceUrl: string;
truthUrl: string;
predictions: Readonly<Record<MaskComparisonCandidate, string>>;
errors: Readonly<Record<MaskComparisonCandidate, string>>;
}
function M48MaskComparisonScene({
item,
mode,
candidate,
}: {
item: M48MaskComparisonCase;
mode: MaskComparisonMode;
candidate: MaskComparisonCandidate;
}) {
const overlay = mode === "truth"
? item.truthUrl
: mode === "prediction"
? item.predictions[candidate]
: mode === "error"
? item.errors[candidate]
: null;
return (
<div className="recorded-evidence-image-scene">
<img src={item.sourceUrl} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function M48MaskComparisonVisual({
cases,
initialCandidate,
}: {
cases: readonly M48MaskComparisonCase[];
initialCandidate: MaskComparisonCandidate;
}) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<MaskComparisonMode>("error");
const [candidate, setCandidate] = useState<MaskComparisonCandidate>(initialCandidate);
const [expanded, setExpanded] = useState(false);
const item = cases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="M4.8 vegetation truth comparison"
className="m48-atlas-visual"
mode={mode}
modes={MASK_COMPARISON_MODES}
secondaryMode={{
value: candidate,
modes: MASK_COMPARISON_CANDIDATES,
label: "Сравниваемая модель",
onChange: setCandidate,
}}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
actions={(
<>
<IconButton
label="Предыдущий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current - 1 + cases.length) % cases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current + 1) % cases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE TRUTH · {index + 1}/{cases.length}</StatusBadge>
<strong>{item.title}</strong>
{item.context ? <small>{item.context}</small> : null}
</div>
) : null}
>
{item ? (
<M48MaskComparisonScene item={item} mode={mode} candidate={candidate} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
Vegetation hard-case каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
function message(error: unknown): string {
return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно.";
}
@@ -6,14 +6,45 @@ import {
} from "../../components/laboratory/LaboratoryPresentation";
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
import {
VegetationRouteVisual,
VegetationValidationVisual,
} from "./VegetationShadowVisual";
M48MaskComparisonVisual,
type M48MaskComparisonCase,
} from "./M48FailureAtlasVisual";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
const VEGETATION_LABELS: Readonly<Record<string, string>> = {
high_grass: "Высокая трава",
low_grass: "Низкая трава",
bush: "Куст",
tree_trunk: "Ствол дерева",
tree_crown: "Крона дерева",
hedge: "Живая изгородь",
forest: "Лесная растительность",
crops: "Посевы",
};
function comparisonCases(result: VegetationShadowResult): readonly M48MaskComparisonCase[] {
return result.validationCases.map((item) => {
const focus = item.focus!;
return {
caseId: item.caseId,
title: `${VEGETATION_LABELS[focus.className] ?? focus.className} · truth ${decimal(focus.truthFraction * 100, 1)}% кадра`,
sourceUrl: item.assets.source,
truthUrl: item.assets.truth,
predictions: {
ddrnet: item.assets.ddrnet,
ppliteseg: item.assets.ppliteseg,
},
errors: {
ddrnet: item.assets.ddrnet_error,
ppliteseg: item.assets.ppliteseg_error,
},
};
});
}
export function VegetationShadowResultView({
rigLabel,
result,
@@ -31,21 +62,21 @@ export function VegetationShadowResultView({
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB V1 · растительность и mission-policy пресеты"
description="Две готовые fine-64 модели GOOSE проверены на полном validation split и перенесены в автономный визуальный shadow по RAVNOVES00. Интерфейс читает sealed-кадры локально и не зависит от доступности Worker 006."
status="Визуальный shadow готов · navigation authority OFF"
title="LAB V1 · готовые модели растительности"
description="Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."
status="Truth-backed model comparison · route transfer не принят"
statusTone="warning"
facts={[
{ label: "Источник", value: `${rigLabel} RIGHT · 12 raw KB4 кадров + GOOSE validation 962` },
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
{ 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" },
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
]}
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.",
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. Ошибки по каждому типу теперь проверяются в одном штатном инструменте.`,
limitation: "Это внешний GOOSE-домен, а не наш fisheye/off-road маршрут. Папоротник отдельным классом отсутствует; RAVNOVES00 не содержит truth-backed vegetation island и не используется как главное визуальное доказательство.",
}}
method={{
completeness: "complete",
@@ -62,29 +93,22 @@ export function VegetationShadowResultView({
/>
)}
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>
</>
<LaboratoryEvidence
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
kind="diagnostic-model"
resizable
>
<M48MaskComparisonVisual
cases={comparisonCases(result)}
initialCandidate={result.selectedCandidate}
/>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Готовые веса дают рабочую точку старта, но ещё не право ехать"
status={`${selected.loadedModelName} выбран для shadow`}
title="DDRNet — стартовые веса; перенос на ровер ещё не доказан"
status={`${selected.loadedModelName} выбран только как vegetation candidate`}
statusTone="warning"
metrics={[
{
@@ -98,7 +122,7 @@ export function VegetationShadowResultView({
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
},
{
label: "RAVNOVES p95",
label: "Worker shadow p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
hint: "чистый inference · одна тяжёлая модель за раз",
},
@@ -108,7 +132,7 @@ export function VegetationShadowResultView({
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
},
{
label: "RAVNOVES throughput",
label: "Worker throughput",
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
hint: "изолированный Worker 006 · не realtime graph целиком",
},
@@ -118,15 +142,15 @@ export function VegetationShadowResultView({
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
},
{
label: "Autonomous evidence",
value: "12 route + 12 validation",
hint: "только текущий кадр загружается в viewer; Worker не требуется",
label: "Hard-case evidence",
value: "12 truth-backed cases",
hint: "8 vegetation strata · 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 геометрии.",
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить DDRNet как стартовый vegetation candidate. Mission-policy и автоматическое переключение пресетов подключать только после truth-backed island нашего офф-роуда; LiDAR/TGS fail-closed геометрию не ослаблять.",
}}
/>
)}
@@ -1,157 +0,0 @@
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>
);
}
@@ -65,10 +65,10 @@ 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`,
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation qualification`,
experimentId: "lab-v1-vegetation-mission-policy",
experimentName: "GOOSE ready weights → RAVNOVES vegetation policy",
variantName: "LAB V1 · DDRNet vs PPLiteSeg · urban/rural/off-road presets",
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
},
"m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
@@ -51,7 +52,7 @@ function candidate(candidateKey, vegetationIou) {
function visualCase(sourceKind, index) {
const caseId = `case-${index}`;
const keys = sourceKind === "goose"
? ["source", "truth", "ddrnet", "ppliteseg"]
? ["source", "truth", "ddrnet", "ppliteseg", "ddrnet_error", "ppliteseg_error"]
: ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"];
return {
case_id: caseId,
@@ -60,6 +61,13 @@ function visualCase(sourceKind, index) {
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",
focus: sourceKind === "goose" ? {
class_name: "high_grass",
label_id: 51,
truth_pixels: 16384,
truth_fraction: 0.0625,
stratum_rank: index + 1,
} : null,
assets: Object.fromEntries(keys.map((key) => [key, {
path: `visual/${sourceKind}/${caseId}/${key}.png`,
sha256: "d".repeat(64),
@@ -101,7 +109,7 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
},
catalogs: {
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
ravnoves: Array.from({ length: 12 }, (_, index) => visualCase("ravnoves", index)),
ravnoves: [],
},
access: "read-only",
}), { status: 200, headers: { "Content-Type": "application/json" } });
@@ -113,9 +121,10 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
);
assert.equal(result.selectedCandidate, "ddrnet");
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
assert.equal(result.routeCases.length, 12);
assert.equal(result.routeCases.length, 0);
assert.equal(result.validationCases.length, 12);
assert.match(result.routeCases[0].assets.offroad, /\/assets\/visual\/ravnoves\//);
assert.equal(result.validationCases[0].focus.className, "high_grass");
assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//);
assert.deepEqual(result.authority, {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
@@ -123,3 +132,17 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
cameraSemanticsCanClearRigidGeometry: false,
});
});
test("vegetation LAB reuses the admitted M4.8 instrument", async () => {
const resultSource = await readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
);
assert.match(resultSource, /M48MaskComparisonVisual/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
await assert.rejects(
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
{ code: "ENOENT" },
);
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": "missioncore.laboratory-value-review-registry/v1",
"reviewed_at_utc": "2026-08-27T20:20:14Z",
"reviewed_at_utc": "2026-08-27T20:59:10Z",
"entries": [
{
"catalog_id": "e28-local-surface",
@@ -285,7 +285,7 @@
{
"catalog_id": "lab-v1-vegetation-shadow",
"evidence_id": "lab-v1-vegetation-shadow-ad4d9fbbb21ff8a270b77f559b4e78dcdaf0455afd61afb5033009623984e554",
"signal": "progress",
"signal": "failed",
"lifecycle": "current",
"visual_evidence": "available"
}
@@ -79,6 +79,27 @@
"hedge",
"tree_root"
],
"visual_case_contract": {
"selection_basis": "ground-truth-class-support-only",
"case_count": 12,
"minimum_focus_pixels": 2048,
"strata": [
{ "class_name": "high_grass", "count": 2 },
{ "class_name": "low_grass", "count": 2 },
{ "class_name": "bush", "count": 2 },
{ "class_name": "tree_trunk", "count": 2 },
{ "class_name": "tree_crown", "count": 1 },
{ "class_name": "hedge", "count": 1 },
{ "class_name": "forest", "count": 1 },
{ "class_name": "crops", "count": 1 }
],
"error_overlay": {
"correct_material_rgba": [34, 197, 94, 72],
"missed_vegetation_rgba": [239, 68, 68, 220],
"false_vegetation_rgba": [245, 158, 11, 220],
"wrong_vegetation_material_rgba": [168, 85, 247, 220]
}
},
"policy_action_colors": {
"ALLOW": "#22c55e",
"HIGH_COST": "#f59e0b",
@@ -161,18 +161,81 @@ def find_goose_pairs(root: Path) -> list[tuple[Path, Path]]:
return pairs
def visual_indices(count: int, visual_count: int) -> set[int]:
def visual_indices(count: int, visual_count: int) -> dict[int, dict[str, Any]]:
if count <= 0 or visual_count <= 0:
return set()
return {}
selected_count = min(count, visual_count)
if selected_count == 1:
return {0}
return {0: {}}
return {
round(index * (count - 1) / (selected_count - 1))
round(index * (count - 1) / (selected_count - 1)): {}
for index in range(selected_count)
}
def truth_focused_visuals(
items: list[tuple[str, Path, Path | None]],
names: dict[int, str],
contract: dict[str, Any],
) -> dict[int, dict[str, Any]]:
if contract.get("selection_basis") != "ground-truth-class-support-only":
raise RunnerError("visual selection basis changed")
case_count = contract.get("case_count")
minimum_pixels = contract.get("minimum_focus_pixels")
strata = contract.get("strata")
if (
not isinstance(case_count, int)
or case_count <= 0
or not isinstance(minimum_pixels, int)
or minimum_pixels <= 0
or not isinstance(strata, list)
or sum(row.get("count", 0) for row in strata if isinstance(row, dict)) != case_count
):
raise RunnerError("visual case contract is invalid")
ids_by_name = {class_name: label_id for label_id, class_name in names.items()}
supports: list[dict[int, int]] = []
for _, _, label_path in items:
if label_path is None:
raise RunnerError("truth-focused selection requires labels")
truth = preprocess_label(Image.open(label_path))
values, counts = np.unique(truth, return_counts=True)
supports.append({int(value): int(count) for value, count in zip(values, counts)})
selected: dict[int, dict[str, Any]] = {}
for raw in strata:
if not isinstance(raw, dict):
raise RunnerError("visual stratum is invalid")
class_name = raw.get("class_name")
count = raw.get("count")
if class_name not in ids_by_name or not isinstance(count, int) or count <= 0:
raise RunnerError("visual stratum identity changed")
label_id = ids_by_name[class_name]
ranked = sorted(
(
(support.get(label_id, 0), items[index][0], index)
for index, support in enumerate(supports)
if index not in selected and support.get(label_id, 0) > 0
),
key=lambda row: (-row[0], row[1]),
)
admitted = [row for row in ranked if row[0] >= minimum_pixels]
if len(admitted) < count:
admitted = ranked
if len(admitted) < count:
raise RunnerError(f"visual stratum {class_name} has fewer than {count} cases")
for rank, (truth_pixels, _, index) in enumerate(admitted[:count], start=1):
selected[index] = {
"class_name": class_name,
"label_id": label_id,
"truth_pixels": truth_pixels,
"truth_fraction": round(truth_pixels / float(512 * 512), 8),
"stratum_rank": rank,
}
if len(selected) != case_count:
raise RunnerError("truth-focused visual selection did not produce the frozen case count")
return selected
def load_model(candidate: str, checkpoint: Path) -> tuple[torch.nn.Module, str, list[str]]:
failures: list[str] = []
for model_name in MODEL_NAMES[candidate]:
@@ -297,6 +360,9 @@ def write_visual_case(
policy_palettes: dict[str, np.ndarray],
crop_box: tuple[int, int, int, int],
truth: np.ndarray | None = None,
focus: dict[str, Any] | None = None,
material_codes: np.ndarray | None = None,
error_colors: dict[str, list[int]] | None = None,
preserve_source_size: bool = False,
) -> dict[str, Any]:
case_root = output / "cases" / case_id
@@ -343,6 +409,26 @@ def write_visual_case(
"relative_path": truth_semantic_path.relative_to(output).as_posix(),
"sha256": save_image(truth_semantic_path, semantic_palette[truth_image], "RGBA"),
}
if material_codes is None or error_colors is None:
raise RunnerError("truth visual case requires the material-error contract")
truth_material = material_codes[truth_image]
predicted_material = material_codes[prediction_image]
truth_vegetation = truth_material > 0
predicted_vegetation = predicted_material > 0
error_overlay = np.zeros((*truth_image.shape, 4), dtype=np.uint8)
correct = truth_vegetation & (truth_material == predicted_material)
missed = truth_vegetation & ~predicted_vegetation
false_positive = ~truth_vegetation & predicted_vegetation
wrong_material = truth_vegetation & predicted_vegetation & (truth_material != predicted_material)
error_overlay[correct] = error_colors["correct_material_rgba"]
error_overlay[missed] = error_colors["missed_vegetation_rgba"]
error_overlay[false_positive] = error_colors["false_vegetation_rgba"]
error_overlay[wrong_material] = error_colors["wrong_vegetation_material_rgba"]
error_path = case_root / "vegetation-material-error.png"
files["vegetation_material_error"] = {
"relative_path": error_path.relative_to(output).as_posix(),
"sha256": save_image(error_path, error_overlay, "RGBA"),
}
return {
"schema_version": VISUAL_SCHEMA,
"case_id": case_id,
@@ -350,6 +436,7 @@ def write_visual_case(
"source_height": source_image.height,
"center_crop_xyxy": list(crop_box),
"outside_crop_state": "undefined" if preserve_source_size else "not-applicable",
"focus": focus,
"files": files,
}
@@ -400,6 +487,27 @@ def run() -> None:
)
for preset in ("urban", "rural", "offroad")
}
provider_labels = provider_map["providers"]["goose-fine-64"]["labels"]
vegetation_materials = sorted(
{
material
for material in provider_labels.values()
if material in {
"grass",
"herbaceous_vegetation",
"cultivated_vegetation",
"woody_shrub",
"tree_or_trunk",
"vegetation_unknown",
}
}
)
material_code_by_name = {
material: index for index, material in enumerate(vegetation_materials, start=1)
}
material_codes = np.zeros(CLASS_COUNT, dtype=np.uint8)
for label_id, class_name in names.items():
material_codes[label_id] = material_code_by_name.get(provider_labels.get(class_name), 0)
if args.mode == "goose":
pairs = find_goose_pairs(mapping_root)
@@ -423,6 +531,17 @@ def run() -> None:
if not items:
raise RunnerError("no inputs were selected")
visual_contract = config.get("visual_case_contract")
if not isinstance(visual_contract, dict):
raise RunnerError("visual case contract is unavailable")
configured_visual_count = visual_contract.get("case_count")
if args.mode == "goose":
if args.visual_count != configured_visual_count:
raise RunnerError("GOOSE visual count differs from the truth-focused contract")
selected_visuals = truth_focused_visuals(items, names, visual_contract)
else:
selected_visuals = visual_indices(len(items), args.visual_count)
args.output.mkdir(parents=True, exist_ok=False)
torch.cuda.empty_cache()
model, model_name, architecture_failures = load_model(args.candidate, args.checkpoint)
@@ -430,7 +549,6 @@ def run() -> None:
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]] = []
@@ -454,6 +572,9 @@ def run() -> None:
policy_palettes,
crop_box,
truth=truth,
focus=selected_visuals[index] or None,
material_codes=material_codes,
error_colors=visual_contract["error_overlay"],
preserve_source_size=args.mode == "ravnoves",
)
)
+57 -4
View File
@@ -16,6 +16,16 @@ 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")
_FOCUS_ORDER: Final = (
"high_grass",
"low_grass",
"bush",
"tree_trunk",
"tree_crown",
"hedge",
"forest",
"crops",
)
_IMAGE_KEYS: Final = (
"source",
"prediction_semantic",
@@ -93,6 +103,22 @@ def _case_map(result: dict[str, Any], label: str) -> dict[str, dict[str, Any]]:
return rows
def _visual_case_order(row: dict[str, Any]) -> tuple[int, int, str]:
focus = _object(row.get("focus"), "GOOSE visual focus")
class_name = focus.get("class_name")
stratum_rank = focus.get("stratum_rank")
case_id = row.get("case_id")
if (
not isinstance(class_name, str)
or class_name not in _FOCUS_ORDER
or not isinstance(stratum_rank, int)
or stratum_rank <= 0
or not isinstance(case_id, str)
):
raise VegetationShadowLabError("GOOSE visual focus ordering is invalid")
return _FOCUS_ORDER.index(class_name), stratum_rank, case_id
def _file_from_case(
root: Path,
case: dict[str, Any],
@@ -198,8 +224,14 @@ def seal_vegetation_shadow_lab(
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)]):
# RAVNOVES is retained in the immutable Worker proof and timing summary,
# but it has no vegetation truth island. Publishing those urban frames
# as primary visual cases would misrepresent the operator question.
for mode in ("goose",):
for case_id in sorted(
cases[("ddrnet", mode)],
key=lambda value: _visual_case_order(cases[("ddrnet", mode)][value]),
):
ddr_case = cases[("ddrnet", mode)][case_id]
pplite_case = cases[("ppliteseg", mode)][case_id]
row: dict[str, object] = {
@@ -209,8 +241,13 @@ def seal_vegetation_shadow_lab(
"height": ddr_case.get("source_height"),
"center_crop_xyxy": ddr_case.get("center_crop_xyxy"),
"outside_crop_state": ddr_case.get("outside_crop_state"),
"focus": ddr_case.get("focus"),
"assets": {},
}
if ddr_case.get("focus") != pplite_case.get("focus"):
raise VegetationShadowLabError(
f"{mode} case {case_id} focus contract differs between candidates"
)
asset_map = _object(row["assets"], "sealed assets")
sources: list[tuple[str, str, dict[str, Any], str]] = [
("source", "ddrnet", ddr_case, "source"),
@@ -218,7 +255,23 @@ def seal_vegetation_shadow_lab(
("ppliteseg", "ppliteseg", pplite_case, "prediction_semantic"),
]
if mode == "goose":
sources.append(("truth", "ddrnet", ddr_case, "truth_semantic"))
sources.extend(
(
("truth", "ddrnet", ddr_case, "truth_semantic"),
(
"ddrnet_error",
"ddrnet",
ddr_case,
"vegetation_material_error",
),
(
"ppliteseg_error",
"ppliteseg",
pplite_case,
"vegetation_material_error",
),
)
)
else:
selected_case = cases[(selected, mode)][case_id]
sources.extend(
@@ -328,7 +381,7 @@ def seal_vegetation_shadow_lab(
},
"limitations": [
"GOOSE validation is external-domain qualification, not RAVNOVES ground truth.",
"The RAVNOVES island is visual shadow evidence without independent labels.",
"The RAVNOVES shadow remains in Worker proofs and is not catalogued as vegetation evidence because it has no independent labels.",
"Vegetation semantics never clears rigid LiDAR/TGS occupancy.",
"Undefined pixels outside the 600x600 center crop remain fail-closed.",
],
@@ -44,6 +44,19 @@ def test_benchmark_contract_is_bounded_and_fail_closed() -> None:
)
assert config["ravnoves"]["expected_frame_count"] == 4489
assert len(config["ravnoves"]["frame_indices"]) == 12
assert config["visual_case_contract"]["selection_basis"] == (
"ground-truth-class-support-only"
)
assert config["visual_case_contract"]["case_count"] == 12
assert sum(
row["count"] for row in config["visual_case_contract"]["strata"]
) == 12
assert {row["class_name"] for row in config["visual_case_contract"]["strata"]} >= {
"high_grass",
"low_grass",
"bush",
"tree_trunk",
}
assert config["invariants"] == {
"one_heavy_candidate_at_a_time": True,
"raw_fisheye_is_immutable": True,
@@ -68,6 +81,8 @@ def test_runner_uses_exact_visible_pairs_and_never_grants_authority() -> None:
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
assert "truth_focused_visuals(items, names, visual_contract)" in source
assert 'files["vegetation_material_error"]' in source
def test_worker_wrapper_is_isolated_from_canonical_triton() -> None:
+15 -5
View File
@@ -28,7 +28,7 @@ def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: flo
case_root.mkdir(parents=True)
keys = ["source", "prediction_semantic", "policy_urban", "policy_rural", "policy_offroad"]
if mode == "goose":
keys.append("truth_semantic")
keys.extend(("truth_semantic", "vegetation_material_error"))
files = {}
for key in keys:
path = case_root / f"{key}.png"
@@ -44,6 +44,13 @@ def _worker_result(root: Path, *, candidate: str, mode: str, vegetation_iou: flo
"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",
"focus": {
"class_name": "high_grass",
"label_id": 51,
"truth_pixels": 16384,
"truth_fraction": 0.0625,
"stratum_rank": index + 1,
} if mode == "goose" else None,
"files": files,
}
)
@@ -99,9 +106,12 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
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"]["ravnoves"]) == 0
assert len(manifest["catalogs"]["goose"]) == 12
assert len(manifest["artifacts"]) == 124
assert len(manifest["artifacts"]) == 76
assert manifest["catalogs"]["goose"][0]["focus"]["class_name"] == "high_grass"
assert "ddrnet_error" in manifest["catalogs"]["goose"][0]["assets"]
assert "ppliteseg_error" in manifest["catalogs"]["goose"][0]["assets"]
assert "all_classes" not in manifest["metrics"]["candidates"]["ddrnet"]["validation_metrics"]
assert (result_root / "result.json").stat().st_size <= 64 * 1024
@@ -111,7 +121,7 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
)
proof = verify_laboratory_evidence_result(definition, result_root)
assert proof["result_id"] == result_root.name
assert proof["artifact_count"] == 124
assert proof["artifact_count"] == 76
app = FastAPI()
app.include_router(build_vegetation_shadow_lab_router(root_provider=lambda: result_root.parent))
@@ -119,7 +129,7 @@ def test_vegetation_shadow_lab_seals_autonomous_visual_evidence(tmp_path: Path)
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_path = manifest["catalogs"]["goose"][0]["assets"]["ddrnet_error"]["path"]
asset = client.get(
f"/api/v1/laboratory/vegetation-shadow/{result_root.name}/assets/{asset_path}"
)