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);
}