feat(lab): publish full TGS shadow evidence
This commit is contained in:
@@ -49,6 +49,7 @@ import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShad
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
export type { AdvancedLaboratoryWorkId };
|
||||
@@ -112,6 +113,9 @@ export function AdvancedLaboratoryResult({
|
||||
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) {
|
||||
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type {
|
||||
RecordedEvidenceSemanticClass,
|
||||
RecordedEvidenceSemanticPaletteEntry,
|
||||
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||
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 [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(() => {
|
||||
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}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -109,6 +109,8 @@ export interface M4ReplayThreatReviewAnchor {
|
||||
|
||||
export interface M4ReplayClassifiedSpatialFrame {
|
||||
sourceSequence: number;
|
||||
sampleAvailable?: boolean;
|
||||
sourcePointCount?: number;
|
||||
pointsMapGravityLocalXyzM: readonly (readonly [number, number, number])[];
|
||||
pointClassIds: readonly (number | null)[];
|
||||
cellsMapGravityLocal: readonly {
|
||||
@@ -129,6 +131,7 @@ export interface M4ReplayClassifiedSpatialLayer {
|
||||
frame: M4ReplayClassifiedSpatialFrame | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
replacePointCloud?: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||
@@ -255,8 +258,8 @@ export function M4ReplayThreatVisual({
|
||||
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
||||
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
||||
useEffect(() => {
|
||||
onActiveSequenceChange?.(frame?.sequence ?? null);
|
||||
}, [frame?.sequence, onActiveSequenceChange]);
|
||||
onActiveSequenceChange?.(timelineFrame.activeSequence);
|
||||
}, [onActiveSequenceChange, timelineFrame.activeSequence]);
|
||||
const lastSpatialFrameRef = useRef<{
|
||||
resultId: string;
|
||||
frame: M4ThreatTimelineFrame;
|
||||
@@ -400,16 +403,18 @@ 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
|
||||
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||
? spatialFrame
|
||||
: null;
|
||||
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
||||
? classifiedSpatialLayer?.frame ?? null
|
||||
: 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 = spatialFrame?.bodyFrame?.basisMapFromBody;
|
||||
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],
|
||||
@@ -418,7 +423,7 @@ export function M4ReplayThreatVisual({
|
||||
// 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]);
|
||||
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||
const classifiedPointsBody = useMemo(
|
||||
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||
mapGravityLocalSensorToBodyGround,
|
||||
@@ -776,7 +781,11 @@ export function M4ReplayThreatVisual({
|
||||
? splitPrimarySize
|
||||
: 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
|
||||
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
|
||||
style={{
|
||||
@@ -785,9 +794,9 @@ export function M4ReplayThreatVisual({
|
||||
>
|
||||
<div>
|
||||
<span>RAVNOVES00 · recorded realtime</span>
|
||||
<strong>frame {frame.sequence + 1}/{metadata.timeline.frameCount}</strong>
|
||||
<strong>frame {overlaySequence + 1}/{metadata.timeline.frameCount}</strong>
|
||||
<small>
|
||||
+{(frame.sessionSeconds - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
||||
+{((overlaySessionSeconds ?? metadata.timeline.timelineStartSeconds) - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
||||
· {displayingBufferedFrame
|
||||
? "держим последний кадр, следующий в буфере"
|
||||
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
||||
@@ -797,24 +806,32 @@ export function M4ReplayThreatVisual({
|
||||
<span>Spatial evidence</span>
|
||||
<strong>{classifiedSpatialLayer
|
||||
? classifiedSpatialFrame
|
||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
||||
? replaceClassifiedPointCloud
|
||||
? `${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`
|
||||
: "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"
|
||||
? 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.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||
: "квалифицированный spatial frame ещё не получен"}
|
||||
{frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
||||
{frame
|
||||
? frame.worldStateAvailable
|
||||
? " · world-state delivered"
|
||||
: ` · world-state gap (${frame.terminalOutcome})`
|
||||
: " · world-state frame unavailable"}
|
||||
{accumulatedCameraPoints
|
||||
? ` · 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 · накопление загружается`
|
||||
: showMediaPoints && cameraPointOverlay.error
|
||||
? " · накопленное camera cloud недоступно"
|
||||
@@ -935,12 +952,12 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{spatialFrame && (!classifiedSpatialLayer || classifiedSpatialFrame) ? (
|
||||
{(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? (
|
||||
<LaboratoryMetricEvidenceScene
|
||||
ref={metricSceneRef}
|
||||
pointCloudBodyXyzM={classifiedSpatialFrame
|
||||
pointCloudBodyXyzM={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedPointsBody
|
||||
: spatialFrame.pointCloudBodyXyzM}
|
||||
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
||||
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
||||
rig={timeline.rig}
|
||||
@@ -952,13 +969,13 @@ export function M4ReplayThreatVisual({
|
||||
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
||||
pointSemanticClassIds={classifiedSpatialFrame
|
||||
pointSemanticClassIds={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.pointClassIds
|
||||
: alignedSemanticPointIds}
|
||||
semanticClasses={classifiedSpatialFrame
|
||||
semanticClasses={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.classes
|
||||
: semanticClasses}
|
||||
semanticPalette={classifiedSpatialFrame
|
||||
semanticPalette={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||
? classifiedSpatialFrame.palette
|
||||
: semanticPalette}
|
||||
classifiedCells={classifiedCellsBody}
|
||||
@@ -979,7 +996,15 @@ export function M4ReplayThreatVisual({
|
||||
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{frame && !frame.spatialAvailable ? (
|
||||
{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">
|
||||
{spatialFrame
|
||||
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
|
||||
|
||||
@@ -112,6 +112,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
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": {
|
||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||
|
||||
@@ -26,6 +26,7 @@ function mergeResults(
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m48t: next.m48t ?? current.m48t,
|
||||
m49Tgs: next.m49Tgs ?? current.m49Tgs,
|
||||
m49TgsFull: next.m49TgsFull ?? current.m49TgsFull,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
@@ -126,6 +127,7 @@ export function useAdvancedLaboratoryCatalog({
|
||||
"m48-static-occupancy-qualification",
|
||||
"m48r3-static-occupancy-shadow",
|
||||
"m49-tgs-fail-closed-evidence",
|
||||
"m49-tgs-full-shadow",
|
||||
].includes(selectedWorkId)
|
||||
&& !indexedResultId
|
||||
) return;
|
||||
|
||||
Reference in New Issue
Block a user