Compare commits
5
Commits
67d5d6fa05
...
296cf610cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
296cf610cd | ||
|
|
46cef8df17 | ||
|
|
d722b0f82b | ||
|
|
40c850b167 | ||
|
|
6544d9e918 |
@@ -41,8 +41,25 @@ export interface LaboratoryMetricCorridorVisual {
|
|||||||
halfWidthM: number;
|
halfWidthM: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LaboratoryMetricCellState =
|
||||||
|
| "unobserved"
|
||||||
|
| "ground-support"
|
||||||
|
| "nonground-occupied"
|
||||||
|
| "unknown-rejected";
|
||||||
|
|
||||||
|
export interface LaboratoryMetricCellEvidence {
|
||||||
|
centerBodyXyM: readonly [number, number];
|
||||||
|
zBoundsM: readonly [number | null, number | null];
|
||||||
|
state: LaboratoryMetricCellState;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LaboratoryMetricLegendEntry {
|
export interface LaboratoryMetricLegendEntry {
|
||||||
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling" | "low-step";
|
id: LaboratoryMetricDecision
|
||||||
|
| LaboratoryMetricCellState
|
||||||
|
| "context"
|
||||||
|
| "local-surface"
|
||||||
|
| "rolling"
|
||||||
|
| "low-step";
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +214,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
pointSemanticClassIds?: readonly (number | null)[];
|
pointSemanticClassIds?: readonly (number | null)[];
|
||||||
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
|
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
|
||||||
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
|
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||||
|
classifiedCells?: readonly LaboratoryMetricCellEvidence[];
|
||||||
|
classifiedCellSizeM?: number;
|
||||||
|
showClassifiedCells?: boolean;
|
||||||
}
|
}
|
||||||
>(function LaboratoryMetricEvidenceScene({
|
>(function LaboratoryMetricEvidenceScene({
|
||||||
pointCloudBodyXyzM,
|
pointCloudBodyXyzM,
|
||||||
@@ -214,6 +234,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
pointSemanticClassIds,
|
pointSemanticClassIds,
|
||||||
semanticClasses,
|
semanticClasses,
|
||||||
semanticPalette,
|
semanticPalette,
|
||||||
|
classifiedCells = [],
|
||||||
|
classifiedCellSizeM = 0.45,
|
||||||
|
showClassifiedCells = true,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||||
@@ -374,6 +397,63 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showClassifiedCells && classifiedCells.length) {
|
||||||
|
const cellsByState = new Map<LaboratoryMetricCellState, LaboratoryMetricCellEvidence[]>();
|
||||||
|
for (const cell of classifiedCells) {
|
||||||
|
const cells = cellsByState.get(cell.state) ?? [];
|
||||||
|
cells.push(cell);
|
||||||
|
cellsByState.set(cell.state, cells);
|
||||||
|
}
|
||||||
|
for (const [state, cells] of cellsByState) {
|
||||||
|
const color = state === "ground-support"
|
||||||
|
? tokenColor(host, "--nodedc-success-rgb", [181, 255, 90])
|
||||||
|
: state === "nonground-occupied"
|
||||||
|
? tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112])
|
||||||
|
: state === "unknown-rejected"
|
||||||
|
? tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92])
|
||||||
|
: tokenColor(host, "--nodedc-text-muted", [96, 99, 106]);
|
||||||
|
const geometry = new THREE.BoxGeometry(
|
||||||
|
classifiedCellSizeM * 0.92,
|
||||||
|
1,
|
||||||
|
classifiedCellSizeM * 0.92,
|
||||||
|
);
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: state === "unobserved" ? 0.035 : state === "ground-support" ? 0.12 : 0.24,
|
||||||
|
depthWrite: false,
|
||||||
|
});
|
||||||
|
const mesh = new THREE.InstancedMesh(geometry, material, cells.length);
|
||||||
|
const matrix = new THREE.Matrix4();
|
||||||
|
const scale = new THREE.Vector3(1, 1, 1);
|
||||||
|
const rotation = new THREE.Quaternion();
|
||||||
|
cells.forEach((cell, index) => {
|
||||||
|
const minimum = cell.zBoundsM[0];
|
||||||
|
const maximum = cell.zBoundsM[1];
|
||||||
|
const height = minimum === null || maximum === null
|
||||||
|
? 0.018
|
||||||
|
: Math.max(0.018, maximum - minimum);
|
||||||
|
const centerZ = minimum === null || maximum === null
|
||||||
|
? -0.012
|
||||||
|
: (minimum + maximum) / 2;
|
||||||
|
const [sceneX, sceneY, sceneZ] = scenePoint([
|
||||||
|
cell.centerBodyXyM[0],
|
||||||
|
cell.centerBodyXyM[1],
|
||||||
|
centerZ,
|
||||||
|
]);
|
||||||
|
scale.set(1, height, 1);
|
||||||
|
matrix.compose(
|
||||||
|
new THREE.Vector3(sceneX, sceneY, sceneZ),
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
mesh.setMatrixAt(index, matrix);
|
||||||
|
});
|
||||||
|
mesh.instanceMatrix.needsUpdate = true;
|
||||||
|
content.add(mesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const obstacle of obstacles) {
|
for (const obstacle of obstacles) {
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
@@ -451,7 +531,10 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
pointSemanticClassIds,
|
pointSemanticClassIds,
|
||||||
semanticClasses,
|
semanticClasses,
|
||||||
semanticPalette,
|
semanticPalette,
|
||||||
|
classifiedCells,
|
||||||
|
classifiedCellSizeM,
|
||||||
showCurrentIncrement,
|
showCurrentIncrement,
|
||||||
|
showClassifiedCells,
|
||||||
showLocalSurface,
|
showLocalSurface,
|
||||||
showRollingMap,
|
showRollingMap,
|
||||||
showLowStep,
|
showLowStep,
|
||||||
@@ -563,7 +646,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
const metricLegendEntries = laboratoryMetricLegendEntries({
|
const metricLegendEntries = laboratoryMetricLegendEntries({
|
||||||
pointCloudCount: pointCloudBodyXyzM.length,
|
pointCloudCount: pointSemanticClassIds?.every((item) => item !== null)
|
||||||
|
? 0
|
||||||
|
: pointCloudBodyXyzM.length,
|
||||||
localSurfaceCount: localSurfaceBodyXyzM.length,
|
localSurfaceCount: localSurfaceBodyXyzM.length,
|
||||||
obstacles,
|
obstacles,
|
||||||
showCurrentIncrement,
|
showCurrentIncrement,
|
||||||
@@ -571,6 +656,28 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
showRollingMap,
|
showRollingMap,
|
||||||
showLowStep,
|
showLowStep,
|
||||||
});
|
});
|
||||||
|
const classifiedLegendEntries = (() => {
|
||||||
|
if (!showClassifiedCells || !classifiedCells.length) return [];
|
||||||
|
const states = new Set(classifiedCells.map((cell) => cell.state));
|
||||||
|
return [
|
||||||
|
states.has("ground-support")
|
||||||
|
? { id: "ground-support" as const, label: "Ground support" }
|
||||||
|
: null,
|
||||||
|
states.has("nonground-occupied")
|
||||||
|
? { id: "nonground-occupied" as const, label: "Non-ground occupied" }
|
||||||
|
: null,
|
||||||
|
states.has("unknown-rejected")
|
||||||
|
? { id: "unknown-rejected" as const, label: "Unknown / rejected" }
|
||||||
|
: null,
|
||||||
|
states.has("unobserved")
|
||||||
|
? { id: "unobserved" as const, label: "Unobserved" }
|
||||||
|
: null,
|
||||||
|
]
|
||||||
|
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
||||||
|
.filter((entry) => !semanticLegendEntries.some(
|
||||||
|
(semanticEntry) => semanticEntry.label === entry.label,
|
||||||
|
));
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="laboratory-metric-evidence-scene">
|
<div className="laboratory-metric-evidence-scene">
|
||||||
@@ -581,6 +688,9 @@ LaboratoryMetricEvidenceSceneHandle,
|
|||||||
{metricLegendEntries.map((entry) => (
|
{metricLegendEntries.map((entry) => (
|
||||||
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
|
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
|
||||||
))}
|
))}
|
||||||
|
{classifiedLegendEntries.map((entry) => (
|
||||||
|
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
|
||||||
|
))}
|
||||||
{semanticLegendEntries.map((entry) => (
|
{semanticLegendEntries.map((entry) => (
|
||||||
<span
|
<span
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualif
|
|||||||
import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
|
import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
|
||||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
|
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||||
|
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||||
|
|
||||||
export type AdvancedLaboratoryWorkId =
|
export type AdvancedLaboratoryWorkId =
|
||||||
| "m48-object-centric-quality"
|
| "m48-object-centric-quality"
|
||||||
@@ -54,6 +56,8 @@ export type AdvancedLaboratoryWorkId =
|
|||||||
| "m48r3-static-occupancy-shadow"
|
| "m48r3-static-occupancy-shadow"
|
||||||
| "m48s-fixed-class-detector"
|
| "m48s-fixed-class-detector"
|
||||||
| "m48t-risk-quality-temporal"
|
| "m48t-risk-quality-temporal"
|
||||||
|
| "m49-tgs-fail-closed-evidence"
|
||||||
|
| "m49-tgs-full-shadow"
|
||||||
| "m47-reference-graph-shadow"
|
| "m47-reference-graph-shadow"
|
||||||
| "m4-replay-threat"
|
| "m4-replay-threat"
|
||||||
| "l3-pointpillars-visual-audit"
|
| "l3-pointpillars-visual-audit"
|
||||||
@@ -102,6 +106,8 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
|||||||
"m48r3-static-occupancy-shadow",
|
"m48r3-static-occupancy-shadow",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
"m4-replay-threat",
|
"m4-replay-threat",
|
||||||
"l3-pointpillars-visual-audit",
|
"l3-pointpillars-visual-audit",
|
||||||
@@ -145,6 +151,8 @@ const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
|||||||
"m48r3-static-occupancy-shadow": "m48r3-static-occupancy-shadow",
|
"m48r3-static-occupancy-shadow": "m48r3-static-occupancy-shadow",
|
||||||
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
||||||
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
||||||
|
"m49-tgs-fail-closed-evidence": "m49-tgs-fail-closed",
|
||||||
|
"m49-tgs-full-shadow": "m49-tgs-full-shadow",
|
||||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||||
"m4-replay-threat": "m4-threat-replay",
|
"m4-replay-threat": "m4-threat-replay",
|
||||||
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
||||||
@@ -196,6 +204,8 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
|||||||
m48r3StaticOccupancy: null,
|
m48r3StaticOccupancy: null,
|
||||||
m48s: null,
|
m48s: null,
|
||||||
m48t: null,
|
m48t: null,
|
||||||
|
m49Tgs: null,
|
||||||
|
m49TgsFull: null,
|
||||||
m4Threat: null,
|
m4Threat: null,
|
||||||
l3: null,
|
l3: null,
|
||||||
l31: null,
|
l31: null,
|
||||||
@@ -326,6 +336,8 @@ export function advancedLaboratoryResultAvailable(
|
|||||||
: workId === "m48r3-static-occupancy-shadow" ? results.m48r3StaticOccupancy !== null
|
: workId === "m48r3-static-occupancy-shadow" ? results.m48r3StaticOccupancy !== null
|
||||||
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
||||||
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
||||||
|
: workId === "m49-tgs-fail-closed-evidence" ? results.m49Tgs !== null
|
||||||
|
: workId === "m49-tgs-full-shadow" ? results.m49TgsFull !== null
|
||||||
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
||||||
: workId === "m4-replay-threat" ? results.m4Threat !== null
|
: workId === "m4-replay-threat" ? results.m4Threat !== null
|
||||||
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
||||||
@@ -393,6 +405,12 @@ export async function fetchAdvancedLaboratoryResult(
|
|||||||
} else if (workId === "m48t-risk-quality-temporal") {
|
} else if (workId === "m48t-risk-quality-temporal") {
|
||||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8T LAB identity не выбрана.");
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8T LAB identity не выбрана.");
|
||||||
results.m48t = await fetchM48TRiskQualityResult(resultId, { fetcher, signal });
|
results.m48t = await fetchM48TRiskQualityResult(resultId, { fetcher, signal });
|
||||||
|
} else if (workId === "m49-tgs-fail-closed-evidence") {
|
||||||
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 TGS LAB identity не выбрана.");
|
||||||
|
results.m49Tgs = await fetchM49TgsFailClosedResult(resultId, { fetcher, signal });
|
||||||
|
} else if (workId === "m49-tgs-full-shadow") {
|
||||||
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 full TGS shadow identity не выбрана.");
|
||||||
|
results.m49TgsFull = await fetchM49TgsFullShadowResult(resultId, { fetcher, signal });
|
||||||
} else if (workId === "m47-reference-graph-shadow") {
|
} else if (workId === "m47-reference-graph-shadow") {
|
||||||
if (!resultId) {
|
if (!resultId) {
|
||||||
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
|
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancy
|
|||||||
import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancyShadow";
|
import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancyShadow";
|
||||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
|
import type { M49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||||
|
import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||||
|
|
||||||
export interface AdvancedLaboratoryResults {
|
export interface AdvancedLaboratoryResults {
|
||||||
m47Graph: M47ReferenceGraphLabResult | null;
|
m47Graph: M47ReferenceGraphLabResult | null;
|
||||||
@@ -49,6 +51,8 @@ export interface AdvancedLaboratoryResults {
|
|||||||
m48r3StaticOccupancy: M48R3StaticOccupancyShadowResult | null;
|
m48r3StaticOccupancy: M48R3StaticOccupancyShadowResult | null;
|
||||||
m48s: M48SFixedClassDetectorResult | null;
|
m48s: M48SFixedClassDetectorResult | null;
|
||||||
m48t: M48TRiskQualityResult | null;
|
m48t: M48TRiskQualityResult | null;
|
||||||
|
m49Tgs: M49TgsFailClosedResult | null;
|
||||||
|
m49TgsFull: M49TgsFullShadowResult | null;
|
||||||
m4Threat: M4ThreatReplayResult | null;
|
m4Threat: M4ThreatReplayResult | null;
|
||||||
l3: L3PointPillarsVisualAuditResult | null;
|
l3: L3PointPillarsVisualAuditResult | null;
|
||||||
l31: L31PointPillarsRavnovesResult | null;
|
l31: L31PointPillarsRavnovesResult | null;
|
||||||
|
|||||||
@@ -969,7 +969,7 @@ export async function fetchAdvancedLaboratoryResults({
|
|||||||
return {
|
return {
|
||||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||||
m48r3StaticOccupancy: null,
|
m48r3StaticOccupancy: null,
|
||||||
m48s: null, m48t: null, m4Threat: null,
|
m48s: null, m48t: null, m49Tgs: null, m49TgsFull: null, m4Threat: null,
|
||||||
l3: null, l31: null, l32: null, l33: null,
|
l3: null, l31: null, l32: null, l33: null,
|
||||||
e31,
|
e31,
|
||||||
e32,
|
e32,
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import type { LaboratoryFetch } from "./advancedResults";
|
||||||
|
|
||||||
|
const RESULT_ID = /^m49-tgs-fail-closed-[a-f0-9]{64}$/;
|
||||||
|
|
||||||
|
export type M49TgsProfile = "current_increment" | "causal_rolling_1s";
|
||||||
|
export type M49TgsStateCode = 0 | 1 | 2 | 3;
|
||||||
|
export type M49TgsPointStateCode = 1 | 2 | 3;
|
||||||
|
|
||||||
|
export interface M49TgsAnchorSummary {
|
||||||
|
anchorFrameIndex: number;
|
||||||
|
slot: number;
|
||||||
|
pointCount: number;
|
||||||
|
groundPointCount: number;
|
||||||
|
nongroundPointCount: number;
|
||||||
|
rejectedPointCount: number;
|
||||||
|
groundCellCount: number;
|
||||||
|
nongroundCellCount: number;
|
||||||
|
rejectedCellCount: number;
|
||||||
|
unobservedCellCount: number;
|
||||||
|
allPointsAccounted: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M49TgsFailClosedResult {
|
||||||
|
resultId: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
source: {
|
||||||
|
sourceId: "RAVNOVES00";
|
||||||
|
sourceSessionId: "20260720T065719Z_viewer_live";
|
||||||
|
sourcePackSha256: string;
|
||||||
|
linkedVisualResultId: string;
|
||||||
|
anchorFrameIndices: readonly number[];
|
||||||
|
};
|
||||||
|
configuration: {
|
||||||
|
profileId: string;
|
||||||
|
configSha256: string;
|
||||||
|
coordinateFrame: "map-gravity-local";
|
||||||
|
primaryProfile: "causal_rolling_1s";
|
||||||
|
cellSizeM: number;
|
||||||
|
radiusM: number;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
worker: "Worker 006";
|
||||||
|
device: "cpu";
|
||||||
|
gpuUsed: false;
|
||||||
|
wrapperElapsedSeconds: number;
|
||||||
|
};
|
||||||
|
metrics: {
|
||||||
|
anchorCount: number;
|
||||||
|
anchorProfileCount: number;
|
||||||
|
allEligiblePointsAccounted: true;
|
||||||
|
costmapCellCount: number;
|
||||||
|
processWallCurrentP50Ms: number;
|
||||||
|
processWallCurrentMaxMs: number;
|
||||||
|
processWallRollingP50Ms: number;
|
||||||
|
processWallRollingMaxMs: number;
|
||||||
|
processMaxRssKib: number;
|
||||||
|
primary: readonly M49TgsAnchorSummary[];
|
||||||
|
};
|
||||||
|
acceptance: {
|
||||||
|
representationComplete: true;
|
||||||
|
allPointsAccounted: true;
|
||||||
|
aosAbsent: true;
|
||||||
|
gpuAbsent: true;
|
||||||
|
visualQualityAccepted: false;
|
||||||
|
traversabilityAccepted: false;
|
||||||
|
};
|
||||||
|
decision: {
|
||||||
|
state: "visual-review-required";
|
||||||
|
candidateRetained: true;
|
||||||
|
nextAction: string;
|
||||||
|
};
|
||||||
|
limitations: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M49TgsAnchorSpatial {
|
||||||
|
resultId: string;
|
||||||
|
anchorFrameIndex: number;
|
||||||
|
sourceSequence: number;
|
||||||
|
profile: M49TgsProfile;
|
||||||
|
coordinateFrame: "map-gravity-local";
|
||||||
|
pointsXyzM: readonly (readonly [number, number, number])[];
|
||||||
|
pointStates: readonly M49TgsPointStateCode[];
|
||||||
|
costmap: {
|
||||||
|
cellSizeM: number;
|
||||||
|
radiusM: number;
|
||||||
|
centersXyM: readonly (readonly [number, number])[];
|
||||||
|
states: readonly M49TgsStateCode[];
|
||||||
|
zBoundsM: readonly (readonly [number | null, number | null])[];
|
||||||
|
};
|
||||||
|
allPointsAccounted: true;
|
||||||
|
aosUsed: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class M49TgsContractError extends Error {}
|
||||||
|
|
||||||
|
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new M49TgsContractError(`${label}: ожидался объект.`);
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(value: unknown, label: string): string {
|
||||||
|
if (typeof value !== "string" || !value.trim()) {
|
||||||
|
throw new M49TgsContractError(`${label}: ожидалась строка.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exact<T extends string | boolean>(value: unknown, expected: T, label: string): T {
|
||||||
|
if (value !== expected) throw new M49TgsContractError(`${label}: нарушен контракт.`);
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown, label: string): number {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
|
throw new M49TgsContractError(`${label}: ожидалось число.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, label: string): number {
|
||||||
|
const parsed = numberValue(value, label);
|
||||||
|
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
||||||
|
throw new M49TgsContractError(`${label}: ожидалось целое число.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(value: unknown, label: string): string {
|
||||||
|
const parsed = text(value, label);
|
||||||
|
if (!/^[a-f0-9]{64}$/.test(parsed)) {
|
||||||
|
throw new M49TgsContractError(`${label}: неверный SHA-256.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultId(value: unknown): string {
|
||||||
|
const parsed = text(value, "M49 result id");
|
||||||
|
if (!RESULT_ID.test(parsed)) throw new M49TgsContractError("M49 identity недопустима.");
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numbers(value: unknown, size: number, label: string): number[] {
|
||||||
|
if (!Array.isArray(value) || value.length !== size) {
|
||||||
|
throw new M49TgsContractError(`${label}: неверная размерность.`);
|
||||||
|
}
|
||||||
|
return value.map((item) => numberValue(item, label));
|
||||||
|
}
|
||||||
|
|
||||||
|
function anchor(value: unknown, label: string): M49TgsAnchorSummary {
|
||||||
|
const row = objectValue(value, label);
|
||||||
|
exact(row.profile_id, "causal_rolling_1s", `${label}.profile`);
|
||||||
|
exact(row.all_points_accounted, true, `${label}.accounting`);
|
||||||
|
const parsed: M49TgsAnchorSummary = {
|
||||||
|
anchorFrameIndex: integer(row.anchor_frame_index, `${label}.anchor`),
|
||||||
|
slot: integer(row.slot, `${label}.slot`),
|
||||||
|
pointCount: integer(row.point_count, `${label}.points`),
|
||||||
|
groundPointCount: integer(row.ground_point_count, `${label}.ground points`),
|
||||||
|
nongroundPointCount: integer(row.nonground_point_count, `${label}.nonground points`),
|
||||||
|
rejectedPointCount: integer(row.rejected_point_count, `${label}.rejected points`),
|
||||||
|
groundCellCount: integer(row.ground_cell_count, `${label}.ground cells`),
|
||||||
|
nongroundCellCount: integer(row.nonground_cell_count, `${label}.nonground cells`),
|
||||||
|
rejectedCellCount: integer(row.rejected_cell_count, `${label}.rejected cells`),
|
||||||
|
unobservedCellCount: integer(row.unobserved_cell_count, `${label}.unobserved cells`),
|
||||||
|
allPointsAccounted: true,
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
parsed.groundPointCount + parsed.nongroundPointCount + parsed.rejectedPointCount
|
||||||
|
!== parsed.pointCount
|
||||||
|
|| parsed.groundCellCount + parsed.nongroundCellCount
|
||||||
|
+ parsed.rejectedCellCount + parsed.unobservedCellCount !== 2244
|
||||||
|
) {
|
||||||
|
throw new M49TgsContractError(`${label}: fail-closed accounting нарушен.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM49TgsFailClosedResult(
|
||||||
|
id: string,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M49TgsFailClosedResult> {
|
||||||
|
if (!RESULT_ID.test(id)) throw new M49TgsContractError("M49 identity недопустима.");
|
||||||
|
const response = await fetcher(
|
||||||
|
`/api/v1/laboratory/m49/tgs-fail-closed/${encodeURIComponent(id)}`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new M49TgsContractError(`M49 TGS недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M49 TGS");
|
||||||
|
exact(payload.schema_version, "missioncore.m49-tgs-fail-closed-view/v1", "M49 schema");
|
||||||
|
exact(payload.result_id, id, "M49 result");
|
||||||
|
exact(payload.ground_truth, false, "M49 ground truth");
|
||||||
|
exact(payload.access, "read-only", "M49 access");
|
||||||
|
const source = objectValue(payload.source, "M49 source");
|
||||||
|
const configuration = objectValue(payload.configuration, "M49 configuration");
|
||||||
|
const execution = objectValue(payload.execution, "M49 execution");
|
||||||
|
const metrics = objectValue(payload.metrics, "M49 metrics");
|
||||||
|
const acceptance = objectValue(payload.acceptance, "M49 acceptance");
|
||||||
|
const decision = objectValue(payload.decision, "M49 decision");
|
||||||
|
if (!Array.isArray(source.anchor_frame_indices) || !Array.isArray(metrics.primary)) {
|
||||||
|
throw new M49TgsContractError("M49 anchors: ожидался массив.");
|
||||||
|
}
|
||||||
|
if (!Array.isArray(payload.limitations)) {
|
||||||
|
throw new M49TgsContractError("M49 limitations: ожидался массив.");
|
||||||
|
}
|
||||||
|
const anchorFrameIndices = source.anchor_frame_indices.map(
|
||||||
|
(item, index) => integer(item, `M49 anchor ${index}`),
|
||||||
|
);
|
||||||
|
const primary = metrics.primary.map((item, index) => anchor(item, `M49 primary ${index}`));
|
||||||
|
if (
|
||||||
|
anchorFrameIndices.length !== 10
|
||||||
|
|| new Set(anchorFrameIndices).size !== 10
|
||||||
|
|| primary.length !== 10
|
||||||
|
|| integer(metrics.anchor_count, "M49 anchor count") !== 10
|
||||||
|
|| integer(metrics.anchor_profile_count, "M49 profile count") !== 20
|
||||||
|
|| integer(metrics.costmap_cell_count, "M49 costmap cells") !== 2244
|
||||||
|
|| primary.some((item, index) => item.anchorFrameIndex !== anchorFrameIndices[index])
|
||||||
|
) {
|
||||||
|
throw new M49TgsContractError("M49 anchor set или costmap contract нарушен.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultId: resultId(payload.result_id),
|
||||||
|
createdAtUtc: text(payload.created_at_utc, "M49 created"),
|
||||||
|
source: {
|
||||||
|
sourceId: exact(source.source_id, "RAVNOVES00", "M49 source id"),
|
||||||
|
sourceSessionId: exact(
|
||||||
|
source.source_session_id,
|
||||||
|
"20260720T065719Z_viewer_live",
|
||||||
|
"M49 source session",
|
||||||
|
),
|
||||||
|
sourcePackSha256: sha256(source.source_pack_sha256, "M49 source pack"),
|
||||||
|
linkedVisualResultId: text(source.linked_visual_result_id, "M49 visual result"),
|
||||||
|
anchorFrameIndices,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
profileId: text(configuration.profile_id, "M49 profile"),
|
||||||
|
configSha256: sha256(configuration.config_sha256, "M49 config"),
|
||||||
|
coordinateFrame: exact(configuration.coordinate_frame, "map-gravity-local", "M49 frame"),
|
||||||
|
primaryProfile: exact(configuration.primary_profile, "causal_rolling_1s", "M49 primary"),
|
||||||
|
cellSizeM: numberValue(configuration.cell_size_m, "M49 cell size"),
|
||||||
|
radiusM: numberValue(configuration.radius_m, "M49 radius"),
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
worker: exact(execution.worker, "Worker 006", "M49 worker"),
|
||||||
|
device: exact(execution.device, "cpu", "M49 device"),
|
||||||
|
gpuUsed: exact(execution.gpu_used, false, "M49 GPU"),
|
||||||
|
wrapperElapsedSeconds: numberValue(execution.wrapper_elapsed_seconds, "M49 wall"),
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
anchorCount: 10,
|
||||||
|
anchorProfileCount: 20,
|
||||||
|
allEligiblePointsAccounted: exact(metrics.all_eligible_points_accounted, true, "M49 accounting"),
|
||||||
|
costmapCellCount: 2244,
|
||||||
|
processWallCurrentP50Ms: numberValue(metrics.process_wall_current_p50_ms, "M49 current p50"),
|
||||||
|
processWallCurrentMaxMs: numberValue(metrics.process_wall_current_max_ms, "M49 current max"),
|
||||||
|
processWallRollingP50Ms: numberValue(metrics.process_wall_rolling_p50_ms, "M49 rolling p50"),
|
||||||
|
processWallRollingMaxMs: numberValue(metrics.process_wall_rolling_max_ms, "M49 rolling max"),
|
||||||
|
processMaxRssKib: integer(metrics.process_max_rss_kib, "M49 RSS"),
|
||||||
|
primary,
|
||||||
|
},
|
||||||
|
acceptance: {
|
||||||
|
representationComplete: exact(acceptance.representation_complete, true, "M49 representation"),
|
||||||
|
allPointsAccounted: exact(acceptance.all_points_accounted, true, "M49 points"),
|
||||||
|
aosAbsent: exact(acceptance.aos_absent, true, "M49 AOS"),
|
||||||
|
gpuAbsent: exact(acceptance.gpu_absent, true, "M49 GPU absent"),
|
||||||
|
visualQualityAccepted: exact(acceptance.visual_quality_accepted, false, "M49 visual quality"),
|
||||||
|
traversabilityAccepted: exact(acceptance.traversability_accepted, false, "M49 traversability"),
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
state: exact(decision.state, "visual-review-required", "M49 decision"),
|
||||||
|
candidateRetained: exact(decision.candidate_retained, true, "M49 retained"),
|
||||||
|
nextAction: text(decision.next_action, "M49 next action"),
|
||||||
|
},
|
||||||
|
limitations: payload.limitations.map((item, index) => text(item, `M49 limitation ${index}`)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM49TgsAnchorSpatial(
|
||||||
|
id: string,
|
||||||
|
anchorFrameIndex: number,
|
||||||
|
profile: M49TgsProfile = "causal_rolling_1s",
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M49TgsAnchorSpatial> {
|
||||||
|
if (!RESULT_ID.test(id) || !Number.isSafeInteger(anchorFrameIndex) || anchorFrameIndex < 0) {
|
||||||
|
throw new M49TgsContractError("M49 anchor identity недопустима.");
|
||||||
|
}
|
||||||
|
const response = await fetcher(
|
||||||
|
`/api/v1/laboratory/m49/tgs-fail-closed/${encodeURIComponent(id)}/anchors/${anchorFrameIndex}/spatial?profile=${encodeURIComponent(profile)}`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new M49TgsContractError(`M49 anchor недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M49 anchor");
|
||||||
|
exact(payload.schema_version, "missioncore.m49-tgs-anchor-spatial/v1", "M49 anchor schema");
|
||||||
|
exact(payload.result_id, id, "M49 anchor result");
|
||||||
|
exact(payload.coordinate_frame, "map-gravity-local", "M49 anchor frame");
|
||||||
|
exact(payload.all_points_accounted, true, "M49 anchor accounting");
|
||||||
|
exact(payload.aos_used, false, "M49 anchor AOS");
|
||||||
|
if (!Array.isArray(payload.points_xyz_m) || !Array.isArray(payload.point_states)) {
|
||||||
|
throw new M49TgsContractError("M49 points: ожидался массив.");
|
||||||
|
}
|
||||||
|
const costmap = objectValue(payload.costmap, "M49 costmap");
|
||||||
|
if (!Array.isArray(costmap.centers_xy_m) || !Array.isArray(costmap.states) || !Array.isArray(costmap.z_bounds_m)) {
|
||||||
|
throw new M49TgsContractError("M49 costmap arrays: нарушен контракт.");
|
||||||
|
}
|
||||||
|
const points = payload.points_xyz_m.map((item, index) => numbers(item, 3, `M49 point ${index}`) as [number, number, number]);
|
||||||
|
const pointStates = payload.point_states.map((item, index) => {
|
||||||
|
const state = integer(item, `M49 point state ${index}`);
|
||||||
|
if (state !== 1 && state !== 2 && state !== 3) throw new M49TgsContractError("M49 point state неизвестен.");
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
if (points.length !== pointStates.length) throw new M49TgsContractError("M49 point accounting нарушен.");
|
||||||
|
const centers = costmap.centers_xy_m.map((item, index) => numbers(item, 2, `M49 cell ${index}`) as [number, number]);
|
||||||
|
const states = costmap.states.map((item, index) => {
|
||||||
|
const state = integer(item, `M49 cell state ${index}`);
|
||||||
|
if (state !== 0 && state !== 1 && state !== 2 && state !== 3) throw new M49TgsContractError("M49 cell state неизвестен.");
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
const zBounds = costmap.z_bounds_m.map((item, index) => {
|
||||||
|
if (!Array.isArray(item) || item.length !== 2) throw new M49TgsContractError(`M49 z ${index}: размерность.`);
|
||||||
|
return item.map((value) => value === null ? null : numberValue(value, `M49 z ${index}`)) as [number | null, number | null];
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
centers.length !== 2244
|
||||||
|
|| centers.length !== states.length
|
||||||
|
|| centers.length !== zBounds.length
|
||||||
|
|| integer(payload.anchor_frame_index, "M49 anchor frame") !== anchorFrameIndex
|
||||||
|
|| integer(payload.source_sequence, "M49 source sequence") !== anchorFrameIndex
|
||||||
|
) {
|
||||||
|
throw new M49TgsContractError("M49 costmap accounting нарушен.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultId: id,
|
||||||
|
anchorFrameIndex,
|
||||||
|
sourceSequence: anchorFrameIndex,
|
||||||
|
profile: exact(payload.profile, profile, "M49 profile"),
|
||||||
|
coordinateFrame: "map-gravity-local",
|
||||||
|
pointsXyzM: points,
|
||||||
|
pointStates,
|
||||||
|
costmap: {
|
||||||
|
cellSizeM: numberValue(costmap.cell_size_m, "M49 cell size"),
|
||||||
|
radiusM: numberValue(costmap.radius_m, "M49 radius"),
|
||||||
|
centersXyM: centers,
|
||||||
|
states,
|
||||||
|
zBoundsM: zBounds,
|
||||||
|
},
|
||||||
|
allPointsAccounted: true,
|
||||||
|
aosUsed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
import type { LaboratoryFetch } from "./advancedResults";
|
||||||
|
|
||||||
|
const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
|
||||||
|
const SEMANTIC_RESULT_ID = /^e47-semantic-slam-[a-f0-9]{64}$/;
|
||||||
|
|
||||||
|
export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
export interface M49TgsFullShadowResult {
|
||||||
|
resultId: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
source: {
|
||||||
|
sourcePackSha256: string;
|
||||||
|
linkedVisualResultId: string;
|
||||||
|
linkedSemanticResultId: string;
|
||||||
|
};
|
||||||
|
configuration: {
|
||||||
|
configSha256: string;
|
||||||
|
cellSizeM: number;
|
||||||
|
radiusM: number;
|
||||||
|
historySeconds: number;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
wrapperElapsedSeconds: number;
|
||||||
|
};
|
||||||
|
timeline: {
|
||||||
|
frameCount: 4489;
|
||||||
|
availableLidarFrameCount: 3928;
|
||||||
|
missingLidarFrameCount: 561;
|
||||||
|
durationSeconds: number;
|
||||||
|
effectiveFps: number;
|
||||||
|
};
|
||||||
|
pointAccounting: {
|
||||||
|
eligible: number;
|
||||||
|
ground: number;
|
||||||
|
nonground: number;
|
||||||
|
rejected: number;
|
||||||
|
unaccounted: 0;
|
||||||
|
};
|
||||||
|
performance: {
|
||||||
|
candidateTgsMs: { p50: number; p95: number; p99: number; max: number };
|
||||||
|
completionAgeMs: { p50: number; p95: number; p99: number; max: number };
|
||||||
|
capacityDropCount: number;
|
||||||
|
};
|
||||||
|
acceptance: {
|
||||||
|
representationComplete: true;
|
||||||
|
visualQualityAccepted: false;
|
||||||
|
integratedGraphPerformanceAccepted: false;
|
||||||
|
allFramesAccounted: boolean;
|
||||||
|
allEligiblePointsAccounted: boolean;
|
||||||
|
};
|
||||||
|
decision: {
|
||||||
|
state: string;
|
||||||
|
candidateRetained: boolean;
|
||||||
|
nextAction: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M49TgsFullShadowSpatial {
|
||||||
|
resultId: string;
|
||||||
|
sourceSequence: number;
|
||||||
|
sourceFrameIndex: number;
|
||||||
|
sessionSeconds: number;
|
||||||
|
sampleAvailable: boolean;
|
||||||
|
costmap: {
|
||||||
|
cellSizeM: number;
|
||||||
|
radiusM: number;
|
||||||
|
centersXyM: readonly (readonly [number, number])[];
|
||||||
|
states: readonly M49TgsFullShadowStateCode[];
|
||||||
|
zBoundsM: readonly (readonly [number | null, number | null])[];
|
||||||
|
};
|
||||||
|
metrics: {
|
||||||
|
eligiblePointCount: number;
|
||||||
|
groundPointCount: number;
|
||||||
|
nongroundPointCount: number;
|
||||||
|
rejectedPointCount: number;
|
||||||
|
occupiedCellCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M49TgsFullShadowSpatialChunk {
|
||||||
|
resultId: string;
|
||||||
|
start: number;
|
||||||
|
count: number;
|
||||||
|
frames: readonly M49TgsFullShadowSpatial[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class M49TgsFullShadowContractError extends Error {}
|
||||||
|
|
||||||
|
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: ожидался объект.`);
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(value: unknown, label: string): string {
|
||||||
|
if (typeof value !== "string" || !value.trim()) {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: ожидалась строка.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown, label: string): number {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: ожидалось число.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, label: string): number {
|
||||||
|
const parsed = numberValue(value, label);
|
||||||
|
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: ожидалось целое число.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanValue(value: unknown, label: string): boolean {
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: ожидался boolean.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exact<T extends string | boolean | number>(value: unknown, expected: T, label: string): T {
|
||||||
|
if (value !== expected) throw new M49TgsFullShadowContractError(`${label}: нарушен контракт.`);
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timing(value: unknown, label: string) {
|
||||||
|
const row = objectValue(value, label);
|
||||||
|
return {
|
||||||
|
p50: numberValue(row.p50, `${label}.p50`),
|
||||||
|
p95: numberValue(row.p95, `${label}.p95`),
|
||||||
|
p99: numberValue(row.p99, `${label}.p99`),
|
||||||
|
max: numberValue(row.max, `${label}.max`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM49TgsFullShadowResult(
|
||||||
|
id: string,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M49TgsFullShadowResult> {
|
||||||
|
if (!RESULT_ID.test(id)) throw new M49TgsFullShadowContractError("M49 full-shadow identity недопустима.");
|
||||||
|
const response = await fetcher(
|
||||||
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full shadow недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M49 full shadow");
|
||||||
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-view/v1", "M49 full schema");
|
||||||
|
exact(payload.result_id, id, "M49 full result");
|
||||||
|
exact(payload.ground_truth, false, "M49 full ground truth");
|
||||||
|
exact(payload.access, "read-only", "M49 full access");
|
||||||
|
const source = objectValue(payload.source, "M49 full source");
|
||||||
|
const configuration = objectValue(payload.configuration, "M49 full configuration");
|
||||||
|
const execution = objectValue(payload.execution, "M49 full execution");
|
||||||
|
const timeline = objectValue(payload.timeline, "M49 full timeline");
|
||||||
|
const accounting = objectValue(payload.point_accounting, "M49 full accounting");
|
||||||
|
const performance = objectValue(payload.performance, "M49 full performance");
|
||||||
|
const acceptance = objectValue(payload.acceptance, "M49 full acceptance");
|
||||||
|
const decision = objectValue(payload.decision, "M49 full decision");
|
||||||
|
const linkedSemanticResultId = text(
|
||||||
|
source.linked_semantic_result_id,
|
||||||
|
"M49 full semantic result",
|
||||||
|
);
|
||||||
|
if (!SEMANTIC_RESULT_ID.test(linkedSemanticResultId)) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full semantic result: нарушена идентичность.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultId: text(payload.result_id, "M49 full result"),
|
||||||
|
createdAtUtc: text(payload.created_at_utc, "M49 full created"),
|
||||||
|
source: {
|
||||||
|
sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"),
|
||||||
|
linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"),
|
||||||
|
linkedSemanticResultId,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
configSha256: text(configuration.config_sha256, "M49 full config"),
|
||||||
|
cellSizeM: numberValue(configuration.cell_size_m, "M49 full cell size"),
|
||||||
|
radiusM: numberValue(configuration.radius_m, "M49 full radius"),
|
||||||
|
historySeconds: numberValue(configuration.history_seconds, "M49 full history"),
|
||||||
|
},
|
||||||
|
execution: { wrapperElapsedSeconds: numberValue(execution.wrapper_elapsed_seconds, "M49 full wall") },
|
||||||
|
timeline: {
|
||||||
|
frameCount: exact(integer(timeline.frame_count, "M49 full frames"), 4489, "M49 full frames"),
|
||||||
|
availableLidarFrameCount: exact(integer(timeline.available_lidar_frame_count, "M49 full available"), 3928, "M49 full available"),
|
||||||
|
missingLidarFrameCount: exact(integer(timeline.missing_lidar_frame_count, "M49 full missing"), 561, "M49 full missing"),
|
||||||
|
durationSeconds: numberValue(timeline.duration_seconds, "M49 full duration"),
|
||||||
|
effectiveFps: numberValue(timeline.effective_fps, "M49 full fps"),
|
||||||
|
},
|
||||||
|
pointAccounting: {
|
||||||
|
eligible: integer(accounting.eligible, "M49 full eligible"),
|
||||||
|
ground: integer(accounting.ground, "M49 full ground"),
|
||||||
|
nonground: integer(accounting.nonground, "M49 full nonground"),
|
||||||
|
rejected: integer(accounting.rejected, "M49 full rejected"),
|
||||||
|
unaccounted: exact(integer(accounting.unaccounted, "M49 full unaccounted"), 0, "M49 full unaccounted"),
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
candidateTgsMs: timing(performance.candidate_tgs_ms, "M49 full TGS"),
|
||||||
|
completionAgeMs: timing(performance.completion_age_ms, "M49 full completion"),
|
||||||
|
capacityDropCount: integer(performance.capacity_drop_count, "M49 full drops"),
|
||||||
|
},
|
||||||
|
acceptance: {
|
||||||
|
representationComplete: exact(acceptance.representation_complete, true, "M49 full representation"),
|
||||||
|
visualQualityAccepted: exact(acceptance.visual_quality_accepted, false, "M49 full visual"),
|
||||||
|
integratedGraphPerformanceAccepted: exact(acceptance.integrated_graph_performance_accepted, false, "M49 full integrated"),
|
||||||
|
allFramesAccounted: booleanValue(acceptance.all_frames_accounted, "M49 full frame accounting"),
|
||||||
|
allEligiblePointsAccounted: booleanValue(acceptance.all_eligible_points_accounted, "M49 full point accounting"),
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
state: text(decision.state, "M49 full decision"),
|
||||||
|
candidateRetained: booleanValue(decision.candidate_retained, "M49 full retained"),
|
||||||
|
nextAction: text(decision.next_action, "M49 full next action"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pair(value: unknown, label: string): readonly [number, number] {
|
||||||
|
if (!Array.isArray(value) || value.length !== 2) {
|
||||||
|
throw new M49TgsFullShadowContractError(`${label}: неверная размерность.`);
|
||||||
|
}
|
||||||
|
return [numberValue(value[0], label), numberValue(value[1], label)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpatial(
|
||||||
|
id: string,
|
||||||
|
sourceSequence: number,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
sharedCostmap?: Record<string, unknown>,
|
||||||
|
): M49TgsFullShadowSpatial {
|
||||||
|
exact(payload.source_sequence, sourceSequence, "M49 full spatial sequence");
|
||||||
|
const costmap = sharedCostmap ?? objectValue(payload.costmap, "M49 full costmap");
|
||||||
|
const metrics = objectValue(payload.metrics, "M49 full frame metrics");
|
||||||
|
const centers = costmap.centers_xy_m;
|
||||||
|
const statesRaw = sharedCostmap ? payload.states : costmap.states;
|
||||||
|
const zBoundsRaw = sharedCostmap ? payload.z_bounds_m : costmap.z_bounds_m;
|
||||||
|
if (!Array.isArray(centers) || !Array.isArray(statesRaw) || !Array.isArray(zBoundsRaw)
|
||||||
|
|| centers.length !== 2244 || statesRaw.length !== 2244 || zBoundsRaw.length !== 2244) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full costmap: неверная размерность.");
|
||||||
|
}
|
||||||
|
const states = statesRaw.map((value, index) => {
|
||||||
|
const parsed = integer(value, `M49 full state ${index}`);
|
||||||
|
if (parsed > 3) throw new M49TgsFullShadowContractError("M49 full state недопустим.");
|
||||||
|
return parsed as M49TgsFullShadowStateCode;
|
||||||
|
});
|
||||||
|
const zBounds = zBoundsRaw.map((value, index) => {
|
||||||
|
if (!Array.isArray(value) || value.length !== 2) {
|
||||||
|
throw new M49TgsFullShadowContractError(`M49 full z ${index}: неверная размерность.`);
|
||||||
|
}
|
||||||
|
return value.map((item) => item === null
|
||||||
|
? null
|
||||||
|
: numberValue(item, `M49 full z ${index}`)) as [number | null, number | null];
|
||||||
|
});
|
||||||
|
const sampleAvailable = booleanValue(payload.sample_available, "M49 full sample available");
|
||||||
|
if (!sampleAvailable && states.some((state) => state !== 0)) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 missing LiDAR frame не остался UNOBSERVED.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
resultId: id,
|
||||||
|
sourceSequence,
|
||||||
|
sourceFrameIndex: integer(payload.source_frame_index, "M49 full source frame"),
|
||||||
|
sessionSeconds: numberValue(payload.session_seconds, "M49 full session seconds"),
|
||||||
|
sampleAvailable,
|
||||||
|
costmap: {
|
||||||
|
cellSizeM: numberValue(costmap.cell_size_m, "M49 full cell size"),
|
||||||
|
radiusM: numberValue(costmap.radius_m, "M49 full radius"),
|
||||||
|
centersXyM: centers.map((value, index) => pair(value, `M49 full center ${index}`)),
|
||||||
|
states,
|
||||||
|
zBoundsM: zBounds,
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
eligiblePointCount: integer(metrics.eligible_point_count, "M49 full eligible"),
|
||||||
|
groundPointCount: integer(metrics.ground_point_count, "M49 full ground"),
|
||||||
|
nongroundPointCount: integer(metrics.nonground_point_count, "M49 full nonground"),
|
||||||
|
rejectedPointCount: integer(metrics.rejected_point_count, "M49 full rejected"),
|
||||||
|
occupiedCellCount: integer(metrics.occupied_cell_count, "M49 full occupied cells"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM49TgsFullShadowSpatial(
|
||||||
|
id: string,
|
||||||
|
sourceSequence: number,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M49TgsFullShadowSpatial> {
|
||||||
|
if (!RESULT_ID.test(id) || !Number.isSafeInteger(sourceSequence) || sourceSequence < 0 || sourceSequence >= 4489) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full-shadow frame identity недопустима.");
|
||||||
|
}
|
||||||
|
const response = await fetcher(
|
||||||
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}/frames/${sourceSequence}/spatial`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full-shadow frame недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M49 full spatial");
|
||||||
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-spatial/v1", "M49 full spatial schema");
|
||||||
|
exact(payload.result_id, id, "M49 full spatial result");
|
||||||
|
return parseSpatial(id, sourceSequence, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM49TgsFullShadowSpatialChunk(
|
||||||
|
id: string,
|
||||||
|
start: number,
|
||||||
|
count = 24,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M49TgsFullShadowSpatialChunk> {
|
||||||
|
if (!RESULT_ID.test(id) || !Number.isSafeInteger(start) || start < 0 || start >= 4489
|
||||||
|
|| !Number.isSafeInteger(count) || count < 1 || count > 24) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full-shadow chunk identity недопустима.");
|
||||||
|
}
|
||||||
|
const response = await fetcher(
|
||||||
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}/spatial/chunk?start=${start}&count=${count}`,
|
||||||
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full-shadow chunk недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M49 full spatial chunk");
|
||||||
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-spatial-chunk/v1", "M49 full chunk schema");
|
||||||
|
exact(payload.result_id, id, "M49 full chunk result");
|
||||||
|
exact(payload.start, start, "M49 full chunk start");
|
||||||
|
const returnedCount = integer(payload.count, "M49 full chunk count");
|
||||||
|
if (!Array.isArray(payload.frames) || payload.frames.length !== returnedCount
|
||||||
|
|| returnedCount !== Math.min(count, 4489 - start)) {
|
||||||
|
throw new M49TgsFullShadowContractError("M49 full chunk: неверная размерность.");
|
||||||
|
}
|
||||||
|
const costmap = objectValue(payload.costmap, "M49 full chunk costmap");
|
||||||
|
return {
|
||||||
|
resultId: id,
|
||||||
|
start,
|
||||||
|
count: returnedCount,
|
||||||
|
frames: payload.frames.map((value, index) => parseSpatial(
|
||||||
|
id,
|
||||||
|
start + index,
|
||||||
|
objectValue(value, `M49 full chunk frame ${index}`),
|
||||||
|
costmap,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -91,6 +91,11 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]
|
||||||
|
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="spatial"] {
|
||||||
|
right: 3.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.m4-replay-threat-visual__pane-toolbar > *,
|
.m4-replay-threat-visual__pane-toolbar > *,
|
||||||
.m4-replay-threat-visual__spatial-toolbar-end > * {
|
.m4-replay-threat-visual__spatial-toolbar-end > * {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -350,3 +355,20 @@
|
|||||||
background: rgb(var(--nodedc-accent-rgb));
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.laboratory-metric-evidence-scene__legend span[data-decision="ground-support"]::before {
|
||||||
|
background: rgb(var(--nodedc-success-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-metric-evidence-scene__legend span[data-decision="nonground-occupied"]::before {
|
||||||
|
background: rgb(var(--nodedc-danger-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-metric-evidence-scene__legend span[data-decision="unknown-rejected"]::before {
|
||||||
|
background: rgb(var(--nodedc-warning-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.laboratory-metric-evidence-scene__legend span[data-decision="unobserved"]::before {
|
||||||
|
background: var(--nodedc-text-muted);
|
||||||
|
opacity: 0.42;
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQ
|
|||||||
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
|
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
|
||||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||||
|
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||||
|
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
||||||
|
|
||||||
export { isAdvancedLaboratoryWorkId };
|
export { isAdvancedLaboratoryWorkId };
|
||||||
export type { AdvancedLaboratoryWorkId };
|
export type { AdvancedLaboratoryWorkId };
|
||||||
@@ -108,6 +110,12 @@ export function AdvancedLaboratoryResult({
|
|||||||
if (workId === "m48t-risk-quality-temporal" && results.m48t) {
|
if (workId === "m48t-risk-quality-temporal" && results.m48t) {
|
||||||
return <M48TRiskQualityResultView rigLabel={rigLabel} result={results.m48t} />;
|
return <M48TRiskQualityResultView rigLabel={rigLabel} result={results.m48t} />;
|
||||||
}
|
}
|
||||||
|
if (workId === "m49-tgs-fail-closed-evidence" && results.m49Tgs) {
|
||||||
|
return <M49TgsFailClosedResultView rigLabel={rigLabel} result={results.m49Tgs} />;
|
||||||
|
}
|
||||||
|
if (workId === "m49-tgs-full-shadow" && results.m49TgsFull) {
|
||||||
|
return <M49TgsFullShadowResultView rigLabel={rigLabel} result={results.m49TgsFull} />;
|
||||||
|
}
|
||||||
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
||||||
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RecordedEvidenceSemanticClass,
|
||||||
|
RecordedEvidenceSemanticPaletteEntry,
|
||||||
|
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||||
|
import {
|
||||||
|
fetchM49TgsAnchorSpatial,
|
||||||
|
type M49TgsAnchorSpatial,
|
||||||
|
type M49TgsFailClosedResult,
|
||||||
|
type M49TgsStateCode,
|
||||||
|
} from "../../core/laboratory/m49TgsFailClosed";
|
||||||
|
import {
|
||||||
|
M4ReplayThreatVisual,
|
||||||
|
type M4ReplayClassifiedSpatialFrame,
|
||||||
|
type M4ReplayThreatReviewAnchor,
|
||||||
|
} from "./M4ReplayThreatVisual";
|
||||||
|
|
||||||
|
const CLASSES: readonly RecordedEvidenceSemanticClass[] = [
|
||||||
|
{ id: 1, label: "Ground support" },
|
||||||
|
{ id: 2, label: "Non-ground occupied" },
|
||||||
|
{ id: 3, label: "Unknown / rejected" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
|
||||||
|
{ classId: 1, color: { kind: "token", token: "--nodedc-success-rgb" } },
|
||||||
|
{ classId: 2, color: { kind: "token", token: "--nodedc-danger-rgb" } },
|
||||||
|
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
|
||||||
|
];
|
||||||
|
|
||||||
|
function cellState(code: M49TgsStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
|
||||||
|
if (code === 1) return "ground-support";
|
||||||
|
if (code === 2) return "nonground-occupied";
|
||||||
|
if (code === 3) return "unknown-rejected";
|
||||||
|
return "unobserved";
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(error: unknown): string {
|
||||||
|
return error instanceof Error && error.message.trim()
|
||||||
|
? error.message
|
||||||
|
: "M49 TGS spatial evidence недоступно.";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFailClosedEvidence({
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
result: M49TgsFailClosedResult;
|
||||||
|
}) {
|
||||||
|
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||||
|
const [spatial, setSpatial] = useState<M49TgsAnchorSpatial | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const spatialCacheRef = useRef(new Map<string, M49TgsAnchorSpatial>());
|
||||||
|
const anchorSequences = useMemo(
|
||||||
|
() => new Set(result.metrics.primary.map((item) => item.anchorFrameIndex)),
|
||||||
|
[result.metrics.primary],
|
||||||
|
);
|
||||||
|
const expectedAtSequence = activeSequence !== null && anchorSequences.has(activeSequence);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSequence === null || !anchorSequences.has(activeSequence)) {
|
||||||
|
setSpatial(null);
|
||||||
|
setLoading(false);
|
||||||
|
setError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cacheKey = `${result.resultId}:${activeSequence}`;
|
||||||
|
const cached = spatialCacheRef.current.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
setSpatial(cached);
|
||||||
|
setLoading(false);
|
||||||
|
setError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const controller = new AbortController();
|
||||||
|
setSpatial(null);
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
void fetchM49TgsAnchorSpatial(
|
||||||
|
result.resultId,
|
||||||
|
activeSequence,
|
||||||
|
"causal_rolling_1s",
|
||||||
|
{ signal: controller.signal },
|
||||||
|
)
|
||||||
|
.then((next) => {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
spatialCacheRef.current.set(cacheKey, next);
|
||||||
|
setSpatial(next);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) setError(message(caught));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!controller.signal.aborted) setLoading(false);
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [activeSequence, anchorSequences, result.resultId]);
|
||||||
|
|
||||||
|
const reviewAnchors = useMemo<readonly M4ReplayThreatReviewAnchor[]>(
|
||||||
|
() => result.metrics.primary.map((item) => ({
|
||||||
|
id: `m49-tgs-${item.anchorFrameIndex}`,
|
||||||
|
sourceSequence: item.anchorFrameIndex,
|
||||||
|
extentXyxyNormalized: [0, 0, 0, 0],
|
||||||
|
matchedAtThreshold: false,
|
||||||
|
statusLabel: "визуальная проверка",
|
||||||
|
})),
|
||||||
|
[result.metrics.primary],
|
||||||
|
);
|
||||||
|
|
||||||
|
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||||
|
if (!spatial) return null;
|
||||||
|
return {
|
||||||
|
sourceSequence: spatial.sourceSequence,
|
||||||
|
pointsMapGravityLocalXyzM: spatial.pointsXyzM,
|
||||||
|
pointClassIds: spatial.pointStates,
|
||||||
|
cellsMapGravityLocal: spatial.costmap.centersXyM.map((center, index) => ({
|
||||||
|
centerXyM: center,
|
||||||
|
zBoundsM: spatial.costmap.zBoundsM[index]!,
|
||||||
|
state: cellState(spatial.costmap.states[index]!),
|
||||||
|
})),
|
||||||
|
cellSizeM: spatial.costmap.cellSizeM,
|
||||||
|
classes: CLASSES,
|
||||||
|
palette: PALETTE,
|
||||||
|
};
|
||||||
|
}, [spatial]);
|
||||||
|
|
||||||
|
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||||
|
setActiveSequence(sequence);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<M4ReplayThreatVisual
|
||||||
|
resultId={result.source.linkedVisualResultId}
|
||||||
|
reviewAnchors={reviewAnchors}
|
||||||
|
showReviewAnchorBoxes={false}
|
||||||
|
reviewLabel="10 gravity-aligned TGS anchors"
|
||||||
|
evidenceLabel="M49 · TGS fail-closed"
|
||||||
|
initialSpatialMode="3d"
|
||||||
|
onActiveSequenceChange={handleSequenceChange}
|
||||||
|
classifiedSpatialLayer={{
|
||||||
|
label: "TGS fail-closed · causal rolling 1 s",
|
||||||
|
pointLayerLabel: "TGS POINTS",
|
||||||
|
cellLayerLabel: "COSTMAP",
|
||||||
|
expectedAtSequence,
|
||||||
|
frame: classifiedFrame,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
LaboratoryEvidence,
|
||||||
|
LaboratoryResultSummary,
|
||||||
|
LaboratorySummary,
|
||||||
|
LaboratoryWorkTemplate,
|
||||||
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
|
import type { M49TgsFailClosedResult } from "../../core/laboratory/m49TgsFailClosed";
|
||||||
|
import { M49TgsFailClosedEvidence } from "./M49TgsFailClosedEvidence";
|
||||||
|
|
||||||
|
function number(value: number, digits = 1): string {
|
||||||
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFailClosedResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M49TgsFailClosedResult;
|
||||||
|
}) {
|
||||||
|
const worst = [...result.metrics.primary].sort(
|
||||||
|
(left, right) => (
|
||||||
|
right.nongroundPointCount / Math.max(right.pointCount, 1)
|
||||||
|
- left.nongroundPointCount / Math.max(left.pointCount, 1)
|
||||||
|
),
|
||||||
|
)[0]!;
|
||||||
|
const status = "Representation complete; визуальное качество ещё не принято";
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.9T4 · TRAVEL TGS fail-closed evidence"
|
||||||
|
description="TRAVEL GroundSeg запущен без AOS на десяти immutable RAVNOVES00 anchors. Вход сохранён в gravity-aligned map frame; каждый eligible point получил состояние, а каждая costmap-ячейка остаётся ground, occupied, rejected или unobserved."
|
||||||
|
status={status}
|
||||||
|
statusTone="warning"
|
||||||
|
facts={[
|
||||||
|
{ label: "Конфигурация", value: `${rigLabel} RIGHT · Camera + gravity-aligned LiDAR · 10 anchors` },
|
||||||
|
{ label: "Метод", value: "TRAVEL TGS only · AOS OFF · causal rolling 1 s" },
|
||||||
|
{ label: "Evidence", value: `${result.metrics.anchorCount} anchors · ${result.metrics.costmapCellCount.toLocaleString("ru-RU")} cells/anchor · all points accounted` },
|
||||||
|
{ label: "Нагрузка", value: `Worker 006 CPU-only · GPU 0 · wrapper ${number(result.execution.wrapperElapsedSeconds, 2)} с` },
|
||||||
|
{ label: "Authority", value: "REPLAY-SIMULATED · visual/traversability/navigation/actuation OFF" },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Отделяет ли готовый TRAVEL TGS опорную поверхность от неизвестной занятой геометрии достаточно чисто, чтобы заменить самодельный static-obstacle threshold pipeline?",
|
||||||
|
approach: "На десяти сложных кадрах проверяется полный gravity-aligned point set и fail-closed costmap. Зелёное — опора, красное — non-ground occupied, жёлтое — rejected/unknown, тёмное — unobserved; камера остаётся синхронным первичным контекстом.",
|
||||||
|
principalResult: `Контракт представления закрыт: ${result.metrics.anchorProfileCount}/20 профилей, ни одной потерянной eligible point, AOS и GPU отсутствуют. Process wall rolling p50/max: ${number(result.metrics.processWallRollingP50Ms, 0)}/${number(result.metrics.processWallRollingMaxMs, 0)} мс.`,
|
||||||
|
limitation: `Качество не принято: особенно проверить кадр ${worst.anchorFrameIndex + 1}, где ${number(worst.nongroundPointCount / Math.max(worst.pointCount, 1) * 100)}% rolling points помечены non-ground. Это может быть реальная боковая геометрия либо ложная блокировка поверхности.`,
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "deterministic",
|
||||||
|
pipelineId: "travel-tgs-gravity-aligned-fail-closed/v1",
|
||||||
|
components: [
|
||||||
|
{ kind: "source", name: "RAVNOVES00", version: "10 immutable anchors", role: "camera + registered map increments", identitySha256: result.source.sourcePackSha256 },
|
||||||
|
{ kind: "algorithm", name: "TRAVEL GroundSeg", version: "95dc2fbd66a343efd9060c45a5711b6307a950a4", role: "ground/nonground separation; AOS excluded", identitySha256: result.configuration.configSha256 },
|
||||||
|
{ kind: "algorithm", name: "fail-closed complement adapter", version: "v1", role: "explicit rejected points and unobserved cells", identitySha256: result.resultId.split("-").at(-1) ?? null },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.9T4 VISUAL EVIDENCE · CAMERA + GRAVITY-ALIGNED TGS"
|
||||||
|
title="10 anchors: полный TGS point set и четырёхсостояний costmap на том же recorded timeline"
|
||||||
|
kind="recorded-replay"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M49TgsFailClosedEvidence result={result} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Что уже доказано и что проверяем глазами"
|
||||||
|
status={status}
|
||||||
|
statusTone="warning"
|
||||||
|
metrics={[
|
||||||
|
{ label: "Point accounting", value: "100%", hint: "ground + non-ground + rejected = exact eligible input" },
|
||||||
|
{ label: "Anchors", value: `${result.metrics.anchorCount}/10`, hint: "current + causal rolling 1 s" },
|
||||||
|
{ label: "Costmap", value: `${result.metrics.costmapCellCount.toLocaleString("ru-RU")} cells`, hint: `${number(result.configuration.cellSizeM, 2)} м · radius ${number(result.configuration.radiusM, 0)} м` },
|
||||||
|
{ label: "Process wall rolling", value: `${number(result.metrics.processWallRollingP50Ms, 0)} / ${number(result.metrics.processWallRollingMaxMs, 0)} мс`, hint: "p50 / max · CPU process envelope, не realtime integration" },
|
||||||
|
{ label: "GPU / AOS", value: "0 / OFF", hint: "Worker 006; Frigate budget не затронут" },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: "Готовый TGS можно встроить fail-closed: исходные точки не теряются, unknown не становится free, AOS не нужен, а вычисление укладывается в лёгкий CPU-контур на этих anchors.",
|
||||||
|
notProved: "Не доказано, что красный non-ground слой не режет дорогу, траву или допустимые просветы. Нет независимой terrain truth, полного replay, realtime graph integration и модели корпуса.",
|
||||||
|
decision: "Открыть десять anchors по очереди. Если красное остаётся на реальных препятствиях и не перекрывает видимую опорную поверхность, TGS идёт в полный shadow; иначе кандидат отклоняется без ручной подгонки порогов под эти кадры.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RecordedEvidenceSemanticClass,
|
||||||
|
RecordedEvidenceSemanticPaletteEntry,
|
||||||
|
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||||
|
import {
|
||||||
|
fetchE47SemanticSlamResult,
|
||||||
|
type E47SemanticSlamResult,
|
||||||
|
} from "../../core/laboratory/e47SemanticSlam";
|
||||||
|
import {
|
||||||
|
fetchM49TgsFullShadowSpatialChunk,
|
||||||
|
type M49TgsFullShadowResult,
|
||||||
|
type M49TgsFullShadowSpatial,
|
||||||
|
type M49TgsFullShadowSpatialChunk,
|
||||||
|
type M49TgsFullShadowStateCode,
|
||||||
|
} from "../../core/laboratory/m49TgsFullShadow";
|
||||||
|
import {
|
||||||
|
M4ReplayThreatVisual,
|
||||||
|
type M4ReplayClassifiedSpatialFrame,
|
||||||
|
} from "./M4ReplayThreatVisual";
|
||||||
|
|
||||||
|
const CLASSES: readonly RecordedEvidenceSemanticClass[] = [
|
||||||
|
{ id: 1, label: "Ground support" },
|
||||||
|
{ id: 2, label: "Non-ground occupied" },
|
||||||
|
{ id: 3, label: "Unknown / rejected" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
|
||||||
|
{ classId: 1, color: { kind: "token", token: "--nodedc-success-rgb" } },
|
||||||
|
{ classId: 2, color: { kind: "token", token: "--nodedc-danger-rgb" } },
|
||||||
|
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHUNK_FRAMES = 24;
|
||||||
|
const RETAINED_CHUNKS = 4;
|
||||||
|
|
||||||
|
function cellState(code: M49TgsFullShadowStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
|
||||||
|
if (code === 1) return "ground-support";
|
||||||
|
if (code === 2) return "nonground-occupied";
|
||||||
|
if (code === 3) return "unknown-rejected";
|
||||||
|
return "unobserved";
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(error: unknown): string {
|
||||||
|
return error instanceof Error && error.message.trim()
|
||||||
|
? error.message
|
||||||
|
: "Полный TGS spatial frame недоступен.";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
|
||||||
|
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||||
|
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
|
||||||
|
const [semanticError, setSemanticError] = useState<string | null>(null);
|
||||||
|
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
||||||
|
() => new Map(),
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const inFlightRef = useRef(new Map<number, AbortController>());
|
||||||
|
const activeChunkStart = activeSequence === null
|
||||||
|
? null
|
||||||
|
: Math.floor(activeSequence / CHUNK_FRAMES) * CHUNK_FRAMES;
|
||||||
|
const activeChunkStartRef = useRef(activeChunkStart);
|
||||||
|
activeChunkStartRef.current = activeChunkStart;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setSemantic(null);
|
||||||
|
setSemanticError(null);
|
||||||
|
void fetchE47SemanticSlamResult({
|
||||||
|
resultId: result.source.linkedSemanticResultId,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
.then((next) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
if (!next || next.baseM4ResultId !== result.source.linkedVisualResultId) {
|
||||||
|
throw new Error("Semantic archive не совпал с исходным M4 timeline.");
|
||||||
|
}
|
||||||
|
setSemantic(next);
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) setSemanticError(message(caught));
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||||
|
inFlightRef.current.clear();
|
||||||
|
setChunks(new Map());
|
||||||
|
setError(null);
|
||||||
|
return () => {
|
||||||
|
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||||
|
inFlightRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [result.resultId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeChunkStart === null) return;
|
||||||
|
const desiredStarts = [activeChunkStart, activeChunkStart + CHUNK_FRAMES]
|
||||||
|
.filter((start) => start < result.timeline.frameCount);
|
||||||
|
const desired = new Set(desiredStarts);
|
||||||
|
for (const [start, controller] of inFlightRef.current) {
|
||||||
|
if (desired.has(start)) continue;
|
||||||
|
controller.abort();
|
||||||
|
inFlightRef.current.delete(start);
|
||||||
|
}
|
||||||
|
for (const start of desiredStarts) {
|
||||||
|
if (chunks.has(start) || inFlightRef.current.has(start)) continue;
|
||||||
|
const controller = new AbortController();
|
||||||
|
inFlightRef.current.set(start, controller);
|
||||||
|
void fetchM49TgsFullShadowSpatialChunk(result.resultId, start, CHUNK_FRAMES, {
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
.then((chunk) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
setChunks((current) => {
|
||||||
|
const next = new Map(current);
|
||||||
|
next.set(start, chunk);
|
||||||
|
const center = activeChunkStartRef.current ?? start;
|
||||||
|
const retained = [...next.keys()]
|
||||||
|
.sort((left, right) => Math.abs(left - center) - Math.abs(right - center))
|
||||||
|
.slice(0, RETAINED_CHUNKS);
|
||||||
|
return new Map(retained.map((key) => [key, next.get(key)!]));
|
||||||
|
});
|
||||||
|
if (start === activeChunkStartRef.current) setError(null);
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted && start === activeChunkStartRef.current) {
|
||||||
|
setError(message(caught));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (inFlightRef.current.get(start) === controller) inFlightRef.current.delete(start);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [activeChunkStart, chunks, result.resultId, result.timeline.frameCount]);
|
||||||
|
|
||||||
|
const spatial = useMemo<M49TgsFullShadowSpatial | null>(() => {
|
||||||
|
if (activeSequence === null || activeChunkStart === null) return null;
|
||||||
|
return chunks.get(activeChunkStart)?.frames.find(
|
||||||
|
(frame) => frame.sourceSequence === activeSequence,
|
||||||
|
) ?? null;
|
||||||
|
}, [activeChunkStart, activeSequence, chunks]);
|
||||||
|
const loading = activeSequence !== null && !spatial && !error;
|
||||||
|
|
||||||
|
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||||
|
if (!spatial) return null;
|
||||||
|
return {
|
||||||
|
sourceSequence: spatial.sourceSequence,
|
||||||
|
sampleAvailable: spatial.sampleAvailable,
|
||||||
|
sourcePointCount: spatial.metrics.eligiblePointCount,
|
||||||
|
pointsMapGravityLocalXyzM: [],
|
||||||
|
pointClassIds: [],
|
||||||
|
cellsMapGravityLocal: spatial.costmap.centersXyM.map((center, index) => ({
|
||||||
|
centerXyM: center,
|
||||||
|
zBoundsM: spatial.costmap.zBoundsM[index]!,
|
||||||
|
state: cellState(spatial.costmap.states[index]!),
|
||||||
|
})),
|
||||||
|
cellSizeM: spatial.costmap.cellSizeM,
|
||||||
|
classes: CLASSES,
|
||||||
|
palette: PALETTE,
|
||||||
|
};
|
||||||
|
}, [spatial]);
|
||||||
|
|
||||||
|
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||||
|
setActiveSequence(sequence);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<M4ReplayThreatVisual
|
||||||
|
resultId={result.source.linkedVisualResultId}
|
||||||
|
semantic={semantic ? {
|
||||||
|
resultId: semantic.resultId,
|
||||||
|
taxonomy: semantic.taxonomy,
|
||||||
|
} : undefined}
|
||||||
|
showReviewAnchorBoxes={false}
|
||||||
|
reviewLabel="4 489 source-paced TGS frames"
|
||||||
|
evidenceLabel="M49 · full TGS shadow"
|
||||||
|
initialSpatialMode="3d"
|
||||||
|
onActiveSequenceChange={handleSequenceChange}
|
||||||
|
classifiedSpatialLayer={{
|
||||||
|
label: "TGS full shadow · causal rolling 1 s",
|
||||||
|
pointLayerLabel: "SOURCE POINTS",
|
||||||
|
cellLayerLabel: "TGS COSTMAP",
|
||||||
|
expectedAtSequence: true,
|
||||||
|
frame: classifiedFrame,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
replacePointCloud: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{semanticError ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="alert">
|
||||||
|
Semantic overlay недоступен: {semanticError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
LaboratoryEvidence,
|
||||||
|
LaboratoryResultSummary,
|
||||||
|
LaboratorySummary,
|
||||||
|
LaboratoryWorkTemplate,
|
||||||
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
|
import type { M49TgsFullShadowResult } from "../../core/laboratory/m49TgsFullShadow";
|
||||||
|
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||||
|
|
||||||
|
function number(value: number, digits = 1): string {
|
||||||
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFullShadowResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M49TgsFullShadowResult;
|
||||||
|
}) {
|
||||||
|
const accepted = result.decision.candidateRetained;
|
||||||
|
const status = accepted
|
||||||
|
? "Source-paced CPU shadow принят; визуальное и integrated-graph качество ещё проверяются"
|
||||||
|
: "Source-paced CPU shadow не прошёл performance gate";
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.9T5 · полный source-paced TRAVEL TGS shadow"
|
||||||
|
description="Один CPU-only процесс прошёл весь recorded timeline RAVNOVES00 в исходном темпе. Камера остаётся владельцем времени; dense source cloud сохраняется, поверх него показывается четырёхсостояний TGS costmap."
|
||||||
|
status={status}
|
||||||
|
statusTone={accepted ? "success" : "danger"}
|
||||||
|
facts={[
|
||||||
|
{ label: "Конфигурация", value: `${rigLabel} RIGHT · gravity-aligned LiDAR · causal ${number(result.configuration.historySeconds)} с` },
|
||||||
|
{ label: "Timeline", value: `${result.timeline.frameCount.toLocaleString("ru-RU")} кадров · ${result.timeline.availableLidarFrameCount.toLocaleString("ru-RU")} LiDAR · ${result.timeline.missingLidarFrameCount} UNOBSERVED` },
|
||||||
|
{ label: "Нагрузка", value: `Worker 006 CPU-only · GPU 0 · ${number(result.timeline.effectiveFps, 3)} source FPS` },
|
||||||
|
{ label: "Authority", value: "REPLAY-SIMULATED · navigation/actuation OFF · integrated graph отдельно" },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Удерживает ли готовый TRAVEL TGS полный десятигерцовый replay без очереди и потери кадров?",
|
||||||
|
approach: "Все 4 489 camera frames планируются по исходным timestamps. Для 3 928 доступных LiDAR frames выполняется causal rolling 1 s; 561 пропуск остаётся полностью UNOBSERVED.",
|
||||||
|
principalResult: `${result.timeline.frameCount}/4 489 frames и ${result.pointAccounting.eligible.toLocaleString("ru-RU")} eligible points учтены; TGS p95/p99 ${number(result.performance.candidateTgsMs.p95, 2)}/${number(result.performance.candidateTgsMs.p99, 2)} мс, completion age p99 ${number(result.performance.completionAgeMs.p99, 2)} мс, capacity drops ${result.performance.capacityDropCount}.`,
|
||||||
|
limitation: "Это isolated CPU shadow. Он ещё не доказывает качество красных occupied-ячеек, проходимость для конкретного корпуса или регрессию FPS полного world-state graph.",
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "deterministic",
|
||||||
|
pipelineId: "travel-tgs-full-source-paced-shadow/v1",
|
||||||
|
components: [
|
||||||
|
{ kind: "source", name: "RAVNOVES00", version: "4 489-frame recorded timeline", role: "camera-owned source clock + registered LiDAR", identitySha256: result.source.sourcePackSha256 },
|
||||||
|
{ kind: "algorithm", name: "TRAVEL GroundSeg", version: "95dc2fbd66a343efd9060c45a5711b6307a950a4", role: "gravity-aligned ground/non-ground separation; AOS OFF", identitySha256: result.configuration.configSha256 },
|
||||||
|
{ kind: "algorithm", name: "fail-closed costmap adapter", version: "v1", role: "occupied > rejected > ground > unobserved; no free inference", identitySha256: result.resultId.split("-").at(-1) ?? null },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.9T5 VISUAL EVIDENCE · FULL CAMERA TIMELINE + SOURCE CLOUD + TGS COSTMAP"
|
||||||
|
title="Полный timeline: dense исходные точки сохранены, TGS-ячейки синхронны каждому кадру"
|
||||||
|
kind="recorded-replay"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M49TgsFullShadowEvidence result={result} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Что доказал полный прогон"
|
||||||
|
status={status}
|
||||||
|
statusTone={accepted ? "success" : "danger"}
|
||||||
|
metrics={[
|
||||||
|
{ label: "Timeline", value: `${result.timeline.frameCount}/4 489`, hint: `${result.timeline.availableLidarFrameCount} LiDAR + ${result.timeline.missingLidarFrameCount} explicit UNOBSERVED` },
|
||||||
|
{ label: "TGS p95 / p99", value: `${number(result.performance.candidateTgsMs.p95, 2)} / ${number(result.performance.candidateTgsMs.p99, 2)} мс`, hint: "чистый candidate stage на CPU" },
|
||||||
|
{ label: "Completion age p99", value: `${number(result.performance.completionAgeMs.p99, 2)} мс`, hint: "от source timestamp до готового frame result" },
|
||||||
|
{ label: "Capacity drops", value: String(result.performance.capacityDropCount), hint: "кадры не отбрасывались ради темпа" },
|
||||||
|
{ label: "Point accounting", value: "100%", hint: `${result.pointAccounting.eligible.toLocaleString("ru-RU")} eligible points` },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: "Полный CPU-only TGS shadow воспроизводимо проходит recorded source clock, сохраняет fail-closed представление и не использует AOS/GPU.",
|
||||||
|
notProved: "Не приняты visual traversability, модель корпуса, камера-проекция TGS и нагрузка после встраивания в полный realtime world-state graph.",
|
||||||
|
decision: accepted
|
||||||
|
? "Кандидат остаётся. Просмотреть полный timeline, затем подключить shadow к realtime graph и измерить общий FPS/latency regression."
|
||||||
|
: "Кандидат не встраивать; сначала локализовать performance gate, который не прошёл полный replay.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
||||||
import {
|
import {
|
||||||
LaboratoryMetricEvidenceScene,
|
LaboratoryMetricEvidenceScene,
|
||||||
|
type LaboratoryMetricCellEvidence,
|
||||||
type LaboratoryMetricEvidenceSceneHandle,
|
type LaboratoryMetricEvidenceSceneHandle,
|
||||||
type LaboratoryMetricSceneMode,
|
type LaboratoryMetricSceneMode,
|
||||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||||
@@ -103,6 +104,34 @@ export interface M4ReplayThreatReviewAnchor {
|
|||||||
sourceSequence: number;
|
sourceSequence: number;
|
||||||
extentXyxyNormalized: readonly [number, number, number, number];
|
extentXyxyNormalized: readonly [number, number, number, number];
|
||||||
matchedAtThreshold: boolean;
|
matchedAtThreshold: boolean;
|
||||||
|
statusLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M4ReplayClassifiedSpatialFrame {
|
||||||
|
sourceSequence: number;
|
||||||
|
sampleAvailable?: boolean;
|
||||||
|
sourcePointCount?: number;
|
||||||
|
pointsMapGravityLocalXyzM: readonly (readonly [number, number, number])[];
|
||||||
|
pointClassIds: readonly (number | null)[];
|
||||||
|
cellsMapGravityLocal: readonly {
|
||||||
|
centerXyM: readonly [number, number];
|
||||||
|
zBoundsM: readonly [number | null, number | null];
|
||||||
|
state: LaboratoryMetricCellEvidence["state"];
|
||||||
|
}[];
|
||||||
|
cellSizeM: number;
|
||||||
|
classes: readonly RecordedEvidenceSemanticClass[];
|
||||||
|
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M4ReplayClassifiedSpatialLayer {
|
||||||
|
label: string;
|
||||||
|
pointLayerLabel: string;
|
||||||
|
cellLayerLabel: string;
|
||||||
|
expectedAtSequence: boolean;
|
||||||
|
frame: M4ReplayClassifiedSpatialFrame | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
replacePointCloud?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||||
@@ -115,6 +144,9 @@ export function M4ReplayThreatVisual({
|
|||||||
reviewLabel = "Контрольные примеры M4.8R1",
|
reviewLabel = "Контрольные примеры M4.8R1",
|
||||||
timelineEndpointRoot,
|
timelineEndpointRoot,
|
||||||
evidenceLabel = "M4.6",
|
evidenceLabel = "M4.6",
|
||||||
|
initialSpatialMode = null,
|
||||||
|
classifiedSpatialLayer,
|
||||||
|
onActiveSequenceChange,
|
||||||
}: {
|
}: {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
semantic?: M4ReplayThreatSemanticLayer;
|
semantic?: M4ReplayThreatSemanticLayer;
|
||||||
@@ -123,9 +155,14 @@ export function M4ReplayThreatVisual({
|
|||||||
reviewLabel?: string;
|
reviewLabel?: string;
|
||||||
timelineEndpointRoot?: string;
|
timelineEndpointRoot?: string;
|
||||||
evidenceLabel?: string;
|
evidenceLabel?: string;
|
||||||
|
initialSpatialMode?: LaboratoryMetricSceneMode | null;
|
||||||
|
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
|
||||||
|
onActiveSequenceChange?: (sequence: number | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
|
||||||
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
|
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
|
||||||
|
initialSpatialMode,
|
||||||
|
);
|
||||||
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
||||||
const [showLocalSurface, setShowLocalSurface] = useState(true);
|
const [showLocalSurface, setShowLocalSurface] = useState(true);
|
||||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||||
@@ -220,6 +257,9 @@ export function M4ReplayThreatVisual({
|
|||||||
}, [resultId]);
|
}, [resultId]);
|
||||||
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
||||||
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
||||||
|
useEffect(() => {
|
||||||
|
onActiveSequenceChange?.(timelineFrame.activeSequence);
|
||||||
|
}, [onActiveSequenceChange, timelineFrame.activeSequence]);
|
||||||
const lastSpatialFrameRef = useRef<{
|
const lastSpatialFrameRef = useRef<{
|
||||||
resultId: string;
|
resultId: string;
|
||||||
frame: M4ThreatTimelineFrame;
|
frame: M4ThreatTimelineFrame;
|
||||||
@@ -303,12 +343,12 @@ export function M4ReplayThreatVisual({
|
|||||||
);
|
);
|
||||||
}, [frame, metadata.timeline, showStaticObstacles]);
|
}, [frame, metadata.timeline, showStaticObstacles]);
|
||||||
const activeBoxes = useMemo(
|
const activeBoxes = useMemo(
|
||||||
() => [
|
() => classifiedSpatialLayer ? [] : [
|
||||||
...boxes(frame?.cameraProposals ?? []),
|
...boxes(frame?.cameraProposals ?? []),
|
||||||
...staticObstacleBoxes,
|
...staticObstacleBoxes,
|
||||||
...reviewAnchorBoxes,
|
...reviewAnchorBoxes,
|
||||||
],
|
],
|
||||||
[frame, reviewAnchorBoxes, staticObstacleBoxes],
|
[classifiedSpatialLayer, frame, reviewAnchorBoxes, staticObstacleBoxes],
|
||||||
);
|
);
|
||||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||||
() => semantic?.taxonomy.map((item) => ({
|
() => semantic?.taxonomy.map((item) => ({
|
||||||
@@ -363,6 +403,81 @@ export function M4ReplayThreatVisual({
|
|||||||
return status === 2 || status === 3 ? classId : null;
|
return status === 2 || status === 3 ? classId : null;
|
||||||
});
|
});
|
||||||
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||||
|
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||||
|
? spatialFrame
|
||||||
|
: null;
|
||||||
|
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
||||||
|
? classifiedSpatialLayer?.frame ?? null
|
||||||
|
: null;
|
||||||
|
const lastClassifiedSpatialFrameRef = useRef<{
|
||||||
|
resultId: string;
|
||||||
|
frame: M4ReplayClassifiedSpatialFrame;
|
||||||
|
} | null>(null);
|
||||||
|
if (classifiedSpatialLayer?.frame) {
|
||||||
|
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
|
||||||
|
}
|
||||||
|
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
|
||||||
|
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
|
||||||
|
? lastClassifiedSpatialFrameRef.current.frame
|
||||||
|
: null);
|
||||||
|
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
||||||
|
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||||
|
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||||
|
point: readonly [number, number, number],
|
||||||
|
): readonly [number, number, number] => {
|
||||||
|
const basis = activeSpatialFrame?.bodyFrame?.basisMapFromBody;
|
||||||
|
const rotated: readonly [number, number, number] = basis ? [
|
||||||
|
basis[0][0] * point[0] + basis[1][0] * point[1] + basis[2][0] * point[2],
|
||||||
|
basis[0][1] * point[0] + basis[1][1] * point[1] + basis[2][1] * point[2],
|
||||||
|
basis[0][2] * point[0] + basis[1][2] * point[1] + basis[2][2] * point[2],
|
||||||
|
] : point;
|
||||||
|
// TGS evidence is translation-only map-gravity-local with the current LiDAR
|
||||||
|
// as its origin. The metric scene uses the body ground projection as z=0.
|
||||||
|
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
||||||
|
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||||
|
const classifiedPointsBody = useMemo(
|
||||||
|
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||||
|
mapGravityLocalSensorToBodyGround,
|
||||||
|
) ?? [],
|
||||||
|
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
||||||
|
);
|
||||||
|
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
|
||||||
|
() => displayedClassifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
|
||||||
|
const body = mapGravityLocalSensorToBodyGround([
|
||||||
|
cell.centerXyM[0],
|
||||||
|
cell.centerXyM[1],
|
||||||
|
0,
|
||||||
|
]);
|
||||||
|
const [minimumSensorRelativeZ, maximumSensorRelativeZ] = cell.zBoundsM;
|
||||||
|
return {
|
||||||
|
centerBodyXyM: [body[0], body[1]],
|
||||||
|
zBoundsM: [
|
||||||
|
minimumSensorRelativeZ === null
|
||||||
|
? null
|
||||||
|
: minimumSensorRelativeZ + nominalSensorHeightM,
|
||||||
|
maximumSensorRelativeZ === null
|
||||||
|
? null
|
||||||
|
: maximumSensorRelativeZ + nominalSensorHeightM,
|
||||||
|
],
|
||||||
|
state: cell.state,
|
||||||
|
};
|
||||||
|
}) ?? [],
|
||||||
|
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
|
||||||
|
);
|
||||||
|
const classifiedCellCounts = useMemo(() => ({
|
||||||
|
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||||
|
(cell) => cell.state === "ground-support",
|
||||||
|
).length ?? 0,
|
||||||
|
occupied: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||||
|
(cell) => cell.state === "nonground-occupied",
|
||||||
|
).length ?? 0,
|
||||||
|
rejected: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||||
|
(cell) => cell.state === "unknown-rejected",
|
||||||
|
).length ?? 0,
|
||||||
|
unobserved: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
|
||||||
|
(cell) => cell.state === "unobserved",
|
||||||
|
).length ?? 0,
|
||||||
|
}), [classifiedSpatialFrame]);
|
||||||
const sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
|
const sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
|
||||||
id: obstacle.componentId,
|
id: obstacle.componentId,
|
||||||
decision: obstacle.assessment.decision,
|
decision: obstacle.assessment.decision,
|
||||||
@@ -522,7 +637,53 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const spatialLayerControls = (
|
const spatialLayerControls = classifiedSpatialLayer ? (
|
||||||
|
<div
|
||||||
|
className="m4-replay-threat-visual__pane-layer-controls"
|
||||||
|
role="group"
|
||||||
|
aria-label={`Слои ${classifiedSpatialLayer.label}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showCurrentIncrement ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showCurrentIncrement}
|
||||||
|
onClick={() => setShowCurrentIncrement((visible) => !visible)}
|
||||||
|
>
|
||||||
|
{classifiedSpatialLayer.pointLayerLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showLocalSurface ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showLocalSurface}
|
||||||
|
title="Bounded local SLAM surface · visual-derived"
|
||||||
|
onClick={() => setShowLocalSurface((visible) => !visible)}
|
||||||
|
>
|
||||||
|
LOCAL SLAM
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showRollingMap ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showRollingMap}
|
||||||
|
onClick={() => setShowRollingMap((visible) => !visible)}
|
||||||
|
>
|
||||||
|
{classifiedSpatialLayer.cellLayerLabel}
|
||||||
|
</Button>
|
||||||
|
{semantic ? (
|
||||||
|
<Button
|
||||||
|
size="compact"
|
||||||
|
shape="pill"
|
||||||
|
variant={showSpatialSemantic ? "primary" : "secondary"}
|
||||||
|
aria-pressed={showSpatialSemantic}
|
||||||
|
onClick={() => setShowSpatialSemantic((visible) => !visible)}
|
||||||
|
>
|
||||||
|
SEMANTICS
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="m4-replay-threat-visual__pane-layer-controls"
|
className="m4-replay-threat-visual__pane-layer-controls"
|
||||||
role="group"
|
role="group"
|
||||||
@@ -626,7 +787,7 @@ export function M4ReplayThreatVisual({
|
|||||||
value={String(selectedReviewAnchorIndex)}
|
value={String(selectedReviewAnchorIndex)}
|
||||||
options={reviewAnchors.map((anchor, index) => ({
|
options={reviewAnchors.map((anchor, index) => ({
|
||||||
value: String(index),
|
value: String(index),
|
||||||
label: `${index + 1}/${reviewAnchors.length} · кадр ${anchor.sourceSequence + 1} · ${anchor.matchedAtThreshold ? "покрыт" : "пропуск"}`,
|
label: `${index + 1}/${reviewAnchors.length} · кадр ${anchor.sourceSequence + 1} · ${anchor.statusLabel ?? (anchor.matchedAtThreshold ? "покрыт" : "пропуск")}`,
|
||||||
}))}
|
}))}
|
||||||
variant="split"
|
variant="split"
|
||||||
menuWidth="anchor"
|
menuWidth="anchor"
|
||||||
@@ -652,7 +813,11 @@ export function M4ReplayThreatVisual({
|
|||||||
? splitPrimarySize
|
? splitPrimarySize
|
||||||
: 100;
|
: 100;
|
||||||
|
|
||||||
const overlay = metadata.timeline && frame ? (
|
const overlaySequence = timelineFrame.activeSequence ?? frame?.sequence ?? null;
|
||||||
|
const overlaySessionSeconds = overlaySequence === null
|
||||||
|
? null
|
||||||
|
: (metadata.timeline?.frameTimesNs[overlaySequence] ?? 0) / 1_000_000_000;
|
||||||
|
const overlay = metadata.timeline && overlaySequence !== null ? (
|
||||||
<div
|
<div
|
||||||
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
|
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
|
||||||
style={{
|
style={{
|
||||||
@@ -661,9 +826,9 @@ export function M4ReplayThreatVisual({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span>RAVNOVES00 · recorded realtime</span>
|
<span>RAVNOVES00 · recorded realtime</span>
|
||||||
<strong>frame {frame.sequence + 1}/{metadata.timeline.frameCount}</strong>
|
<strong>frame {overlaySequence + 1}/{metadata.timeline.frameCount}</strong>
|
||||||
<small>
|
<small>
|
||||||
+{(frame.sessionSeconds - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
+{((overlaySessionSeconds ?? metadata.timeline.timelineStartSeconds) - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
||||||
· {displayingBufferedFrame
|
· {displayingBufferedFrame
|
||||||
? "держим последний кадр, следующий в буфере"
|
? "держим последний кадр, следующий в буфере"
|
||||||
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
||||||
@@ -671,22 +836,34 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Spatial evidence</span>
|
<span>Spatial evidence</span>
|
||||||
<strong>
|
<strong>{classifiedSpatialLayer
|
||||||
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
|
? classifiedSpatialFrame
|
||||||
{metadata.timeline.occupancyProvenanceDelivery
|
? replaceClassifiedPointCloud
|
||||||
? ` · ${lowStepObstacles.length} low-step`
|
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
||||||
: ""}
|
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} TGS cells`
|
||||||
</strong>
|
: "TGS spatial buffer"
|
||||||
<small>
|
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
|
||||||
|
<small>{classifiedSpatialLayer
|
||||||
|
? classifiedSpatialFrame
|
||||||
|
? classifiedSpatialFrame.sampleAvailable === false
|
||||||
|
? "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
|
||||||
|
: activeSpatialFrame
|
||||||
|
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
||||||
|
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
|
||||||
|
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
|
||||||
|
: (
|
||||||
|
<>
|
||||||
{spatialFrame
|
{spatialFrame
|
||||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||||
: "квалифицированный spatial frame ещё не получен"}
|
: "квалифицированный spatial frame ещё не получен"}
|
||||||
{frame.worldStateAvailable
|
{frame
|
||||||
|
? frame.worldStateAvailable
|
||||||
? " · world-state delivered"
|
? " · world-state delivered"
|
||||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
: ` · world-state gap (${frame.terminalOutcome})`
|
||||||
|
: " · world-state frame unavailable"}
|
||||||
{accumulatedCameraPoints
|
{accumulatedCameraPoints
|
||||||
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
||||||
: pointCloudOverlay
|
: pointCloudOverlay && frame
|
||||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
||||||
: showMediaPoints && cameraPointOverlay.error
|
: showMediaPoints && cameraPointOverlay.error
|
||||||
? " · накопленное camera cloud недоступно"
|
? " · накопленное camera cloud недоступно"
|
||||||
@@ -694,16 +871,21 @@ export function M4ReplayThreatVisual({
|
|||||||
{semantic && spatialSemanticFrame
|
{semantic && spatialSemanticFrame
|
||||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||||
: semantic ? " · semantic buffer" : ""}
|
: semantic ? " · semantic buffer" : ""}
|
||||||
</small>
|
</>
|
||||||
|
)}</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Virtual corridor</span>
|
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
|
||||||
<strong>
|
<strong>{classifiedSpatialLayer
|
||||||
{spatialFrame?.decisionCounts.threat ?? 0} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}
|
? classifiedSpatialFrame
|
||||||
</strong>
|
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
|
||||||
<small>
|
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
|
||||||
{metadata.timeline.corridor.forwardLengthM} м · body {metadata.timeline.rig.lengthM}×{metadata.timeline.rig.widthM} м · REPLAY-SIMULATED
|
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
|
||||||
</small>
|
<small>{classifiedSpatialLayer
|
||||||
|
? classifiedSpatialFrame
|
||||||
|
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
|
||||||
|
: "visual review only · navigation authority OFF"
|
||||||
|
: `${metadata.timeline.corridor.forwardLengthM} м · body ${metadata.timeline.rig.lengthM}×${metadata.timeline.rig.widthM} м · REPLAY-SIMULATED`}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
@@ -802,27 +984,59 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{spatialFrame ? (
|
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
|
||||||
<LaboratoryMetricEvidenceScene
|
<LaboratoryMetricEvidenceScene
|
||||||
ref={metricSceneRef}
|
ref={metricSceneRef}
|
||||||
pointCloudBodyXyzM={spatialFrame.pointCloudBodyXyzM}
|
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
|
? classifiedPointsBody
|
||||||
|
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||||
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||||
obstacles={sceneObstacles}
|
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
|
||||||
rig={timeline.rig}
|
rig={timeline.rig}
|
||||||
corridor={timeline.corridor}
|
corridor={timeline.corridor}
|
||||||
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||||
mode={spatialMode}
|
mode={spatialMode}
|
||||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||||
showCurrentIncrement={showCurrentIncrement}
|
showCurrentIncrement={showCurrentIncrement}
|
||||||
showLocalSurface={showLocalSurface}
|
showLocalSurface={showLocalSurface}
|
||||||
showRollingMap={showRollingMap}
|
showRollingMap={showRollingMap}
|
||||||
showLowStep={showLowStep}
|
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
|
||||||
pointSemanticClassIds={alignedSemanticPointIds}
|
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
semanticClasses={semanticClasses}
|
? displayedClassifiedSpatialFrame.pointClassIds
|
||||||
semanticPalette={semanticPalette}
|
: alignedSemanticPointIds}
|
||||||
|
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
|
? displayedClassifiedSpatialFrame.classes
|
||||||
|
: semanticClasses}
|
||||||
|
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
|
? displayedClassifiedSpatialFrame.palette
|
||||||
|
: semanticPalette}
|
||||||
|
classifiedCells={classifiedCellsBody}
|
||||||
|
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
|
||||||
|
showClassifiedCells={showRollingMap}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{frame && !frame.spatialAvailable ? (
|
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
|
||||||
|
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
|
||||||
|
{classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||||
|
? <span className="busy-indicator" aria-hidden="true" />
|
||||||
|
: <Icon name="alert" size={18} />}
|
||||||
|
<span>{classifiedSpatialLayer.error
|
||||||
|
?? (classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||||
|
? `Открываем ${classifiedSpatialLayer.label}`
|
||||||
|
: classifiedSpatialLayer.expectedAtSequence
|
||||||
|
? `Открываем ${classifiedSpatialLayer.label}`
|
||||||
|
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{classifiedSpatialFrame?.sampleAvailable === false ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; все 2 244 TGS-ячейки явно UNOBSERVED.
|
||||||
|
</div>
|
||||||
|
) : classifiedSpatialFrame && !activeSpatialFrame ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Кадр {classifiedSpatialFrame.sourceSequence + 1}: TGS costmap показан cell-only; linked source cloud для отрисовки отсутствует.
|
||||||
|
</div>
|
||||||
|
) : frame && !frame.spatialAvailable ? (
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
{spatialFrame
|
{spatialFrame
|
||||||
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
|
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
|
||||||
|
|||||||
@@ -105,6 +105,20 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
|||||||
experimentName: "RF-DETR native risk review and temporal identity",
|
experimentName: "RF-DETR native risk review and temporal identity",
|
||||||
variantName: "M4.8Q · native raw KB4 review · quality not adjudicated",
|
variantName: "M4.8Q · native raw KB4 review · quality not adjudicated",
|
||||||
},
|
},
|
||||||
|
"m49-tgs-fail-closed-evidence": {
|
||||||
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + gravity-aligned LiDAR`,
|
||||||
|
experimentId: "m49-tgs-fail-closed-evidence",
|
||||||
|
experimentName: "TRAVEL TGS fail-closed traversability evidence",
|
||||||
|
variantName: "M4.9T4 · 10 anchors · causal rolling 1 s · AOS OFF",
|
||||||
|
},
|
||||||
|
"m49-tgs-full-shadow": {
|
||||||
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + gravity-aligned LiDAR`,
|
||||||
|
experimentId: "m49-tgs-full-shadow",
|
||||||
|
experimentName: "TRAVEL TGS complete source-paced shadow",
|
||||||
|
variantName: "M4.9T5 · 4 489 frames · causal rolling 1 s · CPU-only",
|
||||||
|
},
|
||||||
"m47-reference-graph-shadow": {
|
"m47-reference-graph-shadow": {
|
||||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function mergeResults(
|
|||||||
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
|
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
|
||||||
m48s: next.m48s ?? current.m48s,
|
m48s: next.m48s ?? current.m48s,
|
||||||
m48t: next.m48t ?? current.m48t,
|
m48t: next.m48t ?? current.m48t,
|
||||||
|
m49Tgs: next.m49Tgs ?? current.m49Tgs,
|
||||||
|
m49TgsFull: next.m49TgsFull ?? current.m49TgsFull,
|
||||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||||
l3: next.l3 ?? current.l3,
|
l3: next.l3 ?? current.l3,
|
||||||
l31: next.l31 ?? current.l31,
|
l31: next.l31 ?? current.l31,
|
||||||
@@ -124,6 +126,8 @@ export function useAdvancedLaboratoryCatalog({
|
|||||||
"m48-small-static-passage-regression",
|
"m48-small-static-passage-regression",
|
||||||
"m48-static-occupancy-qualification",
|
"m48-static-occupancy-qualification",
|
||||||
"m48r3-static-occupancy-shadow",
|
"m48r3-static-occupancy-shadow",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
].includes(selectedWorkId)
|
].includes(selectedWorkId)
|
||||||
&& !indexedResultId
|
&& !indexedResultId
|
||||||
) return;
|
) return;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let server;
|
|||||||
let fetchAdvancedLaboratoryResults;
|
let fetchAdvancedLaboratoryResults;
|
||||||
let fetchAdvancedLaboratoryIndex;
|
let fetchAdvancedLaboratoryIndex;
|
||||||
let fetchAdvancedLaboratoryResult;
|
let fetchAdvancedLaboratoryResult;
|
||||||
|
let fetchM49TgsAnchorSpatial;
|
||||||
let AdvancedLaboratoryContractError;
|
let AdvancedLaboratoryContractError;
|
||||||
let buildLaboratoryCatalog;
|
let buildLaboratoryCatalog;
|
||||||
let buildLaboratoryProfiles;
|
let buildLaboratoryProfiles;
|
||||||
@@ -836,6 +837,77 @@ function e40() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function m49View() {
|
||||||
|
const anchors = [171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856];
|
||||||
|
return {
|
||||||
|
schema_version: "missioncore.m49-tgs-fail-closed-view/v1",
|
||||||
|
result_id: `m49-tgs-fail-closed-${"2".repeat(64)}`,
|
||||||
|
created_at_utc: "2026-08-26T17:09:43Z",
|
||||||
|
source: {
|
||||||
|
source_id: "RAVNOVES00",
|
||||||
|
source_session_id: "20260720T065719Z_viewer_live",
|
||||||
|
source_pack_sha256: "3".repeat(64),
|
||||||
|
linked_visual_result_id: `m4-threat-replay-${"4".repeat(64)}`,
|
||||||
|
anchor_frame_indices: anchors,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
profile_id: "m49-ravnoves00-tgs-fail-closed-evidence/v1",
|
||||||
|
config_sha256: "5".repeat(64),
|
||||||
|
coordinate_frame: "map-gravity-local",
|
||||||
|
primary_profile: "causal_rolling_1s",
|
||||||
|
cell_size_m: 0.45,
|
||||||
|
radius_m: 12,
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
worker: "Worker 006",
|
||||||
|
device: "cpu",
|
||||||
|
gpu_used: false,
|
||||||
|
wrapper_elapsed_seconds: 12.1,
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
anchor_count: 10,
|
||||||
|
anchor_profile_count: 20,
|
||||||
|
all_eligible_points_accounted: true,
|
||||||
|
costmap_cell_count: 2244,
|
||||||
|
process_wall_current_p50_ms: 20,
|
||||||
|
process_wall_current_max_ms: 30,
|
||||||
|
process_wall_rolling_p50_ms: 30,
|
||||||
|
process_wall_rolling_max_ms: 40,
|
||||||
|
process_max_rss_kib: 9292,
|
||||||
|
primary: anchors.map((anchor, slot) => ({
|
||||||
|
anchor_frame_index: anchor,
|
||||||
|
slot,
|
||||||
|
profile_id: "causal_rolling_1s",
|
||||||
|
point_count: 3,
|
||||||
|
ground_point_count: 1,
|
||||||
|
nonground_point_count: 1,
|
||||||
|
rejected_point_count: 1,
|
||||||
|
ground_cell_count: 1,
|
||||||
|
nonground_cell_count: 1,
|
||||||
|
rejected_cell_count: 1,
|
||||||
|
unobserved_cell_count: 2241,
|
||||||
|
all_points_accounted: true,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
acceptance: {
|
||||||
|
representation_complete: true,
|
||||||
|
all_points_accounted: true,
|
||||||
|
aos_absent: true,
|
||||||
|
gpu_absent: true,
|
||||||
|
visual_quality_accepted: false,
|
||||||
|
traversability_accepted: false,
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
state: "visual-review-required",
|
||||||
|
candidate_retained: true,
|
||||||
|
next_action: "Review the ten anchors.",
|
||||||
|
},
|
||||||
|
limitations: ["bounded diagnostic evidence"],
|
||||||
|
ground_truth: false,
|
||||||
|
access: "read-only",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
server = await createServer({
|
server = await createServer({
|
||||||
appType: "custom",
|
appType: "custom",
|
||||||
@@ -850,6 +922,9 @@ before(async () => {
|
|||||||
fetchAdvancedLaboratoryIndex,
|
fetchAdvancedLaboratoryIndex,
|
||||||
fetchAdvancedLaboratoryResult,
|
fetchAdvancedLaboratoryResult,
|
||||||
} = await server.ssrLoadModule("/src/core/laboratory/advancedIndex.ts"));
|
} = await server.ssrLoadModule("/src/core/laboratory/advancedIndex.ts"));
|
||||||
|
({ fetchM49TgsAnchorSpatial } = await server.ssrLoadModule(
|
||||||
|
"/src/core/laboratory/m49TgsFailClosed.ts",
|
||||||
|
));
|
||||||
({
|
({
|
||||||
buildLaboratoryCatalog,
|
buildLaboratoryCatalog,
|
||||||
buildLaboratoryProfiles,
|
buildLaboratoryProfiles,
|
||||||
@@ -1596,6 +1671,62 @@ test("selected advanced LAB fetches only its own strict catalog", async () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("selected M49 LAB preserves the sealed fail-closed contract", async () => {
|
||||||
|
const payload = m49View();
|
||||||
|
const requests = [];
|
||||||
|
const decoded = await fetchAdvancedLaboratoryResult(
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
{
|
||||||
|
resultId: payload.result_id,
|
||||||
|
fetcher: async (input, init) => {
|
||||||
|
requests.push({ input: String(input), method: init?.method });
|
||||||
|
return new Response(JSON.stringify(payload), { status: 200 });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(decoded.m49Tgs.metrics.anchorCount, 10);
|
||||||
|
assert.equal(decoded.m49Tgs.metrics.costmapCellCount, 2244);
|
||||||
|
assert.equal(decoded.m49Tgs.acceptance.visualQualityAccepted, false);
|
||||||
|
assert.deepEqual(requests, [{
|
||||||
|
input: `/api/v1/laboratory/m49/tgs-fail-closed/${payload.result_id}`,
|
||||||
|
method: "GET",
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M49 anchor fetch keeps every point and all four costmap states", async () => {
|
||||||
|
const resultId = m49View().result_id;
|
||||||
|
const centers = Array.from({ length: 2244 }, (_, index) => [index * 0.45, 0]);
|
||||||
|
const states = Array.from({ length: 2244 }, (_, index) => index % 4);
|
||||||
|
const zBounds = states.map((state) => state === 0 ? [null, null] : [0, 0.2]);
|
||||||
|
const decoded = await fetchM49TgsAnchorSpatial(resultId, 171, "causal_rolling_1s", {
|
||||||
|
fetcher: async () => new Response(JSON.stringify({
|
||||||
|
schema_version: "missioncore.m49-tgs-anchor-spatial/v1",
|
||||||
|
result_id: resultId,
|
||||||
|
anchor_frame_index: 171,
|
||||||
|
source_sequence: 171,
|
||||||
|
profile: "causal_rolling_1s",
|
||||||
|
coordinate_frame: "map-gravity-local",
|
||||||
|
points_xyz_m: [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
||||||
|
point_states: [1, 2, 3],
|
||||||
|
costmap: {
|
||||||
|
cell_size_m: 0.45,
|
||||||
|
radius_m: 12,
|
||||||
|
centers_xy_m: centers,
|
||||||
|
states,
|
||||||
|
z_bounds_m: zBounds,
|
||||||
|
},
|
||||||
|
all_points_accounted: true,
|
||||||
|
aos_used: false,
|
||||||
|
access: "read-only",
|
||||||
|
}), { status: 200 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(decoded.pointStates, [1, 2, 3]);
|
||||||
|
assert.equal(decoded.costmap.states.length, 2244);
|
||||||
|
assert.deepEqual(new Set(decoded.costmap.states), new Set([0, 1, 2, 3]));
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps valid LAB catalogs available when one transport endpoint fails", async () => {
|
test("keeps valid LAB catalogs available when one transport endpoint fails", async () => {
|
||||||
const decoded = await fetchAdvancedLaboratoryResults({
|
const decoded = await fetchAdvancedLaboratoryResults({
|
||||||
fetcher: async (input) => {
|
fetcher: async (input) => {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { after, before, test } from "node:test";
|
||||||
|
|
||||||
|
import { createServer } from "vite";
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let fetchM49TgsFullShadowSpatialChunk;
|
||||||
|
|
||||||
|
const resultId = `m49-tgs-full-shadow-${"a".repeat(64)}`;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = await createServer({
|
||||||
|
appType: "custom",
|
||||||
|
logLevel: "silent",
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
});
|
||||||
|
({ fetchM49TgsFullShadowSpatialChunk } = await server.ssrLoadModule(
|
||||||
|
"/src/core/laboratory/m49TgsFullShadow.ts",
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.9T5 chunk keeps every missing-LiDAR cell explicitly UNOBSERVED", async () => {
|
||||||
|
let requestedUrl = "";
|
||||||
|
const centers = Array.from({ length: 2244 }, (_, index) => [index * 0.45, 0]);
|
||||||
|
const unobserved = Array.from({ length: 2244 }, () => 0);
|
||||||
|
const zBounds = Array.from({ length: 2244 }, () => [null, null]);
|
||||||
|
const metrics = {
|
||||||
|
eligible_point_count: 0,
|
||||||
|
ground_point_count: 0,
|
||||||
|
nonground_point_count: 0,
|
||||||
|
rejected_point_count: 0,
|
||||||
|
occupied_cell_count: 0,
|
||||||
|
};
|
||||||
|
const chunk = await fetchM49TgsFullShadowSpatialChunk(resultId, 7, 1, {
|
||||||
|
fetcher: async (url) => {
|
||||||
|
requestedUrl = String(url);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
schema_version: "missioncore.m49-tgs-full-shadow-spatial-chunk/v1",
|
||||||
|
result_id: resultId,
|
||||||
|
start: 7,
|
||||||
|
count: 1,
|
||||||
|
coordinate_frame: "map-gravity-local",
|
||||||
|
costmap: {
|
||||||
|
cell_size_m: 0.45,
|
||||||
|
radius_m: 12,
|
||||||
|
centers_xy_m: centers,
|
||||||
|
},
|
||||||
|
frames: [{
|
||||||
|
source_sequence: 7,
|
||||||
|
source_frame_index: 7,
|
||||||
|
session_seconds: 36.119857292,
|
||||||
|
sample_available: false,
|
||||||
|
states: unobserved,
|
||||||
|
z_bounds_m: zBounds,
|
||||||
|
metrics,
|
||||||
|
}],
|
||||||
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
requestedUrl,
|
||||||
|
`/api/v1/laboratory/m49/tgs-full-shadow/${resultId}/spatial/chunk?start=7&count=1`,
|
||||||
|
);
|
||||||
|
assert.equal(chunk.count, 1);
|
||||||
|
assert.equal(chunk.frames[0].sampleAvailable, false);
|
||||||
|
assert.equal(chunk.frames[0].costmap.states.length, 2244);
|
||||||
|
assert.deepEqual(new Set(chunk.frames[0].costmap.states), new Set([0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.9T5 viewer prefetches 24-frame immutable chunks", async () => {
|
||||||
|
const [source, contract] = await Promise.all([
|
||||||
|
readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
readFile(
|
||||||
|
new URL("../src/core/laboratory/m49TgsFullShadow.ts", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
assert.match(source, /const CHUNK_FRAMES = 24/);
|
||||||
|
assert.match(source, /activeChunkStart \+ CHUNK_FRAMES/);
|
||||||
|
assert.match(source, /fetchM49TgsFullShadowSpatialChunk/);
|
||||||
|
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
|
||||||
|
assert.match(source, /fetchE47SemanticSlamResult/);
|
||||||
|
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
|
||||||
|
assert.match(source, /semantic=\{semantic \? \{/);
|
||||||
|
assert.match(contract, /linked_semantic_result_id/);
|
||||||
|
});
|
||||||
@@ -799,7 +799,27 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
|||||||
assert.match(metricScene, /OrbitControls/);
|
assert.match(metricScene, /OrbitControls/);
|
||||||
assert.match(visual, /LOCAL SLAM/);
|
assert.match(visual, /LOCAL SLAM/);
|
||||||
assert.match(visual, /showLocalSurface/);
|
assert.match(visual, /showLocalSurface/);
|
||||||
assert.match(visual, /pointCloudBodyXyzM=\{spatialFrame\.pointCloudBodyXyzM\}/);
|
assert.match(
|
||||||
|
visual,
|
||||||
|
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
visual,
|
||||||
|
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame[\s\S]*lastClassifiedSpatialFrameRef/,
|
||||||
|
);
|
||||||
|
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
|
||||||
|
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
|
||||||
|
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
|
||||||
|
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
|
||||||
|
assert.match(
|
||||||
|
visual,
|
||||||
|
/mapGravityLocalSensorToBodyGround[\s\S]*rotated\[2\] \+ nominalSensorHeightM/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
visual,
|
||||||
|
/zBoundsM: \[[\s\S]*minimumSensorRelativeZ \+ nominalSensorHeightM[\s\S]*maximumSensorRelativeZ \+ nominalSensorHeightM/,
|
||||||
|
);
|
||||||
|
assert.match(visual, /classifiedCells=\{classifiedCellsBody\}/);
|
||||||
assert.match(metricScene, /Локальная SLAM-поверхность/);
|
assert.match(metricScene, /Локальная SLAM-поверхность/);
|
||||||
assert.match(visual, /showJumpToEnd=\{false\}/);
|
assert.match(visual, /showJumpToEnd=\{false\}/);
|
||||||
assert.doesNotMatch(visual, /Назад на 5 секунд/);
|
assert.doesNotMatch(visual, /Назад на 5 секунд/);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||||
|
"work_id": "m49-tgs-fail-closed-evidence",
|
||||||
|
"evidence": {
|
||||||
|
"runtime_relative_root": "m49/tgs-fail-closed-results",
|
||||||
|
"result_id_prefix": "m49-tgs-fail-closed",
|
||||||
|
"document_name": "manifest.json",
|
||||||
|
"schema_version": "missioncore.m49-tgs-fail-closed-result/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||||
|
"work_id": "m49-tgs-full-shadow",
|
||||||
|
"evidence": {
|
||||||
|
"runtime_relative_root": "m49/tgs-full-shadow-results",
|
||||||
|
"result_id_prefix": "m49-tgs-full-shadow",
|
||||||
|
"document_name": "manifest.json",
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,34 @@
|
|||||||
"run": "missioncore.laboratory-run/v1",
|
"run": "missioncore.laboratory-run/v1",
|
||||||
"evidence": "missioncore.m48t-risk-quality-temporal-lab/v1"
|
"evidence": "missioncore.m48t-risk-quality-temporal-lab/v1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"work_id": "m49-tgs-fail-closed-evidence",
|
||||||
|
"lifecycle": "experimental",
|
||||||
|
"isolation": "bounded-adapter",
|
||||||
|
"adapter_id": "experimental.m49-tgs-fail-closed-evidence/v1",
|
||||||
|
"input_roles": ["repository_root"],
|
||||||
|
"contracts": {
|
||||||
|
"source": "missioncore.m49-tgs-worker-evidence/v1",
|
||||||
|
"provider": "missioncore.travel-tgs-ground-segmentation/v1",
|
||||||
|
"graph": "missioncore.m49-fail-closed-costmap-evidence/v1",
|
||||||
|
"run": "missioncore.laboratory-run/v1",
|
||||||
|
"evidence": "missioncore.m49-tgs-fail-closed-result/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"work_id": "m49-tgs-full-shadow",
|
||||||
|
"lifecycle": "experimental",
|
||||||
|
"isolation": "bounded-adapter",
|
||||||
|
"adapter_id": "experimental.m49-tgs-full-shadow/v1",
|
||||||
|
"input_roles": ["repository_root"],
|
||||||
|
"contracts": {
|
||||||
|
"source": "missioncore.m49-tgs-full-shadow-result/v1",
|
||||||
|
"provider": "missioncore.travel-tgs-ground-segmentation/v1",
|
||||||
|
"graph": "missioncore.m49-tgs-full-source-paced-shadow/v1",
|
||||||
|
"run": "missioncore.laboratory-run/v1",
|
||||||
|
"evidence": "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"legacy_work_ids": [
|
"legacy_work_ids": [
|
||||||
|
|||||||
@@ -267,6 +267,20 @@
|
|||||||
"signal": "progress",
|
"signal": "progress",
|
||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"catalog_id": "m49-tgs-fail-closed-evidence",
|
||||||
|
"evidence_id": "m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b",
|
||||||
|
"signal": "progress",
|
||||||
|
"lifecycle": "current",
|
||||||
|
"visual_evidence": "available"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"catalog_id": "m49-tgs-full-shadow",
|
||||||
|
"evidence_id": "m49-tgs-full-shadow-0faeaaf3aba8dccae974eab51ff9cccf264a13ec285abb1920c1e5a09a7e87bc",
|
||||||
|
"signal": "progress",
|
||||||
|
"lifecycle": "current",
|
||||||
|
"visual_evidence": "available"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-profile/v1",
|
||||||
|
"profile_id": "m49-ravnoves00-tgs-full-shadow/v1",
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
|
||||||
|
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
|
||||||
|
"travel_revision": "95dc2fbd66a343efd9060c45a5711b6307a950a4",
|
||||||
|
"input_coordinate_frame": "map-gravity-local-translation-only",
|
||||||
|
"expected_timeline_frames": 4489,
|
||||||
|
"expected_available_lidar_frames": 3928
|
||||||
|
},
|
||||||
|
"tgs": {
|
||||||
|
"max_range_m": 80.0,
|
||||||
|
"min_range_m": 1.0,
|
||||||
|
"resolution_m": 8.0,
|
||||||
|
"num_iterations": 3,
|
||||||
|
"num_lowest_representative_points": 5,
|
||||||
|
"minimum_points": 10,
|
||||||
|
"seed_threshold_m": 0.5,
|
||||||
|
"distance_threshold_m": 0.125,
|
||||||
|
"outlier_threshold_m": 0.3,
|
||||||
|
"normal_threshold": 0.94,
|
||||||
|
"weight_threshold": 200.0,
|
||||||
|
"lcc_normal_similarity": 0.03,
|
||||||
|
"lcc_planar_distance_m": 0.1,
|
||||||
|
"obstacle_height_m": 1.0,
|
||||||
|
"refine_mode": true
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"id": "causal_rolling_1s",
|
||||||
|
"history_seconds": 1.0,
|
||||||
|
"local_radius_m": 12.0,
|
||||||
|
"missing_lidar_policy": "all-cells-unobserved"
|
||||||
|
},
|
||||||
|
"costmap": {
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"state_priority": [
|
||||||
|
"NONGROUND_OCCUPIED",
|
||||||
|
"UNKNOWN_REJECTED",
|
||||||
|
"GROUND_SUPPORT",
|
||||||
|
"UNOBSERVED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"state_codes": {
|
||||||
|
"UNOBSERVED": 0,
|
||||||
|
"GROUND_SUPPORT": 1,
|
||||||
|
"NONGROUND_OCCUPIED": 2,
|
||||||
|
"UNKNOWN_REJECTED": 3
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"recorded_source_rate_hz": 10.0,
|
||||||
|
"minimum_effective_timeline_fps": 10.0,
|
||||||
|
"candidate_stage_p95_ms_max": 25.0,
|
||||||
|
"candidate_stage_p99_ms_max": 50.0,
|
||||||
|
"completion_age_p99_ms_max": 100.0,
|
||||||
|
"capacity_drop_count_max": 0,
|
||||||
|
"unaccounted_available_frame_count_max": 0,
|
||||||
|
"unaccounted_eligible_point_count_max": 0
|
||||||
|
},
|
||||||
|
"invariants": {
|
||||||
|
"all_eligible_input_points_accounted": true,
|
||||||
|
"aos_allowed": false,
|
||||||
|
"lidar_orientation_applied_to_tgs_input": false,
|
||||||
|
"map_gravity_axis_preserved": true,
|
||||||
|
"missing_support_means_free": false,
|
||||||
|
"missing_lidar_means_unobserved": true,
|
||||||
|
"unobserved_cells_are_emitted": true,
|
||||||
|
"future_frames_used": false,
|
||||||
|
"camera_projection_is_authoritative": false,
|
||||||
|
"gpu_allowed": false,
|
||||||
|
"navigation_or_actuation_allowed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -326,7 +326,23 @@ invocation. It emits a deterministic `0.45 m`, `12 m` local evidence grid while
|
|||||||
preserving unobserved cells. See
|
preserving unobserved cells. See
|
||||||
[`experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md`](../experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md).
|
[`experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md`](../experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md).
|
||||||
Representation/accounting is accepted for the visual gate; obstacle and
|
Representation/accounting is accepted for the visual gate; obstacle and
|
||||||
traversability quality are not yet accepted.
|
traversability quality are not yet accepted. The sealed result is now published
|
||||||
|
in the canonical LAB as
|
||||||
|
`m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b`,
|
||||||
|
linked to the current M4 v3 recorded timeline. Publication adds no navigation
|
||||||
|
or actuation authority.
|
||||||
|
|
||||||
|
The subsequent complete source-paced TGS-only shadow passed on `2026-08-26`.
|
||||||
|
It accounted for all `4,489` timeline frames and all `63,646,163` eligible
|
||||||
|
points, emitted the `561` missing-LiDAR frames as explicit all-cell
|
||||||
|
`UNOBSERVED`, sustained `10.003945 FPS`, measured TGS `p95/p99` at
|
||||||
|
`1.694/2.08973 ms`, completion-age `p99` at `41.42845548 ms`, and recorded zero
|
||||||
|
capacity drops. The immutable result and canonical LAB acceptance are recorded
|
||||||
|
in
|
||||||
|
[`experiments/perception/M49_TGS_FULL_SHADOW_ACCEPTANCE_2026-08-26.md`](../experiments/perception/M49_TGS_FULL_SHADOW_ACCEPTANCE_2026-08-26.md).
|
||||||
|
This accepts the CPU source-paced shadow and retains the candidate. It does not
|
||||||
|
accept visual traversability quality, integrated graph performance, navigation
|
||||||
|
or actuation.
|
||||||
|
|
||||||
### T4 — Candidate C occupancy/ESDF probe
|
### T4 — Candidate C occupancy/ESDF probe
|
||||||
|
|
||||||
@@ -345,12 +361,15 @@ traversability quality are not yet accepted.
|
|||||||
|
|
||||||
## Immediate next action
|
## Immediate next action
|
||||||
|
|
||||||
Import the sealed gravity-aligned TGS evidence pack into one laboratory review
|
Review the published full gravity-aligned TGS timeline in camera, metric 3D and
|
||||||
surface and review the ten anchors in metric 3D/costmap space. Preserve
|
costmap space. Preserve
|
||||||
`GROUND_SUPPORT`, `NONGROUND_OCCUPIED`, `UNKNOWN_REJECTED` and `UNOBSERVED` as
|
`GROUND_SUPPORT`, `NONGROUND_OCCUPIED`, `UNKNOWN_REJECTED` and `UNOBSERVED` as
|
||||||
separate products; do not use AOS or infer free cells from absent
|
separate products; do not use AOS or infer free cells from absent
|
||||||
republication. Add diagnostic camera projection only after the metric evidence
|
republication. Attach the unchanged candidate to the realtime world-state graph
|
||||||
is accepted. Candidate A T2 replay remains blocked by the failed unmodified T1
|
in non-authoritative shadow mode and measure complete-graph FPS, latency,
|
||||||
gate. No LOW-STEP tuning, new object model, camera resize, fisheye
|
resource use and queue drops. If occupied evidence carpets the route, soft
|
||||||
|
traversable vegetation or usable gaps, reject TGS without tuning it against the
|
||||||
|
review evidence. Candidate A T2 replay remains blocked by the failed unmodified
|
||||||
|
T1 gate. No LOW-STEP tuning, new object model, camera resize, fisheye
|
||||||
rectification, manual dataset or parallel heavy Worker job is authorized by
|
rectification, manual dataset or parallel heavy Worker job is authorized by
|
||||||
this decision.
|
this decision.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# M4.9 gravity-aligned TGS fail-closed evidence — 2026-08-26
|
# M4.9 gravity-aligned TGS fail-closed evidence — 2026-08-26
|
||||||
|
|
||||||
Status: **evidence adapter completed**; visual obstacle/traversability quality,
|
Status: **evidence adapter and canonical LAB publication completed**; visual
|
||||||
full-source realtime, navigation and actuation remain disabled
|
obstacle/traversability quality, full-source realtime, navigation and actuation
|
||||||
|
remain disabled
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ accounts for every point inside TRAVEL's declared `1–80 m` processing range.
|
|||||||
This closes the representation and accounting gate. It does not accept visual
|
This closes the representation and accounting gate. It does not accept visual
|
||||||
quality. Several anchors still contain a high non-ground share, so the next
|
quality. Several anchors still contain a high non-ground share, so the next
|
||||||
gate is a 3D/costmap review of the sealed output rather than threshold tuning or
|
gate is a 3D/costmap review of the sealed output rather than threshold tuning or
|
||||||
camera boxes.
|
camera boxes. The metric output is now available in the canonical LAB on port
|
||||||
|
`8000` as an operator-review surface; publication does not upgrade it to an
|
||||||
|
accepted terrain provider.
|
||||||
|
|
||||||
## Frozen identity
|
## Frozen identity
|
||||||
|
|
||||||
@@ -125,15 +128,73 @@ Worker root:
|
|||||||
| `inputs/input-manifest.json` | `13,357` | `7a22e61606e1fa9ab61d47417f941b590a68b29e4529d4c290992ce7eb89a642` |
|
| `inputs/input-manifest.json` | `13,357` | `7a22e61606e1fa9ab61d47417f941b590a68b29e4529d4c290992ce7eb89a642` |
|
||||||
| `tgs-timing.tsv` | `638` | `a8f439996205286335ac392b37f51b54056d04e9cb07e03648c52c1e8f2f6ae8` |
|
| `tgs-timing.tsv` | `638` | `a8f439996205286335ac392b37f51b54056d04e9cb07e03648c52c1e8f2f6ae8` |
|
||||||
|
|
||||||
|
## Canonical LAB publication
|
||||||
|
|
||||||
|
- LAB result:
|
||||||
|
`m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b`;
|
||||||
|
- linked current visual timeline:
|
||||||
|
`m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`;
|
||||||
|
- source LiDAR pack SHA-256 remains
|
||||||
|
`0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944`;
|
||||||
|
- the earlier immutable seal beginning `200b57cc` is superseded only because it
|
||||||
|
linked the retired M4 v2 visual profile, whose timeline is incompatible with
|
||||||
|
the current v3 API. No Worker evidence byte or TGS result was changed.
|
||||||
|
|
||||||
|
The LAB reuses the canonical recorded-realtime camera and spatial instrument.
|
||||||
|
It exposes the ten anchors, the full classified TGS point set, the four-state
|
||||||
|
costmap, `VIDEO/CAMERA`, `3D/PLAN`, independent point/costmap visibility and
|
||||||
|
exact per-anchor counts. Camera proposal boxes, AOS clusters, future frames and
|
||||||
|
navigation authority are absent. Browser acceptance confirmed that every
|
||||||
|
operator control is independent and that a loading anchor cannot temporarily
|
||||||
|
display unrelated M4 obstacle statistics.
|
||||||
|
|
||||||
|
The published TGS coordinates are intentionally sensor-origin
|
||||||
|
`map-gravity-local`: the ground is therefore approximately `-1.25 m` below the
|
||||||
|
current LiDAR. The canonical LAB presentation converts these immutable values
|
||||||
|
to the scene's body-ground origin by adding the rig's sealed
|
||||||
|
`nominal_sensor_height_m = 1.25` after the gravity-to-body rotation. The same
|
||||||
|
offset is applied to point heights and costmap z-bounds; x/y evidence and the
|
||||||
|
sealed Worker result remain unchanged. A regression check across all ten
|
||||||
|
anchors puts the median displayed ground points in `-0.299…+0.135 m` and the
|
||||||
|
median ground-cell centres in `-0.308…+0.237 m`. On anchor `1856`, where the
|
||||||
|
presentation defect was reported, those medians are `+0.004 m` and `-0.063 m`.
|
||||||
|
The residual variation is measured terrain/pose variation, not the previous
|
||||||
|
systematic `1.25 m` lift.
|
||||||
|
|
||||||
|
## Preliminary corridor triage
|
||||||
|
|
||||||
|
The published evidence was also projected into the frozen virtual body
|
||||||
|
corridor (`-0.5…8.0 m` longitudinal, `±0.5 m` lateral) without changing the
|
||||||
|
sealed result. The table counts costmap cell centres inside that corridor:
|
||||||
|
|
||||||
|
| Anchor | Current occupied | Rolling occupied | Rolling unobserved | Preliminary reading |
|
||||||
|
| ---: | ---: | ---: | ---: | --- |
|
||||||
|
| `171` | `0` | `8` | `6` | unresolved rolling band at `5.63…7.90 m` |
|
||||||
|
| `306` | `0` | `0` | `12` | no corridor obstruction |
|
||||||
|
| `368` | `1` | `1` | `16` | compact occupied support at `6.34 m` |
|
||||||
|
| `402` | `0` | `1` | `10` | compact occupied support at `1.89 m` |
|
||||||
|
| `450` | `0` | `0` | `8` | no corridor obstruction |
|
||||||
|
| `509` | `0` | `0` | `7` | no corridor obstruction |
|
||||||
|
| `525` | `0` | `0` | `9` | no corridor obstruction |
|
||||||
|
| `744` | `0` | `3` | `25` | weakly observed; requires explicit review |
|
||||||
|
| `1122` | `0` | `2` | `24` | rolling recovers compact obstacle support |
|
||||||
|
| `1856` | `1` | `1` | `13` | compact occupied support at `4.14 m` |
|
||||||
|
|
||||||
|
This is a diagnostic, not ground truth. It shows why the causal window cannot
|
||||||
|
simply be removed: anchor `1122` is entirely unobserved in the current-only
|
||||||
|
corridor but gains ground and occupied evidence in rolling. It also identifies
|
||||||
|
the opposing risk: at anchor `171`, rolling adds eight occupied cells where the
|
||||||
|
camera/PLAN comparison appears to show an open route apart from side geometry.
|
||||||
|
Anchor `744` remains too weakly observed for acceptance. These two anchors keep
|
||||||
|
the visual gate open even though the remaining frames do not show a continuous
|
||||||
|
occupied carpet.
|
||||||
|
|
||||||
## Next gate
|
## Next gate
|
||||||
|
|
||||||
Import the sealed `evidence.npz` into one laboratory review surface with:
|
Review the published ten anchors in both metric `3D` and `PLAN`. The operator
|
||||||
|
must focus on the mandatory obstacle/gap cases and the large non-ground
|
||||||
1. 3D points colored by the four evidence states;
|
populations listed above. Accept TGS for a full recorded-source shadow only if
|
||||||
2. top-down `0.45 m` costmap cells in the same local coordinate frame;
|
red non-ground evidence remains on real geometry without carpeting the visible
|
||||||
3. anchor selection and exact counts from `result.json`;
|
route, soft traversable vegetation or usable gaps. Otherwise reject this
|
||||||
4. no AOS clusters, camera boxes, future frames or physical authority.
|
candidate without tuning thresholds against these ten anchors. Camera
|
||||||
|
projection follows only if the metric evidence is accepted.
|
||||||
The operator review must focus on the mandatory obstacle/gap cases and on the
|
|
||||||
large non-ground populations listed above. Camera projection follows only if
|
|
||||||
the metric evidence is accepted.
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# M4.9T5 full source-paced TRAVEL TGS shadow — 2026-08-26
|
||||||
|
|
||||||
|
Status: **recorded source-paced CPU shadow accepted; candidate retained**.
|
||||||
|
Visual traversability quality, full-graph performance, navigation and actuation
|
||||||
|
remain unaccepted.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The gravity-aligned TGS-only candidate completed the entire `RAVNOVES00`
|
||||||
|
recorded timeline on Worker 006. Every source timeline frame and every eligible
|
||||||
|
LiDAR point is represented. No frame was dropped for capacity, AOS was never
|
||||||
|
invoked, no GPU was requested and the canonical Triton container remained
|
||||||
|
healthy with the same identity.
|
||||||
|
|
||||||
|
This accepts TGS as a bounded CPU shadow candidate. It does not yet make TGS a
|
||||||
|
navigation authority or prove that its occupied/ground interpretation is
|
||||||
|
correct for vegetation, terrain, gaps or the future vehicle envelope.
|
||||||
|
|
||||||
|
## Frozen identity
|
||||||
|
|
||||||
|
| Item | Identity |
|
||||||
|
| --- | --- |
|
||||||
|
| LAB result | `m49-tgs-full-shadow-ef98de7db7596d48e8c8c0549ce68e6704ee03e87c8c4bcf1e3e748b7ccb032e` |
|
||||||
|
| Worker run | `Worker 006 / ravnoves00-full-001` |
|
||||||
|
| Mission Core revision used by Worker | `40c850b167dda366d8aa45d828520168affaf9fd` |
|
||||||
|
| Deterministic Worker artifact | `5e0ea16c7a5cc760463836718b0cd8b0006ffc4b202e5f706a20a86ef2f912ab` |
|
||||||
|
| Source pack SHA-256 | `0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944` |
|
||||||
|
| TGS config SHA-256 | `c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c` |
|
||||||
|
| Linked visual result | `m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324` |
|
||||||
|
| Linked semantic result | `e47-semantic-slam-f8c53ba12e9719856195890c7dcd2ba6c114434674159f948064368012e6b3bc` |
|
||||||
|
|
||||||
|
The profile is a `0.45 m`, `12 m` radius, `1 s` causal rolling
|
||||||
|
`map-gravity-local` costmap. The four states remain separate:
|
||||||
|
`GROUND_SUPPORT`, `NONGROUND_OCCUPIED`, `UNKNOWN_REJECTED` and `UNOBSERVED`.
|
||||||
|
|
||||||
|
## Full-run result
|
||||||
|
|
||||||
|
| Measurement | Result |
|
||||||
|
| --- | ---: |
|
||||||
|
| Timeline | `4,489 / 4,489` frames |
|
||||||
|
| LiDAR available / missing | `3,928 / 561` |
|
||||||
|
| Recorded duration / effective rate | `448.623 s / 10.003945 FPS` |
|
||||||
|
| Eligible points | `63,646,163` |
|
||||||
|
| Ground / non-ground / rejected | `16,579,467 / 47,046,815 / 19,881` |
|
||||||
|
| Unaccounted points | `0` |
|
||||||
|
| TGS p50 / p95 / p99 / max | `1.190 / 1.694 / 2.08973 / 9.106 ms` |
|
||||||
|
| Completion age p50 / p95 / p99 / max | `17.920792 / 29.140775 / 41.428455 / 659.980295 ms` |
|
||||||
|
| Capacity drops | `0` |
|
||||||
|
| Worker wrapper wall time | `569.401084 s` |
|
||||||
|
|
||||||
|
All formal source-paced, point-accounting and capacity gates passed. The
|
||||||
|
completion-age maximum is retained as an outlier; the accepted gate is the
|
||||||
|
measured `p99 = 41.43 ms`, not the maximum.
|
||||||
|
|
||||||
|
## Fail-closed behavior
|
||||||
|
|
||||||
|
The `561` timeline frames without a LiDAR sample are not removed, interpolated
|
||||||
|
or copied from the preceding frame. Each is emitted as a complete `2,244`-cell
|
||||||
|
costmap with all states explicitly `UNOBSERVED`, zero occupied cells and zero
|
||||||
|
source points. This preserves chronology without inventing free space.
|
||||||
|
|
||||||
|
## Canonical LAB publication and UI acceptance
|
||||||
|
|
||||||
|
The immutable result is published on the canonical Mission Core service at
|
||||||
|
port `8000`. The LAB reuses the recorded camera timeline and exposes independent
|
||||||
|
`SOURCE POINTS`, `LOCAL SLAM`, `TGS COSTMAP`, `SEMANTICS`, `3D` and `PLAN`
|
||||||
|
controls. The semantic mask is the exact E47 full-route archive bound to the
|
||||||
|
same M4 camera result; it remains diagnostic and does not change TGS states.
|
||||||
|
|
||||||
|
The first implementation fetched one large JSON frame at a time. Browser QA
|
||||||
|
showed that this was only intermittently exact at `1×`. The published viewer
|
||||||
|
therefore uses immutable chunks of `24` spatial frames and prefetches the next
|
||||||
|
chunk. A final `1×` browser acceptance sampled twenty consecutive positions
|
||||||
|
across chunk boundaries: all twenty displayed the exact TGS frame and none
|
||||||
|
showed the loading placeholder. A real missing-LiDAR frame was separately
|
||||||
|
accepted with `0 source points`, `2,244 unobserved`, `0 occupied` and no retained
|
||||||
|
previous cloud.
|
||||||
|
|
||||||
|
A follow-up playback acceptance fixed a UI lifecycle defect that remounted the
|
||||||
|
Three.js scene while the next TGS frame crossed the React buffer boundary. The
|
||||||
|
scene now retains the last sealed classified frame until the exact next frame
|
||||||
|
arrives, so orbit controls and the operator-selected view survive continuous
|
||||||
|
playback. Twelve consecutive `250 ms` playback samples kept one mounted 3D
|
||||||
|
canvas, a visible bounded `LOCAL SLAM` surface and the synchronized E47 video
|
||||||
|
mask. Pausing is no longer required to rotate, pan or zoom the 3D view.
|
||||||
|
|
||||||
|
## Sealed evidence
|
||||||
|
|
||||||
|
| File | Bytes | SHA-256 |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| `costmap-states.npy` | `10,073,444` | `4173f8b6b743755d6d6cbd863c433fe44853974bdd4898002e08574a65783a1a` |
|
||||||
|
| `costmap-z-bounds-m.npy` | `80,586,656` | `f8d68b5ca09860ef857a22e15e114bbeabde32492d5c821ee59dfa62ae40373a` |
|
||||||
|
| `frames.ndjson` | `1,178,262` | `878e20806d55cdc1fa16692397e11a0e56af760a83c173c8fc73319a39b0e779` |
|
||||||
|
| `worker-summary.json` | `1,076` | `487d4be6f3d724bca647aa2e48ba89e7ffd0c78f1bfc5e7e5a01e1db82379dc7` |
|
||||||
|
|
||||||
|
## Next gate
|
||||||
|
|
||||||
|
Attach this unchanged TGS stage to the realtime world-state graph in shadow
|
||||||
|
mode alongside the frozen camera semantic-risk provider. Measure complete-graph
|
||||||
|
FPS, end-to-end latency, CPU/GPU/VRAM and queue drops against the accepted
|
||||||
|
baseline. Keep navigation authority off. In parallel, review the full camera +
|
||||||
|
source cloud + TGS timeline for false occupied carpets, missed compact
|
||||||
|
obstacles, vegetation and usable gaps. Only the combination of acceptable
|
||||||
|
visual behavior and acceptable integrated-graph regression can promote the
|
||||||
|
candidate beyond shadow.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish the verified Worker TGS pack as immutable Mission Core LAB evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_fail_closed import (
|
||||||
|
M49TgsFailClosedError,
|
||||||
|
seal_m49_tgs_fail_closed,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--destination-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--profile", type=Path, required=True)
|
||||||
|
parser.add_argument("--linked-visual-result-id", required=True)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
try:
|
||||||
|
result = seal_m49_tgs_fail_closed(
|
||||||
|
source_root=arguments.source_root,
|
||||||
|
destination_root=arguments.destination_root,
|
||||||
|
profile_path=arguments.profile,
|
||||||
|
linked_visual_result_id=arguments.linked_visual_result_id,
|
||||||
|
)
|
||||||
|
except (M49TgsFailClosedError, OSError, ValueError) as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
print(result.result_id)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseRoot,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
|
||||||
|
[string]$RunId,
|
||||||
|
[string]$SourcePackPath = "D:\NDC_MISSIONCORE\runtime\derived\e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b\lidar-pack.npz",
|
||||||
|
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m49-tgs-full-shadow"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
$TravelImageTag = "ndc/mission-core-m49-t3-travel:20260826"
|
||||||
|
$TravelImageId = "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f"
|
||||||
|
$ParityImageTag = "ndc-mission-core-m48t-upstream-parity:1.9.4-cu130"
|
||||||
|
$ParityImageId = "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0"
|
||||||
|
|
||||||
|
function Assert-LastExitCode([string]$Operation) {
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-DDirectory([string]$Path, [string]$Label, [bool]$Create) {
|
||||||
|
if ($Create -and -not (Test-Path -LiteralPath $Path)) {
|
||||||
|
$null = New-Item -ItemType Directory -Path $Path
|
||||||
|
}
|
||||||
|
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
|
||||||
|
if (
|
||||||
|
-not $item.PSIsContainer -or
|
||||||
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
||||||
|
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
|
||||||
|
) { throw "$Label must be a real D: directory" }
|
||||||
|
return $item.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-DFile([string]$Path, [string]$Label) {
|
||||||
|
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
|
||||||
|
if (
|
||||||
|
$item.PSIsContainer -or
|
||||||
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
||||||
|
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
|
||||||
|
) { throw "$Label must be a real D: file" }
|
||||||
|
return $item.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-ToDockerPath([string]$Path) { return ($Path -replace "\\", "/") }
|
||||||
|
|
||||||
|
function Get-Container([string]$Name) {
|
||||||
|
$rows = @(((& docker inspect $Name) | ConvertFrom-Json))
|
||||||
|
Assert-LastExitCode "Docker inspection for $Name"
|
||||||
|
if ($rows.Count -ne 1) { throw "Container identity for $Name is not unique" }
|
||||||
|
return $rows[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-Image([string]$Tag, [string]$ExpectedId) {
|
||||||
|
$rows = @(((& docker image inspect $Tag) | ConvertFrom-Json))
|
||||||
|
Assert-LastExitCode "Docker image inspection for $Tag"
|
||||||
|
if ($rows.Count -ne 1 -or [string]$rows[0].Id -cne $ExpectedId) {
|
||||||
|
throw "Pinned image identity changed for $Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-ExactContainer([string]$Name) {
|
||||||
|
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$Name$") {
|
||||||
|
& docker rm --force $Name *> $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") { throw "M49 TGS full shadow is pinned to Worker 006" }
|
||||||
|
$release = Resolve-DDirectory $ReleaseRoot "M49 TGS full-shadow release" $false
|
||||||
|
$payload = Resolve-DDirectory (Join-Path $release "payload") "M49 TGS full-shadow payload" $false
|
||||||
|
$sourcePack = Resolve-DFile $SourcePackPath "RAVNOVES00 source pack"
|
||||||
|
$output = Resolve-DDirectory $OutputRoot "M49 TGS full-shadow output root" $true
|
||||||
|
$runCandidate = Join-Path $output $RunId
|
||||||
|
if (Test-Path -LiteralPath $runCandidate) { throw "M49 TGS full-shadow output already exists" }
|
||||||
|
$null = New-Item -ItemType Directory -Path $runCandidate
|
||||||
|
$runOutput = Resolve-DDirectory $runCandidate "M49 TGS full-shadow run output" $false
|
||||||
|
|
||||||
|
$releaseDocument = Get-Content -LiteralPath (Join-Path $payload "release.json") -Raw | ConvertFrom-Json
|
||||||
|
if (
|
||||||
|
$releaseDocument.schema_version -cne "missioncore.m49-tgs-full-shadow-worker-release/v1" -or
|
||||||
|
$releaseDocument.worker_id -cne "worker-006" -or
|
||||||
|
$releaseDocument.candidate_id -cne "travel-tgs-full-shadow"
|
||||||
|
) { throw "M49 TGS full-shadow release contract changed" }
|
||||||
|
foreach ($property in $releaseDocument.files.PSObject.Properties) {
|
||||||
|
$path = Join-Path $payload $property.Name
|
||||||
|
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash.ToLowerInvariant()
|
||||||
|
if ($actual -cne [string]$property.Value.sha256) {
|
||||||
|
throw "M49 TGS full-shadow payload digest changed: $($property.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sourcePackSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $sourcePack).Hash.ToLowerInvariant()
|
||||||
|
if ($sourcePackSha -cne "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944") {
|
||||||
|
throw "RAVNOVES00 source pack digest changed"
|
||||||
|
}
|
||||||
|
$os = Get-CimInstance Win32_OperatingSystem
|
||||||
|
$freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB
|
||||||
|
if ($freeMemoryGiB -lt 24.0) {
|
||||||
|
throw ("M49 TGS full shadow requires 24 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB)
|
||||||
|
}
|
||||||
|
$tritonBefore = Get-Container "ndc-mission-core-triton"
|
||||||
|
if (-not $tritonBefore.State.Running -or $tritonBefore.State.Health.Status -cne "healthy") {
|
||||||
|
throw "Canonical Mission Core Triton must remain healthy during M49 TGS full shadow"
|
||||||
|
}
|
||||||
|
Assert-Image $TravelImageTag $TravelImageId
|
||||||
|
Assert-Image $ParityImageTag $ParityImageId
|
||||||
|
|
||||||
|
$prepareName = "ndc-mission-core-m49-tgs-full-prepare-$RunId"
|
||||||
|
$runName = "ndc-mission-core-m49-tgs-full-run-$RunId"
|
||||||
|
$analyzeName = "ndc-mission-core-m49-tgs-full-analyze-$RunId"
|
||||||
|
foreach ($name in @($prepareName, $runName, $analyzeName)) {
|
||||||
|
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$name$") {
|
||||||
|
throw "M49 TGS full-shadow container name already exists: $name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$started = [DateTimeOffset]::UtcNow
|
||||||
|
try {
|
||||||
|
& docker run --rm --name $prepareName --network none --cpus 8 --memory 16g `
|
||||||
|
--entrypoint python3 `
|
||||||
|
--volume ((Convert-ToDockerPath $sourcePack) + ":/source/lidar-pack.npz:ro") `
|
||||||
|
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
|
||||||
|
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
|
||||||
|
$ParityImageTag /release/prepare_tgs_full_shadow_inputs.py `
|
||||||
|
--source-pack /source/lidar-pack.npz `
|
||||||
|
--config /release/m49-tgs-full-shadow-v1.json `
|
||||||
|
--output-root /tgs/inputs
|
||||||
|
Assert-LastExitCode "M49 TGS full-shadow input preparation"
|
||||||
|
|
||||||
|
& docker run --rm --name $runName --network none --cpus 16 --memory 24g `
|
||||||
|
--entrypoint /bin/bash `
|
||||||
|
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
|
||||||
|
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
|
||||||
|
$TravelImageTag /release/run_tgs_full_shadow.sh
|
||||||
|
Assert-LastExitCode "M49 source-paced TGS full-shadow run"
|
||||||
|
|
||||||
|
& docker run --rm --name $analyzeName --network none --cpus 8 --memory 16g `
|
||||||
|
--entrypoint python3 `
|
||||||
|
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
|
||||||
|
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
|
||||||
|
$ParityImageTag /release/build_tgs_full_shadow_evidence.py `
|
||||||
|
--run-root /tgs `
|
||||||
|
--config /release/m49-tgs-full-shadow-v1.json `
|
||||||
|
--output-root /tgs/evidence
|
||||||
|
Assert-LastExitCode "M49 TGS full-shadow evidence analysis"
|
||||||
|
} finally {
|
||||||
|
foreach ($name in @($prepareName, $runName, $analyzeName)) { Remove-ExactContainer $name }
|
||||||
|
}
|
||||||
|
$completed = [DateTimeOffset]::UtcNow
|
||||||
|
$resultPath = Join-Path $runOutput "evidence\result.json"
|
||||||
|
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) { throw "M49 TGS full-shadow result is missing" }
|
||||||
|
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
|
||||||
|
if (
|
||||||
|
$result.timeline.frame_count -ne 4489 -or
|
||||||
|
$result.timeline.available_lidar_frame_count -ne 3928 -or
|
||||||
|
$result.timeline.missing_lidar_frame_count -ne 561 -or
|
||||||
|
$result.point_accounting.unaccounted -ne 0
|
||||||
|
) { throw "M49 TGS full-shadow structural acceptance failed" }
|
||||||
|
$tritonAfter = Get-Container "ndc-mission-core-triton"
|
||||||
|
if (
|
||||||
|
-not $tritonAfter.State.Running -or
|
||||||
|
$tritonAfter.State.Health.Status -cne "healthy" -or
|
||||||
|
[string]$tritonAfter.Id -cne [string]$tritonBefore.Id
|
||||||
|
) { throw "Canonical Mission Core Triton changed during M49 TGS full shadow" }
|
||||||
|
$summary = [ordered]@{
|
||||||
|
schema_version = "missioncore.m49-tgs-full-shadow-worker-summary/v1"
|
||||||
|
worker_id = "worker-006"
|
||||||
|
run_id = $RunId
|
||||||
|
code_revision = [string]$releaseDocument.code_revision
|
||||||
|
source_pack_sha256 = $sourcePackSha
|
||||||
|
travel_image_id = $TravelImageId
|
||||||
|
parity_image_id = $ParityImageId
|
||||||
|
started_utc = $started.ToString("o")
|
||||||
|
wall_seconds = [math]::Round(($completed - $started).TotalSeconds, 6)
|
||||||
|
free_memory_gib_before = [math]::Round($freeMemoryGiB, 6)
|
||||||
|
canonical_triton_id = [string]$tritonAfter.Id
|
||||||
|
canonical_triton_health = [string]$tritonAfter.State.Health.Status
|
||||||
|
result_status = [string]$result.status
|
||||||
|
all_timeline_frames_accounted = $true
|
||||||
|
all_eligible_points_accounted = $true
|
||||||
|
aos_used = $false
|
||||||
|
gpu_requested = $false
|
||||||
|
integrated_graph_performance_accepted = $false
|
||||||
|
navigation_or_actuation_allowed = $false
|
||||||
|
}
|
||||||
|
$summary | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath (Join-Path $runOutput "worker-summary.json") -Encoding utf8
|
||||||
|
$summary | ConvertTo-Json -Depth 3
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseRoot,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
|
||||||
|
[string]$RunId
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$taskName = "MissionCore-M49TgsFullShadow"
|
||||||
|
$release = (Resolve-Path -LiteralPath $ReleaseRoot).Path
|
||||||
|
$runner = Join-Path $release "payload\Invoke-M49TgsFullShadow.ps1"
|
||||||
|
if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) { throw "M49 TGS full-shadow runner is missing" }
|
||||||
|
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||||
|
if ($existing -and $existing.State -eq "Running") { throw "$taskName is already running" }
|
||||||
|
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$arguments = @(
|
||||||
|
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", "`"$runner`"", "-ReleaseRoot", "`"$release`"", "-RunId", "`"$RunId`""
|
||||||
|
) -join " "
|
||||||
|
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $release
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries `
|
||||||
|
-StartWhenAvailable `
|
||||||
|
-ExecutionTimeLimit ([TimeSpan]::FromHours(2))
|
||||||
|
Register-ScheduledTask `
|
||||||
|
-TaskName $taskName `
|
||||||
|
-Action $action `
|
||||||
|
-Principal $principal `
|
||||||
|
-Trigger $trigger `
|
||||||
|
-Settings $settings `
|
||||||
|
-Description "One-shot CPU-only source-paced TGS full shadow." `
|
||||||
|
-Force | Out-Null
|
||||||
|
Start-ScheduledTask -TaskName $taskName
|
||||||
|
[pscustomobject]@{
|
||||||
|
task_name = $taskName
|
||||||
|
run_id = $RunId
|
||||||
|
release_root = $release
|
||||||
|
state = (Get-ScheduledTask -TaskName $taskName).State.ToString()
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build mmap-friendly evidence for the complete source-paced TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from build_tgs_fail_closed_evidence import costmap_grid
|
||||||
|
|
||||||
|
|
||||||
|
class FullShadowError(RuntimeError):
|
||||||
|
"""The complete TGS shadow or its fail-closed contract is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def load_xyzi(path: Path) -> np.ndarray:
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise FullShadowError(f"sealed input is unavailable: {path.name}")
|
||||||
|
values = np.fromfile(path, dtype=np.float32)
|
||||||
|
if values.size % 4:
|
||||||
|
raise FullShadowError(f"sealed XYZI shape changed: {path.name}")
|
||||||
|
result = values.reshape(-1, 4)
|
||||||
|
if not np.isfinite(result).all():
|
||||||
|
raise FullShadowError(f"sealed XYZI is non-finite: {path.name}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def classify_exact_input(
|
||||||
|
native: np.ndarray, ground: np.ndarray, nonground: np.ndarray
|
||||||
|
) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
ranges = np.linalg.norm(native[:, :2].astype(np.float64), axis=1)
|
||||||
|
points = np.ascontiguousarray(native[(ranges > 1.0) & (ranges < 80.0), :3])
|
||||||
|
output = np.ascontiguousarray(np.concatenate((ground[:, :3], nonground[:, :3]), axis=0))
|
||||||
|
output_states = np.concatenate(
|
||||||
|
(np.ones(ground.shape[0], dtype=np.uint8), np.full(nonground.shape[0], 2, dtype=np.uint8))
|
||||||
|
)
|
||||||
|
key_dtype = np.dtype((np.void, 12))
|
||||||
|
input_keys = points.view(key_dtype).reshape(-1)
|
||||||
|
output_keys = output.view(key_dtype).reshape(-1)
|
||||||
|
input_order = np.argsort(input_keys, kind="stable")
|
||||||
|
output_order = np.argsort(output_keys, kind="stable")
|
||||||
|
sorted_input = input_keys[input_order]
|
||||||
|
sorted_output = output_keys[output_order]
|
||||||
|
positions = np.searchsorted(sorted_input, sorted_output, side="left")
|
||||||
|
if sorted_output.size:
|
||||||
|
group_starts = np.r_[0, np.flatnonzero(sorted_output[1:] != sorted_output[:-1]) + 1]
|
||||||
|
group_lengths = np.diff(np.r_[group_starts, sorted_output.size])
|
||||||
|
occurrence = np.arange(sorted_output.size) - np.repeat(group_starts, group_lengths)
|
||||||
|
targets = positions + occurrence
|
||||||
|
if (
|
||||||
|
np.any(targets >= sorted_input.size)
|
||||||
|
or np.any(sorted_input[targets] != sorted_output)
|
||||||
|
or np.unique(targets).size != targets.size
|
||||||
|
):
|
||||||
|
raise FullShadowError("TGS output is not a multiset subset of its exact input")
|
||||||
|
else:
|
||||||
|
targets = np.empty(0, dtype=np.int64)
|
||||||
|
sorted_states = np.full(points.shape[0], 3, dtype=np.uint8)
|
||||||
|
sorted_states[targets] = output_states[output_order]
|
||||||
|
states = np.empty_like(sorted_states)
|
||||||
|
states[input_order] = sorted_states
|
||||||
|
return points, states
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(
|
||||||
|
points: np.ndarray,
|
||||||
|
states: np.ndarray,
|
||||||
|
grid: np.ndarray,
|
||||||
|
cell_size_m: float,
|
||||||
|
) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
minimum_ix = int(np.min(grid[:, 0]))
|
||||||
|
maximum_ix = int(np.max(grid[:, 0]))
|
||||||
|
minimum_iy = int(np.min(grid[:, 1]))
|
||||||
|
maximum_iy = int(np.max(grid[:, 1]))
|
||||||
|
lookup = np.full(
|
||||||
|
(maximum_ix - minimum_ix + 1, maximum_iy - minimum_iy + 1), -1, dtype=np.int32
|
||||||
|
)
|
||||||
|
lookup[
|
||||||
|
grid[:, 0].astype(np.int32) - minimum_ix,
|
||||||
|
grid[:, 1].astype(np.int32) - minimum_iy,
|
||||||
|
] = np.arange(grid.shape[0], dtype=np.int32)
|
||||||
|
cell_xy = np.floor(points[:, :2] / cell_size_m).astype(np.int32)
|
||||||
|
inside = (
|
||||||
|
(cell_xy[:, 0] >= minimum_ix)
|
||||||
|
& (cell_xy[:, 0] <= maximum_ix)
|
||||||
|
& (cell_xy[:, 1] >= minimum_iy)
|
||||||
|
& (cell_xy[:, 1] <= maximum_iy)
|
||||||
|
)
|
||||||
|
point_indices = np.flatnonzero(inside)
|
||||||
|
cell_indices = lookup[
|
||||||
|
cell_xy[inside, 0] - minimum_ix, cell_xy[inside, 1] - minimum_iy
|
||||||
|
]
|
||||||
|
valid = cell_indices >= 0
|
||||||
|
point_indices = point_indices[valid]
|
||||||
|
cell_indices = cell_indices[valid]
|
||||||
|
cell_states = np.zeros(grid.shape[0], dtype=np.uint8)
|
||||||
|
selected_states = states[point_indices]
|
||||||
|
ground_cells = np.zeros(grid.shape[0], dtype=np.uint8)
|
||||||
|
rejected_cells = np.zeros(grid.shape[0], dtype=np.uint8)
|
||||||
|
nonground_cells = np.zeros(grid.shape[0], dtype=np.uint8)
|
||||||
|
np.maximum.at(ground_cells, cell_indices, (selected_states == 1).astype(np.uint8))
|
||||||
|
np.maximum.at(rejected_cells, cell_indices, (selected_states == 3).astype(np.uint8))
|
||||||
|
np.maximum.at(nonground_cells, cell_indices, (selected_states == 2).astype(np.uint8))
|
||||||
|
cell_states[ground_cells > 0] = 1
|
||||||
|
cell_states[rejected_cells > 0] = 3
|
||||||
|
cell_states[nonground_cells > 0] = 2
|
||||||
|
minimum_z = np.full(grid.shape[0], np.inf, dtype=np.float32)
|
||||||
|
maximum_z = np.full(grid.shape[0], -np.inf, dtype=np.float32)
|
||||||
|
np.minimum.at(minimum_z, cell_indices, points[point_indices, 2])
|
||||||
|
np.maximum.at(maximum_z, cell_indices, points[point_indices, 2])
|
||||||
|
z_bounds = np.column_stack((minimum_z, maximum_z)).astype(np.float32, copy=False)
|
||||||
|
z_bounds[~np.isfinite(z_bounds)] = np.nan
|
||||||
|
return cell_states, z_bounds
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: np.ndarray, value: float) -> float:
|
||||||
|
return float(np.percentile(values.astype(np.float64), value)) if values.size else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def build(run_root: Path, config_path: Path, output_root: Path) -> dict[str, object]:
|
||||||
|
if output_root.exists():
|
||||||
|
raise FullShadowError("full-shadow evidence output already exists")
|
||||||
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
if (
|
||||||
|
config.get("schema_version") != "missioncore.m49-tgs-full-shadow-profile/v1"
|
||||||
|
or config.get("invariants", {}).get("aos_allowed") is not False
|
||||||
|
or config.get("invariants", {}).get("gpu_allowed") is not False
|
||||||
|
or config.get("invariants", {}).get("missing_lidar_means_unobserved") is not True
|
||||||
|
):
|
||||||
|
raise FullShadowError("full-shadow profile changed")
|
||||||
|
manifest_path = run_root / "inputs" / "input-manifest.json"
|
||||||
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
records = manifest.get("records", [])
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != "missioncore.m49-tgs-full-shadow-input/v1"
|
||||||
|
or manifest.get("source_pack_sha256") != config["source"]["source_pack_sha256"]
|
||||||
|
or manifest.get("config_sha256") != sha256_file(config_path)
|
||||||
|
or manifest.get("future_frames_used") is not False
|
||||||
|
or len(records) != 4489
|
||||||
|
or sum(bool(row["sample_available"]) for row in records) != 3928
|
||||||
|
):
|
||||||
|
raise FullShadowError("full-shadow input manifest changed")
|
||||||
|
with (run_root / "tgs-full-timing.tsv").open("r", encoding="utf-8", newline="") as stream:
|
||||||
|
timing_rows = list(csv.DictReader(stream, delimiter="\t"))
|
||||||
|
if len(timing_rows) != 4489:
|
||||||
|
raise FullShadowError("full-shadow timing frame accounting changed")
|
||||||
|
|
||||||
|
cell_size = float(config["costmap"]["cell_size_m"])
|
||||||
|
radius = float(config["costmap"]["radius_m"])
|
||||||
|
grid = costmap_grid(radius, cell_size)
|
||||||
|
output_root.mkdir(parents=True)
|
||||||
|
np.save(output_root / "costmap-cell-indices-xy.npy", grid[:, :2].astype(np.int32))
|
||||||
|
np.save(output_root / "costmap-cell-centers-xy-m.npy", grid[:, 2:].astype(np.float32))
|
||||||
|
states_out = np.lib.format.open_memmap(
|
||||||
|
output_root / "costmap-states.npy", mode="w+", dtype=np.uint8, shape=(4489, grid.shape[0])
|
||||||
|
)
|
||||||
|
z_out = np.lib.format.open_memmap(
|
||||||
|
output_root / "costmap-z-bounds-m.npy",
|
||||||
|
mode="w+",
|
||||||
|
dtype=np.float32,
|
||||||
|
shape=(4489, grid.shape[0], 2),
|
||||||
|
)
|
||||||
|
z_out[:] = np.nan
|
||||||
|
summaries: list[dict[str, object]] = []
|
||||||
|
eligible_total = ground_total = nonground_total = rejected_total = 0
|
||||||
|
available_seen = 0
|
||||||
|
for frame_index, (record, timing) in enumerate(zip(records, timing_rows, strict=True)):
|
||||||
|
if int(timing["timeline_frame_index"]) != frame_index:
|
||||||
|
raise FullShadowError("full-shadow timing order changed")
|
||||||
|
available = bool(record["sample_available"])
|
||||||
|
if not available:
|
||||||
|
if int(timing["sample_available"]) != 0:
|
||||||
|
raise FullShadowError("missing LiDAR frame was processed")
|
||||||
|
states_out[frame_index] = 0
|
||||||
|
summaries.append(
|
||||||
|
{
|
||||||
|
"timeline_frame_index": frame_index,
|
||||||
|
"source_frame_index": int(record["source_frame_index"]),
|
||||||
|
"session_seconds": float(record["session_seconds"]),
|
||||||
|
"sample_available": False,
|
||||||
|
"eligible_point_count": 0,
|
||||||
|
"ground_point_count": 0,
|
||||||
|
"nonground_point_count": 0,
|
||||||
|
"rejected_point_count": 0,
|
||||||
|
"occupied_cell_count": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
available_seen += 1
|
||||||
|
native_path = run_root / "inputs" / str(record["relative_path"])
|
||||||
|
if sha256_file(native_path) != record["sha256"]:
|
||||||
|
raise FullShadowError("sealed gravity-aligned full-shadow input changed")
|
||||||
|
output = run_root / "outputs" / "causal_rolling_1s"
|
||||||
|
ground = load_xyzi(output / f"{frame_index}_ground.bin")
|
||||||
|
nonground = load_xyzi(output / f"{frame_index}_nonground.bin")
|
||||||
|
points, point_states = classify_exact_input(load_xyzi(native_path), ground, nonground)
|
||||||
|
cell_states, z_bounds = rasterize(points, point_states, grid, cell_size)
|
||||||
|
states_out[frame_index] = cell_states
|
||||||
|
z_out[frame_index] = z_bounds
|
||||||
|
ground_count = int(np.count_nonzero(point_states == 1))
|
||||||
|
nonground_count = int(np.count_nonzero(point_states == 2))
|
||||||
|
rejected_count = int(np.count_nonzero(point_states == 3))
|
||||||
|
eligible_total += int(points.shape[0])
|
||||||
|
ground_total += ground_count
|
||||||
|
nonground_total += nonground_count
|
||||||
|
rejected_total += rejected_count
|
||||||
|
summaries.append(
|
||||||
|
{
|
||||||
|
"timeline_frame_index": frame_index,
|
||||||
|
"source_frame_index": int(record["source_frame_index"]),
|
||||||
|
"session_seconds": float(record["session_seconds"]),
|
||||||
|
"sample_available": True,
|
||||||
|
"eligible_point_count": int(points.shape[0]),
|
||||||
|
"ground_point_count": ground_count,
|
||||||
|
"nonground_point_count": nonground_count,
|
||||||
|
"rejected_point_count": rejected_count,
|
||||||
|
"occupied_cell_count": int(np.count_nonzero(cell_states == 2)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
states_out.flush()
|
||||||
|
z_out.flush()
|
||||||
|
if available_seen != 3928 or eligible_total != ground_total + nonground_total + rejected_total:
|
||||||
|
raise FullShadowError("full-shadow eligible point accounting failed")
|
||||||
|
frames_path = output_root / "frames.ndjson"
|
||||||
|
frames_path.write_text(
|
||||||
|
"".join(json.dumps(row, sort_keys=True) + "\n" for row in summaries), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
available_timings = [row for row in timing_rows if int(row["sample_available"]) == 1]
|
||||||
|
tgs_ms = np.asarray([float(row["tgs_ms"]) for row in available_timings])
|
||||||
|
completion_ms = np.asarray([float(row["completion_age_ms"]) for row in timing_rows])
|
||||||
|
capacity_drops = sum(int(row["capacity_drop"]) for row in timing_rows)
|
||||||
|
duration = float(records[-1]["session_seconds"]) - float(records[0]["session_seconds"])
|
||||||
|
effective_fps = (len(records) - 1) / duration
|
||||||
|
thresholds = config["acceptance"]
|
||||||
|
acceptance = {
|
||||||
|
"minimum_effective_timeline_fps": effective_fps >= float(thresholds["minimum_effective_timeline_fps"]),
|
||||||
|
"candidate_stage_p95_ms": percentile(tgs_ms, 95) <= float(thresholds["candidate_stage_p95_ms_max"]),
|
||||||
|
"candidate_stage_p99_ms": percentile(tgs_ms, 99) <= float(thresholds["candidate_stage_p99_ms_max"]),
|
||||||
|
"completion_age_p99_ms": percentile(completion_ms, 99) <= float(thresholds["completion_age_p99_ms_max"]),
|
||||||
|
"capacity_drop_count": capacity_drops <= int(thresholds["capacity_drop_count_max"]),
|
||||||
|
"all_frames_accounted": len(summaries) == 4489 and available_seen == 3928,
|
||||||
|
"all_eligible_points_accounted": eligible_total == ground_total + nonground_total + rejected_total,
|
||||||
|
}
|
||||||
|
files = {}
|
||||||
|
for path in sorted(output_root.iterdir()):
|
||||||
|
if path.is_file() and path.name != "result.json":
|
||||||
|
files[path.name] = {"bytes": path.stat().st_size, "sha256": sha256_file(path)}
|
||||||
|
result = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-result/v1",
|
||||||
|
"status": "passed" if all(acceptance.values()) else "failed",
|
||||||
|
"config_sha256": sha256_file(config_path),
|
||||||
|
"source_pack_sha256": manifest["source_pack_sha256"],
|
||||||
|
"input_manifest_sha256": sha256_file(manifest_path),
|
||||||
|
"timeline": {
|
||||||
|
"frame_count": 4489,
|
||||||
|
"available_lidar_frame_count": 3928,
|
||||||
|
"missing_lidar_frame_count": 561,
|
||||||
|
"duration_seconds": duration,
|
||||||
|
"effective_fps": effective_fps,
|
||||||
|
},
|
||||||
|
"costmap": {
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"cell_size_m": cell_size,
|
||||||
|
"radius_m": radius,
|
||||||
|
"cell_count": int(grid.shape[0]),
|
||||||
|
},
|
||||||
|
"point_accounting": {
|
||||||
|
"eligible": eligible_total,
|
||||||
|
"ground": ground_total,
|
||||||
|
"nonground": nonground_total,
|
||||||
|
"rejected": rejected_total,
|
||||||
|
"unaccounted": eligible_total - ground_total - nonground_total - rejected_total,
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"candidate_tgs_ms": {
|
||||||
|
"p50": percentile(tgs_ms, 50),
|
||||||
|
"p95": percentile(tgs_ms, 95),
|
||||||
|
"p99": percentile(tgs_ms, 99),
|
||||||
|
"max": float(np.max(tgs_ms)),
|
||||||
|
},
|
||||||
|
"completion_age_ms": {
|
||||||
|
"p50": percentile(completion_ms, 50),
|
||||||
|
"p95": percentile(completion_ms, 95),
|
||||||
|
"p99": percentile(completion_ms, 99),
|
||||||
|
"max": float(np.max(completion_ms)),
|
||||||
|
},
|
||||||
|
"capacity_drop_count": capacity_drops,
|
||||||
|
},
|
||||||
|
"acceptance": acceptance,
|
||||||
|
"files": files,
|
||||||
|
"authority": {
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"traversability_accepted": False,
|
||||||
|
"realtime_accepted": bool(all(acceptance.values())),
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
"navigation_or_actuation_allowed": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(output_root / "result.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--run-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--config", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
result = build(arguments.run_root, arguments.config, arguments.output_root)
|
||||||
|
print(json.dumps({"status": result["status"], **result["performance"]}, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Prepare every available RAVNOVES00 frame for the source-paced TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from prepare_tgs_fail_closed_inputs import (
|
||||||
|
EXPECTED_ARRAYS,
|
||||||
|
SOURCE_PACK_SHA256,
|
||||||
|
TgsInputError,
|
||||||
|
_frame_points,
|
||||||
|
_validate_source,
|
||||||
|
gravity_local_xyzi,
|
||||||
|
sha256_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
FULL_SCHEMA = "missioncore.m49-tgs-full-shadow-profile/v1"
|
||||||
|
INPUT_SCHEMA = "missioncore.m49-tgs-full-shadow-input/v1"
|
||||||
|
TIMELINE_FRAME_COUNT = 4_489
|
||||||
|
AVAILABLE_LIDAR_FRAME_COUNT = 3_928
|
||||||
|
|
||||||
|
|
||||||
|
def _bytes_sha256(content: bytes) -> str:
|
||||||
|
return hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def prepare(source_pack: Path, config_path: Path, output_root: Path) -> dict[str, object]:
|
||||||
|
if output_root.exists():
|
||||||
|
raise TgsInputError("TGS full-shadow input output root already exists")
|
||||||
|
if sha256_file(source_pack) != SOURCE_PACK_SHA256:
|
||||||
|
raise TgsInputError("RAVNOVES00 lidar-pack digest changed")
|
||||||
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
source = config.get("source", {})
|
||||||
|
profile = config.get("profile", {})
|
||||||
|
invariants = config.get("invariants", {})
|
||||||
|
if (
|
||||||
|
config.get("schema_version") != FULL_SCHEMA
|
||||||
|
or source.get("source_pack_sha256") != SOURCE_PACK_SHA256
|
||||||
|
or source.get("expected_timeline_frames") != TIMELINE_FRAME_COUNT
|
||||||
|
or source.get("expected_available_lidar_frames") != AVAILABLE_LIDAR_FRAME_COUNT
|
||||||
|
or source.get("input_coordinate_frame")
|
||||||
|
!= "map-gravity-local-translation-only"
|
||||||
|
or profile.get("id") != "causal_rolling_1s"
|
||||||
|
or profile.get("missing_lidar_policy") != "all-cells-unobserved"
|
||||||
|
or invariants.get("lidar_orientation_applied_to_tgs_input") is not False
|
||||||
|
or invariants.get("future_frames_used") is not False
|
||||||
|
or invariants.get("missing_lidar_means_unobserved") is not True
|
||||||
|
):
|
||||||
|
raise TgsInputError("TGS full-shadow profile changed")
|
||||||
|
required = EXPECTED_ARRAYS | {"source_frame_indices", "pose_quaternions_map_from_lidar"}
|
||||||
|
with np.load(source_pack, allow_pickle=False) as archive:
|
||||||
|
if not required.issubset(archive.files):
|
||||||
|
raise TgsInputError("RAVNOVES00 lidar-pack members changed")
|
||||||
|
arrays = {name: archive[name] for name in required}
|
||||||
|
_validate_source(arrays)
|
||||||
|
if (
|
||||||
|
arrays["source_frame_indices"].shape != (TIMELINE_FRAME_COUNT,)
|
||||||
|
or arrays["source_frame_indices"].dtype != np.int64
|
||||||
|
or arrays["pose_quaternions_map_from_lidar"].shape != (TIMELINE_FRAME_COUNT, 4)
|
||||||
|
or arrays["pose_quaternions_map_from_lidar"].dtype != np.float64
|
||||||
|
or int(np.count_nonzero(arrays["sample_available"])) != AVAILABLE_LIDAR_FRAME_COUNT
|
||||||
|
):
|
||||||
|
raise TgsInputError("RAVNOVES00 full timeline contract changed")
|
||||||
|
|
||||||
|
sequence_root = output_root / "profiles" / "causal_rolling_1s" / "velodyne"
|
||||||
|
sequence_root.mkdir(parents=True)
|
||||||
|
seconds = arrays["session_seconds"]
|
||||||
|
availability = arrays["sample_available"]
|
||||||
|
positions = arrays["pose_positions_map"]
|
||||||
|
history_seconds = float(profile["history_seconds"])
|
||||||
|
local_radius_m = float(profile["local_radius_m"])
|
||||||
|
records: list[dict[str, object]] = []
|
||||||
|
schedule_rows = [
|
||||||
|
"timeline_frame_index\tsource_frame_index\tsession_seconds\tavailable_slot\tpoint_count"
|
||||||
|
]
|
||||||
|
available_slot = 0
|
||||||
|
for frame_index in range(TIMELINE_FRAME_COUNT):
|
||||||
|
base = {
|
||||||
|
"timeline_frame_index": frame_index,
|
||||||
|
"frame_index": int(arrays["frame_indices"][frame_index]),
|
||||||
|
"source_frame_index": int(arrays["source_frame_indices"][frame_index]),
|
||||||
|
"session_seconds": float(seconds[frame_index]),
|
||||||
|
"position_map_m": [float(value) for value in positions[frame_index]],
|
||||||
|
"sample_available": bool(availability[frame_index]),
|
||||||
|
}
|
||||||
|
if not bool(availability[frame_index]):
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
**base,
|
||||||
|
"available_slot": None,
|
||||||
|
"point_count": 0,
|
||||||
|
"contributing_frame_indices": [],
|
||||||
|
"relative_path": None,
|
||||||
|
"bytes": 0,
|
||||||
|
"sha256": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
schedule_rows.append(
|
||||||
|
f"{frame_index}\t{base['source_frame_index']}\t{seconds[frame_index]:.9f}\t-1\t0"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = int(np.searchsorted(seconds, seconds[frame_index] - history_seconds, side="left"))
|
||||||
|
contributors = tuple(
|
||||||
|
index for index in range(start, frame_index + 1) if bool(availability[index])
|
||||||
|
)
|
||||||
|
if not contributors or contributors[-1] != frame_index:
|
||||||
|
raise TgsInputError("causal full-shadow profile does not contain its current frame")
|
||||||
|
points_map = np.concatenate(
|
||||||
|
[_frame_points(arrays, index) for index in contributors], axis=0
|
||||||
|
)
|
||||||
|
relative_xy = points_map[:, :2].astype(np.float64) - positions[frame_index, :2]
|
||||||
|
points_map = points_map[np.linalg.norm(relative_xy, axis=1) <= local_radius_m]
|
||||||
|
native = gravity_local_xyzi(points_map, positions[frame_index])
|
||||||
|
if native.shape[0] == 0:
|
||||||
|
raise TgsInputError("available full-shadow frame produced an empty cloud")
|
||||||
|
content = np.ascontiguousarray(native).tobytes()
|
||||||
|
target = sequence_root / f"{available_slot:06d}.bin"
|
||||||
|
target.write_bytes(content)
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
**base,
|
||||||
|
"available_slot": available_slot,
|
||||||
|
"point_count": int(native.shape[0]),
|
||||||
|
"contributing_frame_indices": list(contributors),
|
||||||
|
"relative_path": target.relative_to(output_root).as_posix(),
|
||||||
|
"bytes": len(content),
|
||||||
|
"sha256": _bytes_sha256(content),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
schedule_rows.append(
|
||||||
|
f"{frame_index}\t{base['source_frame_index']}\t{seconds[frame_index]:.9f}"
|
||||||
|
f"\t{available_slot}\t{native.shape[0]}"
|
||||||
|
)
|
||||||
|
available_slot += 1
|
||||||
|
|
||||||
|
if available_slot != AVAILABLE_LIDAR_FRAME_COUNT or len(records) != TIMELINE_FRAME_COUNT:
|
||||||
|
raise TgsInputError("TGS full-shadow frame accounting changed")
|
||||||
|
schedule_path = output_root / "schedule.tsv"
|
||||||
|
schedule_path.write_text("\n".join(schedule_rows) + "\n", encoding="utf-8")
|
||||||
|
manifest = {
|
||||||
|
"schema_version": INPUT_SCHEMA,
|
||||||
|
"source_pack_sha256": SOURCE_PACK_SHA256,
|
||||||
|
"config_sha256": sha256_file(config_path),
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"transform": "translation-only-preserve-map-gravity-axis",
|
||||||
|
"intensity_policy": "zero-filled-algorithm-compatibility-only",
|
||||||
|
"future_frames_used": False,
|
||||||
|
"timeline_frame_count": TIMELINE_FRAME_COUNT,
|
||||||
|
"available_lidar_frame_count": AVAILABLE_LIDAR_FRAME_COUNT,
|
||||||
|
"missing_lidar_frame_count": TIMELINE_FRAME_COUNT - AVAILABLE_LIDAR_FRAME_COUNT,
|
||||||
|
"schedule": {
|
||||||
|
"path": "schedule.tsv",
|
||||||
|
"bytes": schedule_path.stat().st_size,
|
||||||
|
"sha256": sha256_file(schedule_path),
|
||||||
|
},
|
||||||
|
"records": records,
|
||||||
|
}
|
||||||
|
manifest_path = output_root / "input-manifest.json"
|
||||||
|
manifest_path.write_text(
|
||||||
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--source-pack", type=Path, required=True)
|
||||||
|
parser.add_argument("--config", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
manifest = prepare(arguments.source_pack, arguments.config, arguments.output_root)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"timeline_frames": manifest["timeline_frame_count"],
|
||||||
|
"available_lidar_frames": manifest["available_lidar_frame_count"],
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "travel/kitti_loader.hpp"
|
||||||
|
#include "travel/point_types.hpp"
|
||||||
|
#include "travel/tgs.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
struct ScheduleRow {
|
||||||
|
std::size_t timeline_frame_index;
|
||||||
|
long long source_frame_index;
|
||||||
|
double session_seconds;
|
||||||
|
long long available_slot;
|
||||||
|
std::size_t point_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ScheduleRow> readSchedule(const std::string& path) {
|
||||||
|
std::ifstream input(path);
|
||||||
|
if (!input) {
|
||||||
|
throw std::runtime_error("cannot open full-shadow schedule");
|
||||||
|
}
|
||||||
|
std::string line;
|
||||||
|
std::getline(input, line);
|
||||||
|
if (line != "timeline_frame_index\tsource_frame_index\tsession_seconds\tavailable_slot\tpoint_count") {
|
||||||
|
throw std::runtime_error("full-shadow schedule header changed");
|
||||||
|
}
|
||||||
|
std::vector<ScheduleRow> rows;
|
||||||
|
while (std::getline(input, line)) {
|
||||||
|
if (line.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::istringstream stream(line);
|
||||||
|
ScheduleRow row{};
|
||||||
|
if (!(stream >> row.timeline_frame_index >> row.source_frame_index >> row.session_seconds
|
||||||
|
>> row.available_slot >> row.point_count)) {
|
||||||
|
throw std::runtime_error("invalid full-shadow schedule row");
|
||||||
|
}
|
||||||
|
if (row.timeline_frame_index != rows.size()) {
|
||||||
|
throw std::runtime_error("full-shadow schedule is not contiguous");
|
||||||
|
}
|
||||||
|
rows.push_back(row);
|
||||||
|
}
|
||||||
|
if (rows.size() != 4489) {
|
||||||
|
throw std::runtime_error("full-shadow timeline frame count changed");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeXYZI(const std::string& path, const travel::PointCloud<PointXYZILID>& cloud) {
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
if (!output) {
|
||||||
|
throw std::runtime_error("cannot open full-shadow TGS output");
|
||||||
|
}
|
||||||
|
for (const auto& point : cloud.points) {
|
||||||
|
const float row[4] = {point.x, point.y, point.z, point.intensity};
|
||||||
|
output.write(reinterpret_cast<const char*>(row), sizeof(row));
|
||||||
|
}
|
||||||
|
if (!output) {
|
||||||
|
throw std::runtime_error("cannot write full-shadow TGS output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double milliseconds(Clock::duration duration) {
|
||||||
|
return std::chrono::duration<double, std::milli>(duration).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (argc != 5) {
|
||||||
|
std::cerr << "Usage: run_tgs_full_shadow <sequence_dir> <schedule.tsv> <output_dir> <timing.tsv>\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const std::string sequence_dir = argv[1];
|
||||||
|
const std::string schedule_path = argv[2];
|
||||||
|
const std::string output_dir = argv[3];
|
||||||
|
const std::string timing_path = argv[4];
|
||||||
|
const auto schedule = readSchedule(schedule_path);
|
||||||
|
KittiLoader loader(sequence_dir);
|
||||||
|
if (loader.size() != 3928) {
|
||||||
|
throw std::runtime_error("full-shadow available LiDAR frame count changed");
|
||||||
|
}
|
||||||
|
std::filesystem::create_directories(output_dir);
|
||||||
|
std::ofstream timing(timing_path);
|
||||||
|
if (!timing) {
|
||||||
|
throw std::runtime_error("cannot open full-shadow timing output");
|
||||||
|
}
|
||||||
|
timing << "timeline_frame_index\tsource_frame_index\tsession_seconds\tsample_available"
|
||||||
|
<< "\tavailable_slot\tinput_points\tground_points\tnonground_points"
|
||||||
|
<< "\ttgs_ms\tstage_wall_ms\tqueue_delay_ms\tcompletion_age_ms\tcapacity_drop\n";
|
||||||
|
timing << std::fixed << std::setprecision(6);
|
||||||
|
|
||||||
|
const double first_source_seconds = schedule.front().session_seconds;
|
||||||
|
const auto run_started = Clock::now();
|
||||||
|
std::size_t expected_slot = 0;
|
||||||
|
for (const auto& row : schedule) {
|
||||||
|
const auto target = run_started + std::chrono::duration_cast<Clock::duration>(
|
||||||
|
std::chrono::duration<double>(row.session_seconds - first_source_seconds));
|
||||||
|
const auto before_wait = Clock::now();
|
||||||
|
if (before_wait < target) {
|
||||||
|
std::this_thread::sleep_until(target);
|
||||||
|
}
|
||||||
|
const auto stage_started = Clock::now();
|
||||||
|
const double queue_delay_ms = std::max(0.0, milliseconds(stage_started - target));
|
||||||
|
std::size_t input_points = 0;
|
||||||
|
std::size_t ground_points = 0;
|
||||||
|
std::size_t nonground_points = 0;
|
||||||
|
double tgs_seconds = 0.0;
|
||||||
|
|
||||||
|
if (row.available_slot >= 0) {
|
||||||
|
if (static_cast<std::size_t>(row.available_slot) != expected_slot) {
|
||||||
|
throw std::runtime_error("full-shadow available slot order changed");
|
||||||
|
}
|
||||||
|
auto input_xyzi = loader.cloud(expected_slot);
|
||||||
|
if (!input_xyzi || input_xyzi->size() != row.point_count) {
|
||||||
|
throw std::runtime_error("full-shadow input point count changed");
|
||||||
|
}
|
||||||
|
auto input = std::make_shared<travel::PointCloud<PointXYZILID>>();
|
||||||
|
input->reserve(input_xyzi->size());
|
||||||
|
for (const auto& point : input_xyzi->points) {
|
||||||
|
PointXYZILID value{};
|
||||||
|
value.x = point.x;
|
||||||
|
value.y = point.y;
|
||||||
|
value.z = point.z;
|
||||||
|
value.intensity = point.intensity;
|
||||||
|
value.label = 0;
|
||||||
|
value.id = 0;
|
||||||
|
input->emplace_back(value);
|
||||||
|
}
|
||||||
|
travel::TravelGroundSeg<PointXYZILID> tgs;
|
||||||
|
tgs.setParams(
|
||||||
|
80.0, 1.0, 8.0, 3, 5, 10, 0.5, 0.125, 0.3, 0.940,
|
||||||
|
200.0, 0.03, 0.1, 1.0, true, false);
|
||||||
|
travel::PointCloud<PointXYZILID> ground;
|
||||||
|
travel::PointCloud<PointXYZILID> nonground;
|
||||||
|
tgs.estimateGround(*input, ground, nonground, tgs_seconds);
|
||||||
|
input_points = input->size();
|
||||||
|
ground_points = ground.size();
|
||||||
|
nonground_points = nonground.size();
|
||||||
|
const std::string base = output_dir + "/" + std::to_string(row.timeline_frame_index);
|
||||||
|
writeXYZI(base + "_ground.bin", ground);
|
||||||
|
writeXYZI(base + "_nonground.bin", nonground);
|
||||||
|
++expected_slot;
|
||||||
|
}
|
||||||
|
const auto completed = Clock::now();
|
||||||
|
timing << row.timeline_frame_index << '\t' << row.source_frame_index << '\t'
|
||||||
|
<< row.session_seconds << '\t' << (row.available_slot >= 0 ? 1 : 0) << '\t'
|
||||||
|
<< row.available_slot << '\t' << input_points << '\t' << ground_points << '\t'
|
||||||
|
<< nonground_points << '\t' << (tgs_seconds * 1000.0) << '\t'
|
||||||
|
<< milliseconds(completed - stage_started) << '\t' << queue_delay_ms << '\t'
|
||||||
|
<< std::max(0.0, milliseconds(completed - target)) << "\t0\n";
|
||||||
|
if ((row.timeline_frame_index + 1) % 100 == 0) {
|
||||||
|
timing.flush();
|
||||||
|
std::cout << "[TGS-FULL] frame=" << (row.timeline_frame_index + 1)
|
||||||
|
<< "/4489 available=" << expected_slot << "/3928\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timing.flush();
|
||||||
|
if (expected_slot != 3928) {
|
||||||
|
throw std::runtime_error("full-shadow available frame accounting changed");
|
||||||
|
}
|
||||||
|
std::cout << "[TGS-FULL] complete timeline=4489 available=3928\n";
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << "[TGS-FULL] " << error.what() << '\n';
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly INPUT_ROOT=/tgs/inputs
|
||||||
|
readonly OUTPUT_ROOT=/tgs/outputs/causal_rolling_1s
|
||||||
|
readonly TIMING_PATH=/tgs/tgs-full-timing.tsv
|
||||||
|
readonly BINARY=/tmp/run_tgs_full_shadow
|
||||||
|
|
||||||
|
test -f "${INPUT_ROOT}/input-manifest.json"
|
||||||
|
test -f "${INPUT_ROOT}/schedule.tsv"
|
||||||
|
test ! -e /tgs/outputs
|
||||||
|
test ! -e "${TIMING_PATH}"
|
||||||
|
g++ -std=c++17 -O3 -DNDEBUG -pthread \
|
||||||
|
-I/opt/travel/src/TRAVEL/cpp/travel/core \
|
||||||
|
-I/usr/include/eigen3 \
|
||||||
|
/release/run_tgs_full_shadow.cpp \
|
||||||
|
-o "${BINARY}"
|
||||||
|
mkdir -p "${OUTPUT_ROOT}"
|
||||||
|
exec /usr/bin/time -v "${BINARY}" \
|
||||||
|
"${INPUT_ROOT}/profiles/causal_rolling_1s" \
|
||||||
|
"${INPUT_ROOT}/schedule.tsv" \
|
||||||
|
"${OUTPUT_ROOT}" \
|
||||||
|
"${TIMING_PATH}"
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the deterministic Worker 006 release for the complete TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gzip
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SOURCES = (
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/prepare_tgs_fail_closed_inputs.py"),
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/build_tgs_fail_closed_evidence.py"),
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/prepare_tgs_full_shadow_inputs.py"),
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/run_tgs_full_shadow.cpp"),
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/run_tgs_full_shadow.sh"),
|
||||||
|
Path("experiments/perception/worker/m49_t3_travel/build_tgs_full_shadow_evidence.py"),
|
||||||
|
Path("experiments/perception/worker/Invoke-M49TgsFullShadow.ps1"),
|
||||||
|
Path("experiments/perception/worker/Invoke-M49TgsFullShadowAsInteractiveUser.ps1"),
|
||||||
|
Path("config/perception/m49-tgs-full-shadow-v1.json"),
|
||||||
|
)
|
||||||
|
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactBuildError(RuntimeError):
|
||||||
|
"""The full-shadow release cannot be built from the declared source."""
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def git_revision() -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "rev-parse", "HEAD"], cwd=REPOSITORY_ROOT, check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
|
||||||
|
info = tarfile.TarInfo(arcname)
|
||||||
|
info.uid = info.gid = 0
|
||||||
|
info.uname = info.gname = "root"
|
||||||
|
info.mtime = 0
|
||||||
|
if path.is_dir():
|
||||||
|
info.type = tarfile.DIRTYPE
|
||||||
|
info.mode = 0o755
|
||||||
|
else:
|
||||||
|
info.type = tarfile.REGTYPE
|
||||||
|
info.mode = 0o755 if path.suffix in {".sh", ".ps1", ".py"} else 0o644
|
||||||
|
info.size = path.stat().st_size
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def write_archive(stage: Path, target: Path) -> None:
|
||||||
|
members = [stage / "manifest.env", stage / "files.txt", stage / "payload"]
|
||||||
|
members.extend(sorted((stage / "payload").rglob("*")))
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with (
|
||||||
|
target.open("wb") as raw,
|
||||||
|
gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed,
|
||||||
|
tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive,
|
||||||
|
):
|
||||||
|
for path in members:
|
||||||
|
info = tar_info(path, path.relative_to(stage).as_posix())
|
||||||
|
if path.is_file():
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
archive.addfile(info, stream)
|
||||||
|
else:
|
||||||
|
archive.addfile(info, io.BytesIO())
|
||||||
|
|
||||||
|
|
||||||
|
def build(patch_id: str, output_directory: Path, *, revision: str | None = None) -> dict[str, object]:
|
||||||
|
if PATCH_ID.fullmatch(patch_id) is None:
|
||||||
|
raise ArtifactBuildError("patch id is invalid")
|
||||||
|
sources = tuple(REPOSITORY_ROOT / source for source in SOURCES)
|
||||||
|
if any(path.is_symlink() or not path.is_file() for path in sources):
|
||||||
|
raise ArtifactBuildError("release input is not a regular file")
|
||||||
|
selected_revision = revision or git_revision()
|
||||||
|
if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None:
|
||||||
|
raise ArtifactBuildError("artifact revision is invalid")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-full-") as directory:
|
||||||
|
stage = Path(directory)
|
||||||
|
payload = stage / "payload"
|
||||||
|
payload.mkdir()
|
||||||
|
files: dict[str, dict[str, object]] = {}
|
||||||
|
for source in sources:
|
||||||
|
destination = payload / source.name
|
||||||
|
destination.write_bytes(source.read_bytes())
|
||||||
|
files[destination.name] = {"bytes": destination.stat().st_size, "sha256": sha256_file(destination)}
|
||||||
|
release = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-worker-release/v1",
|
||||||
|
"patch_id": patch_id,
|
||||||
|
"code_revision": selected_revision,
|
||||||
|
"worker_id": "worker-006",
|
||||||
|
"candidate_id": "travel-tgs-full-shadow",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
|
||||||
|
"images": {
|
||||||
|
"travel": "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
|
||||||
|
"parity": "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0",
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"traversability_accepted": False,
|
||||||
|
"realtime_accepted": False,
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
"navigation_or_actuation_allowed": False,
|
||||||
|
},
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
release_path = payload / "release.json"
|
||||||
|
release_path.write_text(json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
payload_names = sorted((*files, release_path.name))
|
||||||
|
(stage / "manifest.env").write_text(
|
||||||
|
f"id={patch_id}\ncomponent=mission-core-worker\ntype=qualification-release\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(stage / "files.txt").write_text("\n".join(payload_names) + "\n", encoding="utf-8")
|
||||||
|
target = output_directory.resolve() / f"nodedc-{patch_id}.tgz"
|
||||||
|
write_archive(stage, target)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"artifact": str(target),
|
||||||
|
"sha256": sha256_file(target),
|
||||||
|
"patch_id": patch_id,
|
||||||
|
"code_revision": selected_revision,
|
||||||
|
"payload_files": payload_names,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("patch_id")
|
||||||
|
parser.add_argument("--output-directory", type=Path, default=REPOSITORY_ROOT / ".runtime/worker-artifacts")
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
try:
|
||||||
|
result = build(arguments.patch_id, arguments.output_directory)
|
||||||
|
except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
print(json.dumps(result, indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
"""Seal and verify bounded gravity-aligned TRAVEL TGS evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
M49_TGS_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-result/v1"
|
||||||
|
M49_TGS_REPORT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-report/v1"
|
||||||
|
M49_TGS_WORKER_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-evidence-result/v1"
|
||||||
|
M49_TGS_PREFIX: Final = "m49-tgs-fail-closed-"
|
||||||
|
M49_TGS_PROFILE_ID: Final = "m49-ravnoves00-tgs-fail-closed-evidence/v1"
|
||||||
|
M49_TGS_ANCHORS: Final = (171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856)
|
||||||
|
M49_TGS_PROFILES: Final = ("current_increment", "causal_rolling_1s")
|
||||||
|
_MAX_JSON_BYTES: Final = 1024 * 1024
|
||||||
|
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class M49TgsFailClosedError(RuntimeError):
|
||||||
|
"""The TGS evidence pack is unavailable or failed its immutable contract."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class M49TgsFailClosedResult:
|
||||||
|
result_id: str
|
||||||
|
root: Path
|
||||||
|
manifest: dict[str, Any]
|
||||||
|
report: dict[str, Any]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def evidence_path(self) -> Path:
|
||||||
|
return self.root / "evidence.npz"
|
||||||
|
|
||||||
|
|
||||||
|
def seal_m49_tgs_fail_closed(
|
||||||
|
*,
|
||||||
|
source_root: Path,
|
||||||
|
destination_root: Path,
|
||||||
|
profile_path: Path,
|
||||||
|
linked_visual_result_id: str,
|
||||||
|
created_at_utc: str | None = None,
|
||||||
|
) -> M49TgsFailClosedResult:
|
||||||
|
source = _real_directory(source_root, "M49 Worker evidence")
|
||||||
|
destination = destination_root.expanduser().absolute()
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
if destination.is_symlink():
|
||||||
|
raise M49TgsFailClosedError("M49 destination must not be a symlink")
|
||||||
|
profile = _json_file(profile_path, "M49 profile")
|
||||||
|
worker_result = _json_file(source / "result.json", "M49 Worker result")
|
||||||
|
worker_summary = _json_file(source / "worker-summary.json", "M49 Worker summary")
|
||||||
|
input_manifest = _json_file(source / "input-manifest.json", "M49 input manifest")
|
||||||
|
timing = _timing_metrics(source / "tgs-timing.tsv")
|
||||||
|
_validate_source(profile, worker_result, worker_summary, input_manifest, source)
|
||||||
|
if (
|
||||||
|
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||||
|
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 linked visual result is invalid")
|
||||||
|
|
||||||
|
evidence_sha = _file_sha256(source / "evidence.npz")
|
||||||
|
identity = {
|
||||||
|
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"source_pack_sha256": worker_result["source_pack_sha256"],
|
||||||
|
"input_manifest_sha256": worker_result["input_manifest_sha256"],
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"anchor_frame_indices": list(M49_TGS_ANCHORS),
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"profile_id": M49_TGS_PROFILE_ID,
|
||||||
|
"config_sha256": worker_result["config_sha256"],
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"primary_profile": "causal_rolling_1s",
|
||||||
|
"cell_size_m": worker_result["costmap"]["cell_size_m"],
|
||||||
|
"radius_m": worker_result["costmap"]["radius_m"],
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"execution_class": "deterministic",
|
||||||
|
"pipeline_id": "travel-tgs-gravity-aligned-fail-closed/v1",
|
||||||
|
"travel_revision": profile["source"]["travel_revision"],
|
||||||
|
"aos_used": False,
|
||||||
|
"missing_support_means_free": False,
|
||||||
|
"eligible_point_accounting": "exact-multiset-complement",
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"sha256": evidence_sha,
|
||||||
|
"byte_length": (source / "evidence.npz").stat().st_size,
|
||||||
|
"state_codes": profile["state_codes"],
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
identity_sha256 = _canonical_sha256(identity)
|
||||||
|
result_id = f"{M49_TGS_PREFIX}{identity_sha256}"
|
||||||
|
target = destination / result_id
|
||||||
|
if target.exists():
|
||||||
|
return read_m49_tgs_fail_closed(target)
|
||||||
|
|
||||||
|
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
anchors = worker_result["anchors"]
|
||||||
|
primary = [row for row in anchors if row["profile_id"] == "causal_rolling_1s"]
|
||||||
|
report = {
|
||||||
|
"schema_version": M49_TGS_REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"source": identity["source"],
|
||||||
|
"configuration": {
|
||||||
|
**identity["configuration"],
|
||||||
|
"state_priority": profile["costmap"]["state_priority"],
|
||||||
|
"tgs": profile["tgs"],
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
**identity["method"],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"kind": "algorithm",
|
||||||
|
"name": "TRAVEL GroundSeg",
|
||||||
|
"version": profile["source"]["travel_revision"],
|
||||||
|
"role": "gravity-aligned ground/nonground separation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "algorithm",
|
||||||
|
"name": "fail-closed complement adapter",
|
||||||
|
"version": "v1",
|
||||||
|
"role": "retain rejected points and explicit unobserved cells",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "runtime",
|
||||||
|
"name": "Worker 006 CPU qualification",
|
||||||
|
"version": worker_summary["code_revision"],
|
||||||
|
"role": "20 bounded TGS invocations without GPU",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"worker": "Worker 006",
|
||||||
|
"device": "cpu",
|
||||||
|
"gpu_used": False,
|
||||||
|
"wrapper_elapsed_seconds": worker_summary["wall_seconds"],
|
||||||
|
"canonical_triton_id": worker_summary["canonical_triton_id"],
|
||||||
|
"canonical_triton_health": worker_summary["canonical_triton_health"],
|
||||||
|
"free_memory_gib_before": worker_summary["free_memory_gib_before"],
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"anchor_count": len(M49_TGS_ANCHORS),
|
||||||
|
"anchor_profile_count": len(anchors),
|
||||||
|
"all_eligible_points_accounted": True,
|
||||||
|
"primary_profile": "causal_rolling_1s",
|
||||||
|
"primary": primary,
|
||||||
|
"costmap_cell_count": worker_result["costmap"]["cell_count"],
|
||||||
|
"process_wall_current_p50_ms": timing["current_increment"]["p50_ms"],
|
||||||
|
"process_wall_current_max_ms": timing["current_increment"]["max_ms"],
|
||||||
|
"process_wall_rolling_p50_ms": timing["causal_rolling_1s"]["p50_ms"],
|
||||||
|
"process_wall_rolling_max_ms": timing["causal_rolling_1s"]["max_ms"],
|
||||||
|
"process_max_rss_kib": timing["max_rss_kib"],
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"representation_complete": True,
|
||||||
|
"all_points_accounted": True,
|
||||||
|
"aos_absent": True,
|
||||||
|
"gpu_absent": True,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"traversability_accepted": False,
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"state": "visual-review-required",
|
||||||
|
"candidate_retained": True,
|
||||||
|
"next_action": (
|
||||||
|
"Review exact gravity-aligned points and fail-closed costmap "
|
||||||
|
"on the ten immutable anchors."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"The ten anchors are bounded diagnostic evidence, not a full 4,489-frame replay.",
|
||||||
|
"No independent terrain or traversability truth is available.",
|
||||||
|
"No vehicle envelope exists, so occupied cells do not grant or deny physical passage.",
|
||||||
|
"Visual quality, realtime integration, navigation and actuation remain unaccepted.",
|
||||||
|
],
|
||||||
|
"authority": {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
"visual_review": {
|
||||||
|
"instrument": "m4-canonical-reference-graph",
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"anchors": list(M49_TGS_ANCHORS),
|
||||||
|
"profiles": list(M49_TGS_PROFILES),
|
||||||
|
"default_profile": "causal_rolling_1s",
|
||||||
|
"point_states": profile["state_codes"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-", dir=destination) as raw:
|
||||||
|
staging = Path(raw) / result_id
|
||||||
|
staging.mkdir()
|
||||||
|
for name in (
|
||||||
|
"evidence.npz",
|
||||||
|
"worker-summary.json",
|
||||||
|
"input-manifest.json",
|
||||||
|
"tgs-timing.tsv",
|
||||||
|
):
|
||||||
|
shutil.copyfile(source / name, staging / name)
|
||||||
|
_write_json(staging / "report.json", report)
|
||||||
|
artifacts = [
|
||||||
|
_artifact(staging / "report.json", "report", M49_TGS_REPORT_SCHEMA, "application/json"),
|
||||||
|
_artifact(
|
||||||
|
staging / "evidence.npz", "visual-spatial-evidence", None, "application/x-npz"
|
||||||
|
),
|
||||||
|
_artifact(staging / "worker-summary.json", "runtime-summary", None, "application/json"),
|
||||||
|
_artifact(staging / "input-manifest.json", "source-manifest", None, "application/json"),
|
||||||
|
_artifact(
|
||||||
|
staging / "tgs-timing.tsv", "runtime-timing", None, "text/tab-separated-values"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
manifest = {
|
||||||
|
"schema_version": M49_TGS_RESULT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
"authority": report["authority"],
|
||||||
|
"ground_truth": False,
|
||||||
|
}
|
||||||
|
_write_json(staging / "manifest.json", manifest)
|
||||||
|
os.replace(staging, target)
|
||||||
|
return read_m49_tgs_fail_closed(target)
|
||||||
|
|
||||||
|
|
||||||
|
def read_m49_tgs_fail_closed(root: Path) -> M49TgsFailClosedResult:
|
||||||
|
candidate = _real_directory(root, "M49 result")
|
||||||
|
if not candidate.name.startswith(M49_TGS_PREFIX):
|
||||||
|
raise M49TgsFailClosedError("M49 result identity is invalid")
|
||||||
|
manifest = _json_file(candidate / "manifest.json", "M49 manifest")
|
||||||
|
report = _json_file(candidate / "report.json", "M49 report")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != M49_TGS_RESULT_SCHEMA
|
||||||
|
or manifest.get("result_id") != candidate.name
|
||||||
|
or report.get("schema_version") != M49_TGS_REPORT_SCHEMA
|
||||||
|
or report.get("result_id") != candidate.name
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 result contract changed")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
identity_sha = manifest.get("identity_sha256")
|
||||||
|
if (
|
||||||
|
not isinstance(identity, dict)
|
||||||
|
or not isinstance(identity_sha, str)
|
||||||
|
or _canonical_sha256(identity) != identity_sha
|
||||||
|
or candidate.name != f"{M49_TGS_PREFIX}{identity_sha}"
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 identity proof changed")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list) or len(artifacts) != 5:
|
||||||
|
raise M49TgsFailClosedError("M49 artifact manifest changed")
|
||||||
|
for item in artifacts:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise M49TgsFailClosedError("M49 artifact descriptor changed")
|
||||||
|
path = candidate / str(item.get("path", ""))
|
||||||
|
if (
|
||||||
|
path.is_symlink()
|
||||||
|
or not path.is_file()
|
||||||
|
or path.parent != candidate
|
||||||
|
or path.stat().st_size != item.get("byte_length")
|
||||||
|
or _file_sha256(path) != item.get("sha256")
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 artifact proof changed")
|
||||||
|
return M49TgsFailClosedResult(candidate.name, candidate, manifest, report)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_source(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
worker_result: dict[str, Any],
|
||||||
|
worker_summary: dict[str, Any],
|
||||||
|
input_manifest: dict[str, Any],
|
||||||
|
source: Path,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
|
||||||
|
or profile.get("profile_id") != M49_TGS_PROFILE_ID
|
||||||
|
or tuple(profile.get("anchors", ())) != M49_TGS_ANCHORS
|
||||||
|
or profile.get("invariants", {}).get("aos_allowed") is not False
|
||||||
|
or profile.get("invariants", {}).get("missing_support_means_free") is not False
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 profile changed")
|
||||||
|
if (
|
||||||
|
worker_result.get("schema_version") != M49_TGS_WORKER_RESULT_SCHEMA
|
||||||
|
or worker_result.get("status") != "passed"
|
||||||
|
or worker_result.get("summary", {}).get("aos_used") is not False
|
||||||
|
or worker_result.get("summary", {}).get("all_eligible_points_accounted") is not True
|
||||||
|
or worker_result.get("summary", {}).get("primary_profile") != "causal_rolling_1s"
|
||||||
|
or len(worker_result.get("anchors", ())) != 20
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 Worker result changed")
|
||||||
|
if (
|
||||||
|
input_manifest.get("schema_version") != "missioncore.m49-tgs-fail-closed-input/v1"
|
||||||
|
or input_manifest.get("coordinate_frame") != "map-gravity-local"
|
||||||
|
or len(input_manifest.get("records", ())) != 20
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 input manifest changed")
|
||||||
|
if (
|
||||||
|
worker_summary.get("gpu_requested") is not False
|
||||||
|
and worker_summary.get("gpu_requested") is not None
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 Worker GPU contract changed")
|
||||||
|
if (
|
||||||
|
worker_summary.get("aos_used") is not False
|
||||||
|
or worker_summary.get("all_eligible_points_accounted") is not True
|
||||||
|
or worker_summary.get("canonical_triton_health") != "healthy"
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 Worker cleanup changed")
|
||||||
|
evidence = worker_result.get("evidence", {})
|
||||||
|
if (
|
||||||
|
evidence.get("path") != "evidence.npz"
|
||||||
|
or evidence.get("bytes") != (source / "evidence.npz").stat().st_size
|
||||||
|
or evidence.get("sha256") != _file_sha256(source / "evidence.npz")
|
||||||
|
or worker_result.get("input_manifest_sha256")
|
||||||
|
!= _file_sha256(source / "input-manifest.json")
|
||||||
|
):
|
||||||
|
raise M49TgsFailClosedError("M49 evidence proof changed")
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(
|
||||||
|
path: Path,
|
||||||
|
role: str,
|
||||||
|
schema_version: str | None,
|
||||||
|
media_type: str,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result: dict[str, object] = {
|
||||||
|
"role": role,
|
||||||
|
"path": path.name,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": _file_sha256(path),
|
||||||
|
"media_type": media_type,
|
||||||
|
}
|
||||||
|
if schema_version is not None:
|
||||||
|
result["schema_version"] = schema_version
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _timing_metrics(path: Path) -> dict[str, Any]:
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise M49TgsFailClosedError("M49 timing evidence is unavailable")
|
||||||
|
profiles: dict[str, list[float]] = {name: [] for name in M49_TGS_PROFILES}
|
||||||
|
maximum_rss = 0
|
||||||
|
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
||||||
|
if not lines or lines[0] != "profile\tslot\twall_seconds\tmax_rss_kib":
|
||||||
|
raise M49TgsFailClosedError("M49 timing evidence changed")
|
||||||
|
for line in lines[1:]:
|
||||||
|
fields = line.split("\t")
|
||||||
|
if len(fields) != 4 or fields[0] not in profiles:
|
||||||
|
raise M49TgsFailClosedError("M49 timing row changed")
|
||||||
|
profiles[fields[0]].append(float(fields[2]))
|
||||||
|
maximum_rss = max(maximum_rss, int(fields[3]))
|
||||||
|
if any(len(values) != len(M49_TGS_ANCHORS) for values in profiles.values()):
|
||||||
|
raise M49TgsFailClosedError("M49 timing coverage changed")
|
||||||
|
result: dict[str, Any] = {"max_rss_kib": maximum_rss}
|
||||||
|
for name, values in profiles.items():
|
||||||
|
ordered = sorted(values)
|
||||||
|
result[name] = {
|
||||||
|
"p50_ms": ordered[len(ordered) // 2] * 1000,
|
||||||
|
"max_ms": max(ordered) * 1000,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _real_directory(path: Path, label: str) -> Path:
|
||||||
|
candidate = path.expanduser().absolute()
|
||||||
|
if candidate.is_symlink():
|
||||||
|
raise M49TgsFailClosedError(f"{label} must not be a symlink")
|
||||||
|
try:
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
except OSError as exc:
|
||||||
|
raise M49TgsFailClosedError(f"{label} is unavailable") from exc
|
||||||
|
if not resolved.is_dir():
|
||||||
|
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _json_file(path: Path, label: str) -> dict[str, Any]:
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||||
|
raise M49TgsFailClosedError(f"{label} is unavailable")
|
||||||
|
try:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||||
|
except (json.JSONDecodeError, OSError) as exc:
|
||||||
|
raise M49TgsFailClosedError(f"{label} is invalid") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M49TgsFailClosedError(f"{label} is invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: object) -> None:
|
||||||
|
path.write_bytes(_canonical_json(value) + b"\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_sha256(value: object) -> str:
|
||||||
|
return hashlib.sha256(_canonical_json(value)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _file_sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"M49_TGS_ANCHORS",
|
||||||
|
"M49_TGS_PREFIX",
|
||||||
|
"M49_TGS_REPORT_SCHEMA",
|
||||||
|
"M49_TGS_RESULT_SCHEMA",
|
||||||
|
"M49TgsFailClosedError",
|
||||||
|
"M49TgsFailClosedResult",
|
||||||
|
"read_m49_tgs_fail_closed",
|
||||||
|
"seal_m49_tgs_fail_closed",
|
||||||
|
]
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
"""Seal and verify the complete source-paced TRAVEL TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
RESULT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
REPORT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-report/v1"
|
||||||
|
WORKER_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-result/v1"
|
||||||
|
PREFIX: Final = "m49-tgs-full-shadow-"
|
||||||
|
PROFILE_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-profile/v1"
|
||||||
|
EVIDENCE_FILES: Final = (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
)
|
||||||
|
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||||
|
_MAX_JSON_BYTES: Final = 4 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class M49TgsFullShadowError(RuntimeError):
|
||||||
|
"""The full TGS shadow is unavailable or violates its immutable contract."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class M49TgsFullShadowResult:
|
||||||
|
result_id: str
|
||||||
|
root: Path
|
||||||
|
manifest: dict[str, Any]
|
||||||
|
report: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_sha256(value: object) -> str:
|
||||||
|
content = json.dumps(
|
||||||
|
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||||
|
).encode("utf-8")
|
||||||
|
return hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _json(path: Path, label: str) -> dict[str, Any]:
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||||
|
raise M49TgsFullShadowError(f"{label} is unavailable")
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M49TgsFullShadowError(f"{label} is invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(path: Path, role: str, media_type: str) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"path": path.name,
|
||||||
|
"role": role,
|
||||||
|
"media_type": media_type,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": _sha256(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def seal_m49_tgs_full_shadow(
|
||||||
|
*,
|
||||||
|
source_root: Path,
|
||||||
|
destination_root: Path,
|
||||||
|
profile_path: Path,
|
||||||
|
linked_visual_result_id: str,
|
||||||
|
linked_semantic_result_id: str,
|
||||||
|
created_at_utc: str | None = None,
|
||||||
|
) -> M49TgsFullShadowResult:
|
||||||
|
source = source_root.expanduser().resolve(strict=True)
|
||||||
|
if source.is_symlink() or not source.is_dir():
|
||||||
|
raise M49TgsFullShadowError("Worker evidence root is unavailable")
|
||||||
|
destination = destination_root.expanduser().absolute()
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
if destination.is_symlink():
|
||||||
|
raise M49TgsFullShadowError("destination must not be a symlink")
|
||||||
|
profile = _json(profile_path, "full-shadow profile")
|
||||||
|
worker = _json(source / "result.json", "Worker result")
|
||||||
|
summary = _json(source / "worker-summary.json", "Worker summary")
|
||||||
|
timeline = worker.get("timeline")
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != PROFILE_SCHEMA
|
||||||
|
or worker.get("schema_version") != WORKER_SCHEMA
|
||||||
|
or not isinstance(timeline, dict)
|
||||||
|
or (
|
||||||
|
timeline.get("frame_count") != 4489
|
||||||
|
or timeline.get("available_lidar_frame_count") != 3928
|
||||||
|
or timeline.get("missing_lidar_frame_count") != 561
|
||||||
|
)
|
||||||
|
or worker.get("point_accounting", {}).get("unaccounted") != 0
|
||||||
|
or summary.get("schema_version") != "missioncore.m49-tgs-full-shadow-worker-summary/v1"
|
||||||
|
or summary.get("gpu_requested") is not False
|
||||||
|
or summary.get("aos_used") is not False
|
||||||
|
or summary.get("all_timeline_frames_accounted") is not True
|
||||||
|
or summary.get("all_eligible_points_accounted") is not True
|
||||||
|
or summary.get("canonical_triton_health") != "healthy"
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("Worker full-shadow contract changed")
|
||||||
|
if (
|
||||||
|
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||||
|
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("linked visual result is invalid")
|
||||||
|
if (
|
||||||
|
not linked_semantic_result_id.startswith("e47-semantic-slam-")
|
||||||
|
or len(linked_semantic_result_id) != len("e47-semantic-slam-") + 64
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("linked semantic result is invalid")
|
||||||
|
for name in EVIDENCE_FILES:
|
||||||
|
path = source / name
|
||||||
|
proof = worker.get("files", {}).get(name, {})
|
||||||
|
if (
|
||||||
|
path.is_symlink()
|
||||||
|
or not path.is_file()
|
||||||
|
or proof.get("bytes") != path.stat().st_size
|
||||||
|
or proof.get("sha256") != _sha256(path)
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError(f"Worker evidence changed: {name}")
|
||||||
|
identity = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"source_pack_sha256": worker["source_pack_sha256"],
|
||||||
|
"input_manifest_sha256": worker["input_manifest_sha256"],
|
||||||
|
"config_sha256": worker["config_sha256"],
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
|
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
|
||||||
|
"authority": {
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
identity_sha256 = _canonical_sha256(identity)
|
||||||
|
result_id = f"{PREFIX}{identity_sha256}"
|
||||||
|
target = destination / result_id
|
||||||
|
if target.exists():
|
||||||
|
return read_m49_tgs_full_shadow(target)
|
||||||
|
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
performance_accepted = worker.get("status") == "passed"
|
||||||
|
report = {
|
||||||
|
"schema_version": REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"source_pack_sha256": worker["source_pack_sha256"],
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"profile_id": profile["profile_id"],
|
||||||
|
"config_sha256": worker["config_sha256"],
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"history_seconds": profile["profile"]["history_seconds"],
|
||||||
|
"cell_size_m": worker["costmap"]["cell_size_m"],
|
||||||
|
"radius_m": worker["costmap"]["radius_m"],
|
||||||
|
"state_priority": profile["costmap"]["state_priority"],
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"worker": "Worker 006",
|
||||||
|
"device": "cpu",
|
||||||
|
"gpu_used": False,
|
||||||
|
"aos_used": False,
|
||||||
|
"wrapper_elapsed_seconds": summary["wall_seconds"],
|
||||||
|
"canonical_triton_id": summary["canonical_triton_id"],
|
||||||
|
"canonical_triton_health": summary["canonical_triton_health"],
|
||||||
|
},
|
||||||
|
"timeline": worker["timeline"],
|
||||||
|
"point_accounting": worker["point_accounting"],
|
||||||
|
"performance": worker["performance"],
|
||||||
|
"acceptance": {
|
||||||
|
**worker["acceptance"],
|
||||||
|
"representation_complete": True,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"state": (
|
||||||
|
"source-paced-qualified-visual-review-required"
|
||||||
|
if performance_accepted
|
||||||
|
else "performance-rejected"
|
||||||
|
),
|
||||||
|
"candidate_retained": performance_accepted,
|
||||||
|
"next_action": (
|
||||||
|
"Review the complete camera-synchronised TGS costmap timeline; "
|
||||||
|
"then measure the integrated graph regression separately."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"The run proves recorded source-paced CPU shadow performance, not live sensor transport.",
|
||||||
|
"No independent traversability truth or vehicle envelope is present.",
|
||||||
|
"Missing LiDAR frames are explicit all-cell UNOBSERVED and never inferred free.",
|
||||||
|
"Camera projection, navigation and actuation remain disabled.",
|
||||||
|
],
|
||||||
|
"authority": {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"commands_enabled": False,
|
||||||
|
"realtime_shadow_accepted": performance_accepted,
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
"visual_review": {
|
||||||
|
"instrument": "m4-canonical-reference-graph",
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"linked_semantic_result_id": linked_semantic_result_id,
|
||||||
|
"frame_count": 4489,
|
||||||
|
"state_codes": profile["state_codes"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-full-", dir=destination) as raw:
|
||||||
|
staging = Path(raw) / result_id
|
||||||
|
staging.mkdir()
|
||||||
|
for name in EVIDENCE_FILES:
|
||||||
|
shutil.copyfile(source / name, staging / name)
|
||||||
|
shutil.copyfile(source / "worker-summary.json", staging / "worker-summary.json")
|
||||||
|
(staging / "report.json").write_text(
|
||||||
|
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
artifacts = [
|
||||||
|
_artifact(staging / "report.json", "report", "application/json"),
|
||||||
|
_artifact(staging / "worker-summary.json", "runtime-summary", "application/json"),
|
||||||
|
]
|
||||||
|
artifacts.extend(
|
||||||
|
_artifact(
|
||||||
|
staging / name,
|
||||||
|
"frame-catalog" if name == "frames.ndjson" else "spatial-evidence",
|
||||||
|
"application/x-ndjson" if name == "frames.ndjson" else "application/x-npy",
|
||||||
|
)
|
||||||
|
for name in EVIDENCE_FILES
|
||||||
|
)
|
||||||
|
manifest = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
(staging / "manifest.json").write_text(
|
||||||
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
staging.replace(target)
|
||||||
|
return read_m49_tgs_full_shadow(target)
|
||||||
|
|
||||||
|
|
||||||
|
def read_m49_tgs_full_shadow(root: Path) -> M49TgsFullShadowResult:
|
||||||
|
candidate = root.expanduser().resolve(strict=True)
|
||||||
|
if candidate.is_symlink() or not candidate.is_dir() or not candidate.name.startswith(PREFIX):
|
||||||
|
raise M49TgsFullShadowError("full-shadow result root is invalid")
|
||||||
|
manifest = _json(candidate / "manifest.json", "full-shadow manifest")
|
||||||
|
report = _json(candidate / "report.json", "full-shadow report")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != RESULT_SCHEMA
|
||||||
|
or report.get("schema_version") != REPORT_SCHEMA
|
||||||
|
or manifest.get("result_id") != candidate.name
|
||||||
|
or report.get("result_id") != candidate.name
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or manifest.get("identity_sha256") != _canonical_sha256(identity)
|
||||||
|
or candidate.name != f"{PREFIX}{manifest['identity_sha256']}"
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("full-shadow identity changed")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact catalog changed")
|
||||||
|
for artifact in artifacts:
|
||||||
|
if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact entry changed")
|
||||||
|
path = candidate / artifact["path"]
|
||||||
|
if (
|
||||||
|
path.parent != candidate
|
||||||
|
or path.is_symlink()
|
||||||
|
or not path.is_file()
|
||||||
|
or artifact.get("byte_length") != path.stat().st_size
|
||||||
|
or artifact.get("sha256") != _sha256(path)
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact digest changed")
|
||||||
|
return M49TgsFullShadowResult(candidate.name, candidate, manifest, report)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"M49TgsFullShadowError",
|
||||||
|
"M49TgsFullShadowResult",
|
||||||
|
"PREFIX",
|
||||||
|
"read_m49_tgs_full_shadow",
|
||||||
|
"seal_m49_tgs_full_shadow",
|
||||||
|
]
|
||||||
@@ -133,6 +133,8 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
|
|||||||
build_m48s_fixed_class_detector_lab_router,
|
build_m48s_fixed_class_detector_lab_router,
|
||||||
)
|
)
|
||||||
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
||||||
|
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.map_api import (
|
from k1link.web.map_api import (
|
||||||
MapGatewayConfiguration,
|
MapGatewayConfiguration,
|
||||||
MapGatewayProxy,
|
MapGatewayProxy,
|
||||||
@@ -992,6 +994,28 @@ app.include_router(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
app.include_router(
|
||||||
|
build_m49_tgs_fail_closed_router(
|
||||||
|
root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "m49"
|
||||||
|
/ "tgs-fail-closed-results"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app.include_router(
|
||||||
|
build_m49_tgs_full_shadow_router(
|
||||||
|
root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "m49"
|
||||||
|
/ "tgs-full-shadow-results"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
build_m48s_fixed_class_detector_lab_router(
|
build_m48s_fixed_class_detector_lab_router(
|
||||||
root_provider=lambda: (
|
root_provider=lambda: (
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
"""Read-only API for sealed gravity-aligned M49 TGS evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from fastapi import APIRouter, HTTPException, Query, Response
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_fail_closed import (
|
||||||
|
M49_TGS_ANCHORS,
|
||||||
|
M49_TGS_PREFIX,
|
||||||
|
M49TgsFailClosedError,
|
||||||
|
M49TgsFailClosedResult,
|
||||||
|
read_m49_tgs_fail_closed,
|
||||||
|
)
|
||||||
|
|
||||||
|
RootProvider = Callable[[], Path | None]
|
||||||
|
|
||||||
|
RESULT_ID: Final = re.compile(rf"^{re.escape(M49_TGS_PREFIX)}[a-f0-9]{{64}}$")
|
||||||
|
RESULT_VIEW_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-view/v1"
|
||||||
|
RESULT_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-catalog/v1"
|
||||||
|
ANCHOR_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-anchor-catalog/v1"
|
||||||
|
ANCHOR_SPATIAL_SCHEMA: Final = "missioncore.m49-tgs-anchor-spatial/v1"
|
||||||
|
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-fail-closed"
|
||||||
|
PROFILES: Final = ("current_increment", "causal_rolling_1s")
|
||||||
|
|
||||||
|
|
||||||
|
def build_m49_tgs_fail_closed_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
|
||||||
|
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||||
|
|
||||||
|
def result(result_id: str) -> M49TgsFailClosedResult:
|
||||||
|
candidate = _resolve_candidate(root_provider, result_id)
|
||||||
|
try:
|
||||||
|
return _read_result_cached(str(candidate), _signature(candidate))
|
||||||
|
except (M49TgsFailClosedError, OSError, ValueError):
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS result not found") from None
|
||||||
|
|
||||||
|
@router.get("/results")
|
||||||
|
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
return _empty_catalog(configured=False)
|
||||||
|
items: list[dict[str, object]] = []
|
||||||
|
invalid_total = 0
|
||||||
|
for candidate in sorted(root.iterdir()):
|
||||||
|
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sealed = _read_result_cached(str(candidate.resolve()), _signature(candidate))
|
||||||
|
items.append(_project_result(sealed))
|
||||||
|
except (M49TgsFailClosedError, OSError, ValueError):
|
||||||
|
invalid_total += 1
|
||||||
|
items.sort(
|
||||||
|
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||||
|
"configured": True,
|
||||||
|
"items": items[:limit],
|
||||||
|
"candidate_total": len(items) + invalid_total,
|
||||||
|
"invalid_total": invalid_total,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/{result_id}")
|
||||||
|
def get_result(result_id: str) -> dict[str, object]:
|
||||||
|
return _project_result(result(result_id))
|
||||||
|
|
||||||
|
@router.get("/{result_id}/anchors")
|
||||||
|
def get_anchors(result_id: str) -> dict[str, object]:
|
||||||
|
sealed = result(result_id)
|
||||||
|
return {
|
||||||
|
"schema_version": ANCHOR_CATALOG_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"linked_visual_result_id": sealed.report["visual_review"]["linked_visual_result_id"],
|
||||||
|
"anchors": copy.deepcopy(sealed.report["metrics"]["primary"]),
|
||||||
|
"anchor_count": len(M49_TGS_ANCHORS),
|
||||||
|
"profiles": list(PROFILES),
|
||||||
|
"default_profile": "causal_rolling_1s",
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/{result_id}/anchors/{anchor_frame_index}/spatial")
|
||||||
|
def get_anchor_spatial(
|
||||||
|
result_id: str,
|
||||||
|
anchor_frame_index: int,
|
||||||
|
profile: str = Query(default="causal_rolling_1s"),
|
||||||
|
) -> Response:
|
||||||
|
if profile not in PROFILES or anchor_frame_index not in M49_TGS_ANCHORS:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS anchor not found")
|
||||||
|
sealed = result(result_id)
|
||||||
|
try:
|
||||||
|
content = _anchor_json_cached(
|
||||||
|
str(sealed.evidence_path),
|
||||||
|
result_id,
|
||||||
|
anchor_frame_index,
|
||||||
|
profile,
|
||||||
|
_evidence_signature(sealed.evidence_path),
|
||||||
|
)
|
||||||
|
except (KeyError, OSError, ValueError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503, detail="M49 TGS spatial evidence failed verification"
|
||||||
|
) from None
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/json",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "private, max-age=31536000, immutable",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _read_result_cached(result_root: str, signature: tuple[int, ...]) -> M49TgsFailClosedResult:
|
||||||
|
del signature
|
||||||
|
return read_m49_tgs_fail_closed(Path(result_root))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=24)
|
||||||
|
def _anchor_json_cached(
|
||||||
|
evidence_path: str,
|
||||||
|
result_id: str,
|
||||||
|
anchor_frame_index: int,
|
||||||
|
profile: str,
|
||||||
|
signature: tuple[int, int],
|
||||||
|
) -> bytes:
|
||||||
|
del signature
|
||||||
|
slot = M49_TGS_ANCHORS.index(anchor_frame_index)
|
||||||
|
with np.load(evidence_path, allow_pickle=False) as evidence:
|
||||||
|
offsets = evidence[f"{profile}_point_offsets"]
|
||||||
|
start = int(offsets[slot])
|
||||||
|
end = int(offsets[slot + 1])
|
||||||
|
points = evidence[f"{profile}_points_xyz_m"][start:end]
|
||||||
|
point_states = evidence[f"{profile}_point_states"][start:end]
|
||||||
|
centers = evidence["costmap_cell_centers_xy_m"]
|
||||||
|
cell_states = evidence[f"{profile}_costmap_states"][slot]
|
||||||
|
z_bounds = evidence[f"{profile}_costmap_z_bounds_m"][slot]
|
||||||
|
if (
|
||||||
|
points.shape[1:] != (3,)
|
||||||
|
or point_states.shape != (points.shape[0],)
|
||||||
|
or centers.shape != (2244, 2)
|
||||||
|
or cell_states.shape != (2244,)
|
||||||
|
or z_bounds.shape != (2244, 2)
|
||||||
|
or not np.isfinite(points).all()
|
||||||
|
or not np.isfinite(centers).all()
|
||||||
|
or not np.isin(point_states, np.asarray([1, 2, 3], dtype=np.uint8)).all()
|
||||||
|
or not np.isin(cell_states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
|
||||||
|
):
|
||||||
|
raise ValueError("M49 TGS spatial shape changed")
|
||||||
|
payload = {
|
||||||
|
"schema_version": ANCHOR_SPATIAL_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"anchor_frame_index": anchor_frame_index,
|
||||||
|
"source_sequence": anchor_frame_index,
|
||||||
|
"profile": profile,
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"points_xyz_m": points.astype(float).tolist(),
|
||||||
|
"point_states": point_states.astype(int).tolist(),
|
||||||
|
"costmap": {
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"centers_xy_m": centers.astype(float).tolist(),
|
||||||
|
"states": cell_states.astype(int).tolist(),
|
||||||
|
"z_bounds_m": [
|
||||||
|
[
|
||||||
|
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||||
|
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||||
|
]
|
||||||
|
for row in z_bounds
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"state_codes": {
|
||||||
|
"UNOBSERVED": 0,
|
||||||
|
"GROUND_SUPPORT": 1,
|
||||||
|
"NONGROUND_OCCUPIED": 2,
|
||||||
|
"UNKNOWN_REJECTED": 3,
|
||||||
|
},
|
||||||
|
"all_points_accounted": True,
|
||||||
|
"aos_used": False,
|
||||||
|
"authority": {
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
},
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
return json.dumps(
|
||||||
|
payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _project_result(result: M49TgsFailClosedResult) -> dict[str, object]:
|
||||||
|
report = result.report
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_VIEW_SCHEMA,
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"created_at_utc": result.manifest["created_at_utc"],
|
||||||
|
"source": copy.deepcopy(report["source"]),
|
||||||
|
"configuration": copy.deepcopy(report["configuration"]),
|
||||||
|
"method": copy.deepcopy(report["method"]),
|
||||||
|
"execution": copy.deepcopy(report["execution"]),
|
||||||
|
"metrics": copy.deepcopy(report["metrics"]),
|
||||||
|
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||||
|
"decision": copy.deepcopy(report["decision"]),
|
||||||
|
"limitations": copy.deepcopy(report["limitations"]),
|
||||||
|
"authority": copy.deepcopy(report["authority"]),
|
||||||
|
"visual_review": copy.deepcopy(report["visual_review"]),
|
||||||
|
"ground_truth": False,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
||||||
|
if RESULT_ID.fullmatch(result_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||||
|
root = _configured_root(provider)
|
||||||
|
if root is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||||
|
candidate = root / result_id
|
||||||
|
if candidate.is_symlink() or not candidate.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved.parent != root:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS result not found")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_root(provider: RootProvider) -> Path | None:
|
||||||
|
value = provider()
|
||||||
|
if value is None or value.is_symlink() or not value.is_dir():
|
||||||
|
return None
|
||||||
|
return value.resolve(strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _signature(candidate: Path) -> tuple[int, ...]:
|
||||||
|
result: list[int] = []
|
||||||
|
for name in (
|
||||||
|
"manifest.json",
|
||||||
|
"report.json",
|
||||||
|
"evidence.npz",
|
||||||
|
"worker-summary.json",
|
||||||
|
"input-manifest.json",
|
||||||
|
"tgs-timing.tsv",
|
||||||
|
):
|
||||||
|
path = candidate / name
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise ValueError("M49 TGS artifact unavailable")
|
||||||
|
stat = path.stat()
|
||||||
|
result.extend((stat.st_size, stat.st_mtime_ns))
|
||||||
|
return tuple(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_signature(path: Path) -> tuple[int, int]:
|
||||||
|
stat = path.stat()
|
||||||
|
return stat.st_size, stat.st_mtime_ns
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_catalog(*, configured: bool) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||||
|
"configured": configured,
|
||||||
|
"items": [],
|
||||||
|
"candidate_total": 0,
|
||||||
|
"invalid_total": 0,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["build_m49_tgs_fail_closed_router"]
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"""Read-only API for the sealed complete TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from fastapi import APIRouter, HTTPException, Query, Response
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_full_shadow import (
|
||||||
|
M49TgsFullShadowError,
|
||||||
|
M49TgsFullShadowResult,
|
||||||
|
PREFIX,
|
||||||
|
read_m49_tgs_full_shadow,
|
||||||
|
)
|
||||||
|
|
||||||
|
RootProvider = Callable[[], Path | None]
|
||||||
|
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
|
||||||
|
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-full-shadow"
|
||||||
|
|
||||||
|
|
||||||
|
def build_m49_tgs_full_shadow_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
|
||||||
|
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||||
|
|
||||||
|
def sealed(result_id: str) -> M49TgsFullShadowResult:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||||
|
candidate = root / result_id
|
||||||
|
if candidate.is_symlink() or not candidate.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||||
|
try:
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved.parent != root:
|
||||||
|
raise ValueError("result escaped configured root")
|
||||||
|
return _read_cached(str(resolved), _signature(resolved))
|
||||||
|
except (M49TgsFullShadowError, OSError, ValueError):
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found") from None
|
||||||
|
|
||||||
|
@router.get("/results")
|
||||||
|
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
return _catalog([], configured=False, invalid_total=0)
|
||||||
|
results: list[dict[str, object]] = []
|
||||||
|
invalid = 0
|
||||||
|
for candidate in sorted(root.iterdir()):
|
||||||
|
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
results.append(_project(_read_cached(str(candidate.resolve()), _signature(candidate))))
|
||||||
|
except (M49TgsFullShadowError, OSError, ValueError):
|
||||||
|
invalid += 1
|
||||||
|
results.sort(key=lambda row: (str(row["created_at_utc"]), str(row["result_id"])), reverse=True)
|
||||||
|
return _catalog(results[:limit], configured=True, invalid_total=invalid)
|
||||||
|
|
||||||
|
@router.get("/{result_id}")
|
||||||
|
def get_result(result_id: str) -> dict[str, object]:
|
||||||
|
return _project(sealed(result_id))
|
||||||
|
|
||||||
|
@router.get("/{result_id}/frames/{source_sequence}/spatial")
|
||||||
|
def get_spatial(result_id: str, source_sequence: int) -> Response:
|
||||||
|
if source_sequence < 0 or source_sequence >= 4489:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full-shadow frame not found")
|
||||||
|
result = sealed(result_id)
|
||||||
|
try:
|
||||||
|
content = _frame_json_cached(
|
||||||
|
str(result.root), result_id, source_sequence, _evidence_signature(result.root)
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||||
|
raise HTTPException(status_code=503, detail="M49 TGS full-shadow spatial evidence failed verification") from None
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/json",
|
||||||
|
headers={"Cache-Control": "private, max-age=31536000, immutable", "X-Content-Type-Options": "nosniff"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{result_id}/spatial/chunk")
|
||||||
|
def get_spatial_chunk(
|
||||||
|
result_id: str,
|
||||||
|
start: int = Query(ge=0, lt=4489),
|
||||||
|
count: int = Query(default=24, ge=1, le=24),
|
||||||
|
) -> Response:
|
||||||
|
result = sealed(result_id)
|
||||||
|
bounded_count = min(count, 4489 - start)
|
||||||
|
try:
|
||||||
|
content = _chunk_json_cached(
|
||||||
|
str(result.root), result_id, start, bounded_count, _evidence_signature(result.root)
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="M49 TGS full-shadow spatial chunk failed verification",
|
||||||
|
) from None
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/json",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "private, max-age=31536000, immutable",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _read_cached(root: str, signature: tuple[int, ...]) -> M49TgsFullShadowResult:
|
||||||
|
del signature
|
||||||
|
return read_m49_tgs_full_shadow(Path(root))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _frames(root: str, signature: tuple[int, int]) -> tuple[dict[str, object], ...]:
|
||||||
|
del signature
|
||||||
|
path = Path(root) / "frames.ndjson"
|
||||||
|
rows = tuple(json.loads(line) for line in path.read_text(encoding="utf-8").splitlines())
|
||||||
|
if len(rows) != 4489:
|
||||||
|
raise ValueError("full-shadow frame catalog changed")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=96)
|
||||||
|
def _frame_json_cached(
|
||||||
|
root: str, result_id: str, source_sequence: int, signature: tuple[int, ...]
|
||||||
|
) -> bytes:
|
||||||
|
root_path = Path(root)
|
||||||
|
frame_signature = (signature[-2], signature[-1])
|
||||||
|
frame = _frames(root, frame_signature)[source_sequence]
|
||||||
|
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states = states_all[source_sequence]
|
||||||
|
z_bounds = z_all[source_sequence]
|
||||||
|
if (
|
||||||
|
centers.shape != (2244, 2)
|
||||||
|
or states_all.shape != (4489, 2244)
|
||||||
|
or z_all.shape != (4489, 2244, 2)
|
||||||
|
or not np.isfinite(centers).all()
|
||||||
|
or not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
|
||||||
|
):
|
||||||
|
raise ValueError("full-shadow spatial shape changed")
|
||||||
|
payload = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-spatial/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"source_sequence": source_sequence,
|
||||||
|
"source_frame_index": frame["source_frame_index"],
|
||||||
|
"session_seconds": frame["session_seconds"],
|
||||||
|
"sample_available": frame["sample_available"],
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"costmap": {
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"centers_xy_m": centers.astype(float).tolist(),
|
||||||
|
"states": states.astype(int).tolist(),
|
||||||
|
"z_bounds_m": [
|
||||||
|
[
|
||||||
|
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||||
|
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||||
|
]
|
||||||
|
for row in z_bounds
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"metrics": copy.deepcopy(frame),
|
||||||
|
"state_codes": {"UNOBSERVED": 0, "GROUND_SUPPORT": 1, "NONGROUND_OCCUPIED": 2, "UNKNOWN_REJECTED": 3},
|
||||||
|
"aos_used": False,
|
||||||
|
"gpu_used": False,
|
||||||
|
"authority": {"navigation_or_safety_accepted": False, "visual_quality_accepted": False},
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def _chunk_json_cached(
|
||||||
|
root: str,
|
||||||
|
result_id: str,
|
||||||
|
start: int,
|
||||||
|
count: int,
|
||||||
|
signature: tuple[int, ...],
|
||||||
|
) -> bytes:
|
||||||
|
root_path = Path(root)
|
||||||
|
frame_signature = (signature[-2], signature[-1])
|
||||||
|
frames = _frames(root, frame_signature)
|
||||||
|
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
if (
|
||||||
|
centers.shape != (2244, 2)
|
||||||
|
or states_all.shape != (4489, 2244)
|
||||||
|
or z_all.shape != (4489, 2244, 2)
|
||||||
|
or not np.isfinite(centers).all()
|
||||||
|
):
|
||||||
|
raise ValueError("full-shadow spatial chunk shape changed")
|
||||||
|
rows: list[dict[str, object]] = []
|
||||||
|
for source_sequence in range(start, start + count):
|
||||||
|
frame = frames[source_sequence]
|
||||||
|
states = states_all[source_sequence]
|
||||||
|
z_bounds = z_all[source_sequence]
|
||||||
|
if not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all():
|
||||||
|
raise ValueError("full-shadow spatial state changed")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"source_sequence": source_sequence,
|
||||||
|
"source_frame_index": frame["source_frame_index"],
|
||||||
|
"session_seconds": frame["session_seconds"],
|
||||||
|
"sample_available": frame["sample_available"],
|
||||||
|
"states": states.astype(int).tolist(),
|
||||||
|
"z_bounds_m": [
|
||||||
|
[
|
||||||
|
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||||
|
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||||
|
]
|
||||||
|
for row in z_bounds
|
||||||
|
],
|
||||||
|
"metrics": copy.deepcopy(frame),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-spatial-chunk/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"start": start,
|
||||||
|
"count": count,
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"costmap": {
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"centers_xy_m": centers.astype(float).tolist(),
|
||||||
|
},
|
||||||
|
"frames": rows,
|
||||||
|
"state_codes": {
|
||||||
|
"UNOBSERVED": 0,
|
||||||
|
"GROUND_SUPPORT": 1,
|
||||||
|
"NONGROUND_OCCUPIED": 2,
|
||||||
|
"UNKNOWN_REJECTED": 3,
|
||||||
|
},
|
||||||
|
"aos_used": False,
|
||||||
|
"gpu_used": False,
|
||||||
|
"authority": {
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
return json.dumps(
|
||||||
|
payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _project(result: M49TgsFullShadowResult) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
**copy.deepcopy(result.report),
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-view/v1",
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"created_at_utc": result.manifest["created_at_utc"],
|
||||||
|
"ground_truth": False,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog(items: list[dict[str, object]], *, configured: bool, invalid_total: int) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-catalog/v1",
|
||||||
|
"configured": configured,
|
||||||
|
"items": items,
|
||||||
|
"candidate_total": len(items) + invalid_total,
|
||||||
|
"invalid_total": invalid_total,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_root(provider: RootProvider) -> Path | None:
|
||||||
|
value = provider()
|
||||||
|
if value is None or value.is_symlink() or not value.is_dir():
|
||||||
|
return None
|
||||||
|
return value.resolve(strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _signature(root: Path) -> tuple[int, ...]:
|
||||||
|
values: list[int] = []
|
||||||
|
for name in ("manifest.json", "report.json", "worker-summary.json", *EVIDENCE_FILES):
|
||||||
|
path = root / name
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise ValueError("full-shadow artifact unavailable")
|
||||||
|
stat = path.stat()
|
||||||
|
values.extend((stat.st_size, stat.st_mtime_ns))
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_signature(root: Path) -> tuple[int, ...]:
|
||||||
|
return _signature(root)
|
||||||
|
|
||||||
|
|
||||||
|
EVIDENCE_FILES: Final = (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["build_m49_tgs_full_shadow_router"]
|
||||||
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
repository_root / "config" / "laboratories"
|
repository_root / "config" / "laboratories"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.definitions) == 39
|
assert len(registry.definitions) == 42
|
||||||
assert {item.work_id for item in registry.definitions} >= {
|
assert {item.work_id for item in registry.definitions} >= {
|
||||||
"e31-source-binding",
|
"e31-source-binding",
|
||||||
"e46j-raw-fisheye-realtime",
|
"e46j-raw-fisheye-realtime",
|
||||||
@@ -144,6 +144,8 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
"m48-static-occupancy-qualification",
|
"m48-static-occupancy-qualification",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
m48 = next(
|
m48 = next(
|
||||||
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
|
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
"e47-semantic-slam-shadow",
|
"e47-semantic-slam-shadow",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
by_work_id = {row.work_id: row for row in execution.definitions}
|
by_work_id = {row.work_id: row for row in execution.definitions}
|
||||||
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
|
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
|
||||||
@@ -118,6 +120,10 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
assert by_work_id["m48s-fixed-class-detector"].isolation == "bounded-adapter"
|
assert by_work_id["m48s-fixed-class-detector"].isolation == "bounded-adapter"
|
||||||
assert by_work_id["m48t-risk-quality-temporal"].lifecycle == "experimental"
|
assert by_work_id["m48t-risk-quality-temporal"].lifecycle == "experimental"
|
||||||
assert by_work_id["m48t-risk-quality-temporal"].isolation == "bounded-adapter"
|
assert by_work_id["m48t-risk-quality-temporal"].isolation == "bounded-adapter"
|
||||||
|
assert by_work_id["m49-tgs-fail-closed-evidence"].lifecycle == "experimental"
|
||||||
|
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 all(
|
assert all(
|
||||||
row.lifecycle == "canonical"
|
row.lifecycle == "canonical"
|
||||||
for row in execution.definitions
|
for row in execution.definitions
|
||||||
@@ -126,6 +132,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
"e47-semantic-slam-shadow",
|
"e47-semantic-slam-shadow",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert len(execution.definitions) + len(execution.legacy_work_ids) == len(
|
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"
|
root / "config" / "laboratory-value-review.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.entries) == 38
|
assert len(registry.entries) == 40
|
||||||
assert {entry.catalog_id for entry in registry.entries} >= {
|
assert {entry.catalog_id for entry in registry.entries} >= {
|
||||||
"e28-local-surface",
|
"e28-local-surface",
|
||||||
"e46d-temporal-failure-audit",
|
"e46d-temporal-failure-audit",
|
||||||
@@ -91,4 +91,6 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
"m48-static-occupancy-qualification",
|
"m48-static-occupancy-qualification",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_fail_closed import (
|
||||||
|
M49_TGS_ANCHORS,
|
||||||
|
read_m49_tgs_fail_closed,
|
||||||
|
seal_m49_tgs_fail_closed,
|
||||||
|
)
|
||||||
|
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _sha(path: Path) -> str:
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: object) -> None:
|
||||||
|
path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _source(root: Path) -> Path:
|
||||||
|
root.mkdir()
|
||||||
|
centers = np.zeros((2244, 2), dtype=np.float32)
|
||||||
|
arrays: dict[str, np.ndarray] = {"costmap_cell_centers_xy_m": centers}
|
||||||
|
summaries: list[dict[str, object]] = []
|
||||||
|
for profile in ("current_increment", "causal_rolling_1s"):
|
||||||
|
arrays[f"{profile}_points_xyz_m"] = np.asarray(
|
||||||
|
[[float(index), 0.0, 0.1] for index in range(10)], dtype=np.float32
|
||||||
|
)
|
||||||
|
arrays[f"{profile}_point_states"] = np.asarray([1, 2] * 5, dtype=np.uint8)
|
||||||
|
arrays[f"{profile}_point_offsets"] = np.arange(11, dtype=np.int64)
|
||||||
|
arrays[f"{profile}_costmap_states"] = np.zeros((10, 2244), dtype=np.uint8)
|
||||||
|
arrays[f"{profile}_costmap_ground_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
|
||||||
|
arrays[f"{profile}_costmap_nonground_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
|
||||||
|
arrays[f"{profile}_costmap_rejected_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
|
||||||
|
arrays[f"{profile}_costmap_z_bounds_m"] = np.full((10, 2244, 2), np.nan, dtype=np.float32)
|
||||||
|
for slot, anchor in enumerate(M49_TGS_ANCHORS):
|
||||||
|
summaries.append(
|
||||||
|
{
|
||||||
|
"profile_id": profile,
|
||||||
|
"slot": slot,
|
||||||
|
"anchor_frame_index": anchor,
|
||||||
|
"point_count": 1,
|
||||||
|
"ground_point_count": 1 if slot % 2 == 0 else 0,
|
||||||
|
"nonground_point_count": 0 if slot % 2 == 0 else 1,
|
||||||
|
"rejected_point_count": 0,
|
||||||
|
"ground_cell_count": 0,
|
||||||
|
"nonground_cell_count": 0,
|
||||||
|
"rejected_cell_count": 0,
|
||||||
|
"unobserved_cell_count": 2244,
|
||||||
|
"all_points_accounted": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
np.savez_compressed(root / "evidence.npz", **arrays)
|
||||||
|
records = [
|
||||||
|
{
|
||||||
|
"profile_id": profile,
|
||||||
|
"slot": slot,
|
||||||
|
"anchor_frame_index": anchor,
|
||||||
|
}
|
||||||
|
for profile in ("current_increment", "causal_rolling_1s")
|
||||||
|
for slot, anchor in enumerate(M49_TGS_ANCHORS)
|
||||||
|
]
|
||||||
|
_write_json(
|
||||||
|
root / "input-manifest.json",
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.m49-tgs-fail-closed-input/v1",
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"records": records,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_write_json(
|
||||||
|
root / "worker-summary.json",
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.m49-tgs-worker-summary/v1",
|
||||||
|
"code_revision": "a" * 40,
|
||||||
|
"wall_seconds": 1.5,
|
||||||
|
"free_memory_gib_before": 42.0,
|
||||||
|
"canonical_triton_id": "b" * 64,
|
||||||
|
"canonical_triton_health": "healthy",
|
||||||
|
"all_eligible_points_accounted": True,
|
||||||
|
"aos_used": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
timing = ["profile\tslot\twall_seconds\tmax_rss_kib"]
|
||||||
|
timing.extend(
|
||||||
|
f"{profile}\t{slot}\t0.02\t7000"
|
||||||
|
for profile in ("current_increment", "causal_rolling_1s")
|
||||||
|
for slot in range(10)
|
||||||
|
)
|
||||||
|
(root / "tgs-timing.tsv").write_text("\n".join(timing) + "\n", encoding="utf-8")
|
||||||
|
_write_json(
|
||||||
|
root / "result.json",
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.m49-tgs-fail-closed-evidence-result/v1",
|
||||||
|
"status": "passed",
|
||||||
|
"config_sha256": "c" * 64,
|
||||||
|
"source_pack_sha256": "d" * 64,
|
||||||
|
"input_manifest_sha256": _sha(root / "input-manifest.json"),
|
||||||
|
"evidence": {
|
||||||
|
"path": "evidence.npz",
|
||||||
|
"bytes": (root / "evidence.npz").stat().st_size,
|
||||||
|
"sha256": _sha(root / "evidence.npz"),
|
||||||
|
},
|
||||||
|
"costmap": {
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"cell_count": 2244,
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"aos_used": False,
|
||||||
|
"all_eligible_points_accounted": True,
|
||||||
|
"primary_profile": "causal_rolling_1s",
|
||||||
|
},
|
||||||
|
"anchors": summaries,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def test_m49_tgs_seal_and_api_are_immutable_and_fail_closed(tmp_path: Path) -> None:
|
||||||
|
source = _source(tmp_path / "source")
|
||||||
|
destination = tmp_path / "results"
|
||||||
|
linked = "m4-threat-replay-" + "e" * 64
|
||||||
|
sealed = seal_m49_tgs_fail_closed(
|
||||||
|
source_root=source,
|
||||||
|
destination_root=destination,
|
||||||
|
profile_path=(
|
||||||
|
REPOSITORY_ROOT / "config" / "perception" / "m49-tgs-fail-closed-evidence-v1.json"
|
||||||
|
),
|
||||||
|
linked_visual_result_id=linked,
|
||||||
|
created_at_utc="2026-08-26T17:30:00Z",
|
||||||
|
)
|
||||||
|
assert read_m49_tgs_fail_closed(sealed.root).result_id == sealed.result_id
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(build_m49_tgs_fail_closed_router(root_provider=lambda: destination))
|
||||||
|
client = TestClient(app)
|
||||||
|
catalog = client.get("/api/v1/laboratory/m49/tgs-fail-closed/results")
|
||||||
|
assert catalog.status_code == 200
|
||||||
|
assert catalog.json()["items"][0]["result_id"] == sealed.result_id
|
||||||
|
anchors = client.get(f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}/anchors")
|
||||||
|
assert anchors.status_code == 200
|
||||||
|
assert anchors.json()["anchor_count"] == 10
|
||||||
|
spatial = client.get(
|
||||||
|
f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}/anchors/171/spatial"
|
||||||
|
)
|
||||||
|
assert spatial.status_code == 200
|
||||||
|
assert spatial.json()["point_states"] == [1]
|
||||||
|
assert spatial.json()["costmap"]["states"] == [0] * 2244
|
||||||
|
assert spatial.json()["authority"]["navigation_or_safety_accepted"] is False
|
||||||
|
|
||||||
|
(sealed.root / "worker-summary.json").write_text("{}", encoding="utf-8")
|
||||||
|
assert (
|
||||||
|
client.get(f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}").status_code == 404
|
||||||
|
)
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_full_shadow import seal_m49_tgs_full_shadow
|
||||||
|
from k1link.web import m49_tgs_full_shadow_api as full_shadow_api
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
WORKER_ROOT = REPOSITORY_ROOT / "experiments" / "perception" / "worker" / "m49_t3_travel"
|
||||||
|
sys.path.insert(0, str(WORKER_ROOT))
|
||||||
|
|
||||||
|
EVIDENCE_SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"m49_tgs_full_shadow_evidence", WORKER_ROOT / "build_tgs_full_shadow_evidence.py"
|
||||||
|
)
|
||||||
|
assert EVIDENCE_SPEC and EVIDENCE_SPEC.loader
|
||||||
|
EVIDENCE = importlib.util.module_from_spec(EVIDENCE_SPEC)
|
||||||
|
EVIDENCE_SPEC.loader.exec_module(EVIDENCE)
|
||||||
|
|
||||||
|
ARTIFACT_SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"m49_tgs_full_shadow_artifact",
|
||||||
|
REPOSITORY_ROOT / "scripts" / "build_m49_tgs_full_shadow_worker_artifact.py",
|
||||||
|
)
|
||||||
|
assert ARTIFACT_SPEC and ARTIFACT_SPEC.loader
|
||||||
|
ARTIFACT = importlib.util.module_from_spec(ARTIFACT_SPEC)
|
||||||
|
ARTIFACT_SPEC.loader.exec_module(ARTIFACT)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_exact_multiset_and_costmap_priority() -> None:
|
||||||
|
native = np.asarray(
|
||||||
|
[[2.0, 0.0, 0.0, 0.0], [2.0, 0.0, 0.0, 0.0], [3.0, 0.0, 1.0, 0.0], [4.0, 0.0, 2.0, 0.0]],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
points, states = EVIDENCE.classify_exact_input(native, native[[0]], native[[1, 2]])
|
||||||
|
assert sorted(states.tolist()) == [1, 2, 2, 3]
|
||||||
|
grid = EVIDENCE.costmap_grid(12.0, 0.45)
|
||||||
|
cell_states, _ = EVIDENCE.rasterize(points, states, grid, 0.45)
|
||||||
|
assert np.count_nonzero(cell_states == 2) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_worker_artifact_is_deterministic_and_cpu_only(tmp_path: Path) -> None:
|
||||||
|
revision = "a" * 40
|
||||||
|
first = ARTIFACT.build("m49-tgs-full-test", tmp_path / "one", revision=revision)
|
||||||
|
second = ARTIFACT.build("m49-tgs-full-test", tmp_path / "two", revision=revision)
|
||||||
|
assert first["sha256"] == second["sha256"]
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||||
|
release = archive.extractfile("payload/release.json")
|
||||||
|
runner = archive.extractfile("payload/Invoke-M49TgsFullShadow.ps1")
|
||||||
|
assert release is not None and runner is not None
|
||||||
|
release_text = release.read().decode()
|
||||||
|
runner_text = runner.read().decode()
|
||||||
|
assert '"candidate_id": "travel-tgs-full-shadow"' in release_text
|
||||||
|
assert "--gpus" not in runner_text
|
||||||
|
assert "gpu_requested = $false" in runner_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_seal_binds_visual_and_semantic_timelines(tmp_path: Path) -> None:
|
||||||
|
source = tmp_path / "worker"
|
||||||
|
source.mkdir()
|
||||||
|
files: dict[str, dict[str, object]] = {}
|
||||||
|
for name in (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
):
|
||||||
|
payload = f"sealed:{name}\n".encode()
|
||||||
|
(source / name).write_bytes(payload)
|
||||||
|
files[name] = {
|
||||||
|
"bytes": len(payload),
|
||||||
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||||
|
}
|
||||||
|
worker = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-result/v1",
|
||||||
|
"status": "passed",
|
||||||
|
"source_pack_sha256": "a" * 64,
|
||||||
|
"input_manifest_sha256": "b" * 64,
|
||||||
|
"config_sha256": "c" * 64,
|
||||||
|
"timeline": {
|
||||||
|
"frame_count": 4489,
|
||||||
|
"available_lidar_frame_count": 3928,
|
||||||
|
"missing_lidar_frame_count": 561,
|
||||||
|
},
|
||||||
|
"point_accounting": {"unaccounted": 0},
|
||||||
|
"costmap": {"cell_size_m": 0.45, "radius_m": 12.0},
|
||||||
|
"performance": {},
|
||||||
|
"acceptance": {},
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
(source / "result.json").write_text(json.dumps(worker), encoding="utf-8")
|
||||||
|
(source / "worker-summary.json").write_text(json.dumps({
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-worker-summary/v1",
|
||||||
|
"gpu_requested": False,
|
||||||
|
"aos_used": False,
|
||||||
|
"all_timeline_frames_accounted": True,
|
||||||
|
"all_eligible_points_accounted": True,
|
||||||
|
"canonical_triton_health": "healthy",
|
||||||
|
"canonical_triton_id": "triton",
|
||||||
|
"wall_seconds": 1.0,
|
||||||
|
}), encoding="utf-8")
|
||||||
|
profile = tmp_path / "profile.json"
|
||||||
|
profile.write_text(json.dumps({
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-profile/v1",
|
||||||
|
"profile_id": "test",
|
||||||
|
"profile": {"history_seconds": 1.0},
|
||||||
|
"costmap": {"state_priority": ["NONGROUND_OCCUPIED"]},
|
||||||
|
"state_codes": {"UNOBSERVED": 0},
|
||||||
|
}), encoding="utf-8")
|
||||||
|
visual = "m4-threat-replay-" + "d" * 64
|
||||||
|
semantic = "e47-semantic-slam-" + "e" * 64
|
||||||
|
|
||||||
|
sealed = seal_m49_tgs_full_shadow(
|
||||||
|
source_root=source,
|
||||||
|
destination_root=tmp_path / "results",
|
||||||
|
profile_path=profile,
|
||||||
|
linked_visual_result_id=visual,
|
||||||
|
linked_semantic_result_id=semantic,
|
||||||
|
created_at_utc="2026-08-26T20:27:19Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sealed.report["source"]["linked_visual_result_id"] == visual
|
||||||
|
assert sealed.report["source"]["linked_semantic_result_id"] == semantic
|
||||||
|
assert sealed.manifest["identity"]["linked_semantic_result_id"] == semantic
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_chunk_contract_keeps_missing_lidar_unobserved(
|
||||||
|
monkeypatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
frames = tuple(
|
||||||
|
{
|
||||||
|
"source_frame_index": index,
|
||||||
|
"session_seconds": index / 10,
|
||||||
|
"sample_available": index != 1,
|
||||||
|
"eligible_point_count": 0 if index == 1 else 1,
|
||||||
|
"ground_point_count": 0,
|
||||||
|
"nonground_point_count": 0 if index == 1 else 1,
|
||||||
|
"rejected_point_count": 0,
|
||||||
|
"occupied_cell_count": 0 if index == 1 else 1,
|
||||||
|
}
|
||||||
|
for index in range(4489)
|
||||||
|
)
|
||||||
|
centers = np.zeros((2244, 2), dtype=np.float32)
|
||||||
|
states = np.broadcast_to(np.zeros((1, 2244), dtype=np.uint8), (4489, 2244))
|
||||||
|
z_bounds = np.broadcast_to(
|
||||||
|
np.full((1, 2244, 2), np.nan, dtype=np.float32),
|
||||||
|
(4489, 2244, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(full_shadow_api, "_frames", lambda *_args: frames)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
full_shadow_api.np,
|
||||||
|
"load",
|
||||||
|
lambda path, **_kwargs: (
|
||||||
|
centers if "centers" in str(path) else states if "states" in str(path) else z_bounds
|
||||||
|
),
|
||||||
|
)
|
||||||
|
content = full_shadow_api._chunk_json_cached.__wrapped__(
|
||||||
|
str(tmp_path),
|
||||||
|
"m49-tgs-full-shadow-" + "a" * 64,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
(0, 0),
|
||||||
|
)
|
||||||
|
payload = json.loads(content)
|
||||||
|
assert payload["schema_version"] == "missioncore.m49-tgs-full-shadow-spatial-chunk/v1"
|
||||||
|
assert payload["count"] == 2
|
||||||
|
assert len(payload["costmap"]["centers_xy_m"]) == 2244
|
||||||
|
assert payload["frames"][1]["sample_available"] is False
|
||||||
|
assert set(payload["frames"][1]["states"]) == {0}
|
||||||
Reference in New Issue
Block a user