feat(lab): publish fail-closed TGS evidence
This commit is contained in:
@@ -48,6 +48,7 @@ import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQ
|
||||
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
export type { AdvancedLaboratoryWorkId };
|
||||
@@ -108,6 +109,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m48t-risk-quality-temporal" && 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 === "m47-reference-graph-shadow" && 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; иначе кандидат отклоняется без ручной подгонки порогов под эти кадры.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { ObservationTimeline } from "../../components/ObservationTimeline";
|
||||
import {
|
||||
LaboratoryMetricEvidenceScene,
|
||||
type LaboratoryMetricCellEvidence,
|
||||
type LaboratoryMetricEvidenceSceneHandle,
|
||||
type LaboratoryMetricSceneMode,
|
||||
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
|
||||
@@ -103,6 +104,31 @@ export interface M4ReplayThreatReviewAnchor {
|
||||
sourceSequence: number;
|
||||
extentXyxyNormalized: readonly [number, number, number, number];
|
||||
matchedAtThreshold: boolean;
|
||||
statusLabel?: string;
|
||||
}
|
||||
|
||||
export interface M4ReplayClassifiedSpatialFrame {
|
||||
sourceSequence: 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;
|
||||
}
|
||||
|
||||
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||
@@ -115,6 +141,9 @@ export function M4ReplayThreatVisual({
|
||||
reviewLabel = "Контрольные примеры M4.8R1",
|
||||
timelineEndpointRoot,
|
||||
evidenceLabel = "M4.6",
|
||||
initialSpatialMode = null,
|
||||
classifiedSpatialLayer,
|
||||
onActiveSequenceChange,
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
@@ -123,9 +152,14 @@ export function M4ReplayThreatVisual({
|
||||
reviewLabel?: string;
|
||||
timelineEndpointRoot?: string;
|
||||
evidenceLabel?: string;
|
||||
initialSpatialMode?: LaboratoryMetricSceneMode | null;
|
||||
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
|
||||
onActiveSequenceChange?: (sequence: number | null) => void;
|
||||
}) {
|
||||
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 [showLocalSurface, setShowLocalSurface] = useState(true);
|
||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||
@@ -220,6 +254,9 @@ export function M4ReplayThreatVisual({
|
||||
}, [resultId]);
|
||||
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
||||
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
||||
useEffect(() => {
|
||||
onActiveSequenceChange?.(frame?.sequence ?? null);
|
||||
}, [frame?.sequence, onActiveSequenceChange]);
|
||||
const lastSpatialFrameRef = useRef<{
|
||||
resultId: string;
|
||||
frame: M4ThreatTimelineFrame;
|
||||
@@ -303,12 +340,12 @@ export function M4ReplayThreatVisual({
|
||||
);
|
||||
}, [frame, metadata.timeline, showStaticObstacles]);
|
||||
const activeBoxes = useMemo(
|
||||
() => [
|
||||
() => classifiedSpatialLayer ? [] : [
|
||||
...boxes(frame?.cameraProposals ?? []),
|
||||
...staticObstacleBoxes,
|
||||
...reviewAnchorBoxes,
|
||||
],
|
||||
[frame, reviewAnchorBoxes, staticObstacleBoxes],
|
||||
[classifiedSpatialLayer, frame, reviewAnchorBoxes, staticObstacleBoxes],
|
||||
);
|
||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
@@ -363,6 +400,68 @@ export function M4ReplayThreatVisual({
|
||||
return status === 2 || status === 3 ? classId : null;
|
||||
});
|
||||
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||
const classifiedSpatialFrame = !displayingBufferedFrame
|
||||
&& classifiedSpatialLayer?.frame?.sourceSequence === frame?.sequence
|
||||
&& spatialFrame?.sequence === frame?.sequence
|
||||
? classifiedSpatialLayer?.frame ?? null
|
||||
: null;
|
||||
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||
point: readonly [number, number, number],
|
||||
): readonly [number, number, number] => {
|
||||
const basis = spatialFrame?.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];
|
||||
}, [nominalSensorHeightM, spatialFrame?.bodyFrame?.basisMapFromBody]);
|
||||
const classifiedPointsBody = useMemo(
|
||||
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||
mapGravityLocalSensorToBodyGround,
|
||||
) ?? [],
|
||||
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
|
||||
);
|
||||
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
|
||||
() => classifiedSpatialFrame?.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,
|
||||
};
|
||||
}) ?? [],
|
||||
[classifiedSpatialFrame, 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) => ({
|
||||
id: obstacle.componentId,
|
||||
decision: obstacle.assessment.decision,
|
||||
@@ -522,7 +621,32 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
) : 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={showRollingMap ? "primary" : "secondary"}
|
||||
aria-pressed={showRollingMap}
|
||||
onClick={() => setShowRollingMap((visible) => !visible)}
|
||||
>
|
||||
{classifiedSpatialLayer.cellLayerLabel}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
@@ -626,7 +750,7 @@ export function M4ReplayThreatVisual({
|
||||
value={String(selectedReviewAnchorIndex)}
|
||||
options={reviewAnchors.map((anchor, 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"
|
||||
menuWidth="anchor"
|
||||
@@ -671,39 +795,48 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
<div>
|
||||
<span>Spatial evidence</span>
|
||||
<strong>
|
||||
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
|
||||
{metadata.timeline.occupancyProvenanceDelivery
|
||||
? ` · ${lowStepObstacles.length} low-step`
|
||||
: ""}
|
||||
</strong>
|
||||
<small>
|
||||
{spatialFrame
|
||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "квалифицированный spatial frame ещё не получен"}
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{accumulatedCameraPoints
|
||||
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
||||
: pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
||||
: showMediaPoints && cameraPointOverlay.error
|
||||
? " · накопленное camera cloud недоступно"
|
||||
: ""}
|
||||
{semantic && spatialSemanticFrame
|
||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
</small>
|
||||
<strong>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
||||
: "TGS spatial buffer"
|
||||
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
|
||||
<small>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
||||
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
|
||||
: (
|
||||
<>
|
||||
{spatialFrame
|
||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "квалифицированный spatial frame ещё не получен"}
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{accumulatedCameraPoints
|
||||
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
||||
: pointCloudOverlay
|
||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
||||
: showMediaPoints && cameraPointOverlay.error
|
||||
? " · накопленное camera cloud недоступно"
|
||||
: ""}
|
||||
{semantic && spatialSemanticFrame
|
||||
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
|
||||
: semantic ? " · semantic buffer" : ""}
|
||||
</>
|
||||
)}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Virtual corridor</span>
|
||||
<strong>
|
||||
{spatialFrame?.decisionCounts.threat ?? 0} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}
|
||||
</strong>
|
||||
<small>
|
||||
{metadata.timeline.corridor.forwardLengthM} м · body {metadata.timeline.rig.lengthM}×{metadata.timeline.rig.widthM} м · REPLAY-SIMULATED
|
||||
</small>
|
||||
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
|
||||
<strong>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
|
||||
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
|
||||
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
|
||||
<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>
|
||||
) : undefined;
|
||||
@@ -802,26 +935,50 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{spatialFrame ? (
|
||||
{spatialFrame && (!classifiedSpatialLayer || classifiedSpatialFrame) ? (
|
||||
<LaboratoryMetricEvidenceScene
|
||||
ref={metricSceneRef}
|
||||
pointCloudBodyXyzM={spatialFrame.pointCloudBodyXyzM}
|
||||
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
|
||||
obstacles={sceneObstacles}
|
||||
pointCloudBodyXyzM={classifiedSpatialFrame
|
||||
? classifiedPointsBody
|
||||
: spatialFrame.pointCloudBodyXyzM}
|
||||
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
||||
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
||||
rig={timeline.rig}
|
||||
corridor={timeline.corridor}
|
||||
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
|
||||
occupiedVoxelSizeM={classifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
|
||||
mode={spatialMode}
|
||||
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={showLowStep}
|
||||
pointSemanticClassIds={alignedSemanticPointIds}
|
||||
semanticClasses={semanticClasses}
|
||||
semanticPalette={semanticPalette}
|
||||
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
||||
pointSemanticClassIds={classifiedSpatialFrame
|
||||
? classifiedSpatialFrame.pointClassIds
|
||||
: alignedSemanticPointIds}
|
||||
semanticClasses={classifiedSpatialFrame
|
||||
? classifiedSpatialFrame.classes
|
||||
: semanticClasses}
|
||||
semanticPalette={classifiedSpatialFrame
|
||||
? classifiedSpatialFrame.palette
|
||||
: semanticPalette}
|
||||
classifiedCells={classifiedCellsBody}
|
||||
classifiedCellSizeM={classifiedSpatialFrame?.cellSizeM}
|
||||
showClassifiedCells={showRollingMap}
|
||||
/>
|
||||
) : null}
|
||||
{classifiedSpatialLayer && !classifiedSpatialFrame ? (
|
||||
<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}
|
||||
{frame && !frame.spatialAvailable ? (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
{spatialFrame
|
||||
|
||||
@@ -105,6 +105,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
experimentName: "RF-DETR native risk review and temporal identity",
|
||||
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",
|
||||
},
|
||||
"m47-reference-graph-shadow": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||
|
||||
@@ -25,6 +25,7 @@ function mergeResults(
|
||||
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m48t: next.m48t ?? current.m48t,
|
||||
m49Tgs: next.m49Tgs ?? current.m49Tgs,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
@@ -124,6 +125,7 @@ export function useAdvancedLaboratoryCatalog({
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-static-occupancy-qualification",
|
||||
"m48r3-static-occupancy-shadow",
|
||||
"m49-tgs-fail-closed-evidence",
|
||||
].includes(selectedWorkId)
|
||||
&& !indexedResultId
|
||||
) return;
|
||||
|
||||
Reference in New Issue
Block a user