feat(perception): qualify E35 degradation recovery
This commit is contained in:
@@ -8,13 +8,15 @@ import { E31Result } from "./E31Result";
|
||||
import { E32Result } from "./E32Result";
|
||||
import { E33Result } from "./E33Result";
|
||||
import { E34Result } from "./E34Result";
|
||||
import { E35Result } from "./E35Result";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "e31-source-binding"
|
||||
| "e32-track-geometry"
|
||||
| "e33-worker-shadow"
|
||||
| "e34-temporal-layer";
|
||||
| "e34-temporal-layer"
|
||||
| "e35-degradation-recovery";
|
||||
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
@@ -28,6 +30,7 @@ export function isAdvancedLaboratoryWorkId(
|
||||
|| value === "e32-track-geometry"
|
||||
|| value === "e33-worker-shadow"
|
||||
|| value === "e34-temporal-layer"
|
||||
|| value === "e35-degradation-recovery"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,6 +63,12 @@ export function advancedLaboratoryWorkOptions(
|
||||
label: "LAB E34 · temporal occupied/unknown",
|
||||
});
|
||||
}
|
||||
if (results.e35) {
|
||||
options.push({
|
||||
id: "e35-degradation-recovery",
|
||||
label: "LAB E35 · degradation recovery",
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -97,6 +106,9 @@ export function AdvancedLaboratoryResult({
|
||||
failedSessionId: string | null;
|
||||
replayError: string | null;
|
||||
}) {
|
||||
if (workId === "e35-degradation-recovery" && results.e35) {
|
||||
return <E35Result rigLabel={rigLabel} result={results.e35} />;
|
||||
}
|
||||
if (workId === "e34-temporal-layer" && results.e34) {
|
||||
return <E34Result rigLabel={rigLabel} result={results.e34} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Button, Select } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type {
|
||||
E35DegradationKind,
|
||||
E35DegradationRecoveryResult,
|
||||
E35RecoveryReviewFrame,
|
||||
E35RecoveryReviewScenario,
|
||||
} from "../../core/laboratory/e35DegradationRecovery";
|
||||
import type { E34TemporalReviewFrame } from "../../core/laboratory/e34TemporalLayer";
|
||||
import { formatNumber } from "../../presentation";
|
||||
import {
|
||||
E34TemporalLayerScene,
|
||||
type E34TemporalViewMode,
|
||||
} from "./E34TemporalLayerScene";
|
||||
|
||||
const SCENARIO_LABELS: Record<E35DegradationKind, string> = {
|
||||
"camera-loss": "Потеря камеры",
|
||||
"lidar-loss": "Потеря LiDAR",
|
||||
"pose-staleness": "Устаревшая поза",
|
||||
"delayed-frames": "Просроченные кадры",
|
||||
"bounded-drop": "Ограниченные пропуски",
|
||||
"timing-offset": "Рассинхрон 250 мс",
|
||||
};
|
||||
|
||||
const PHASE_LABELS = {
|
||||
before: "До отказа",
|
||||
during: "Во время отказа",
|
||||
after: "После восстановления",
|
||||
} as const;
|
||||
|
||||
function sceneFrame(frame: E35RecoveryReviewFrame): E34TemporalReviewFrame {
|
||||
return {
|
||||
frameIndex: frame.frameIndex,
|
||||
sessionSeconds: frame.sessionSeconds,
|
||||
sourceAvailable: frame.transformedPointRows > 0,
|
||||
layerState: frame.layerState,
|
||||
counts: frame.counts,
|
||||
cellCentersMapXyzM: frame.cellCentersMapXyzM,
|
||||
components: frame.components.map((component) => ({
|
||||
temporalId: component.temporalId,
|
||||
state: component.state,
|
||||
occupancyState: component.occupancyState,
|
||||
ownerKind: component.ownerKind,
|
||||
centroidMapXyzM: component.centroidMapXyzM,
|
||||
lastObservedAgeSeconds: component.lastObservedAgeSeconds,
|
||||
associationReason: frame.action,
|
||||
history: [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function channelSummary(frame: E35RecoveryReviewFrame): string {
|
||||
return Object.entries(frame.channels)
|
||||
.map(([channel, state]) => `${channel}: ${state}`)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
function unsafeClaims(
|
||||
result: E35DegradationRecoveryResult,
|
||||
): number {
|
||||
return result.scenarios.reduce((total, scenario) => (
|
||||
total
|
||||
+ scenario.hiddenSuccessFrames
|
||||
+ scenario.falseFreeRows
|
||||
+ scenario.semanticClaimsDuringCameraLoss
|
||||
+ scenario.metricRowsDuringLidarOrPoseLoss
|
||||
+ scenario.agreeClaimsDuringTimingOffset
|
||||
+ scenario.lateResultsReintroduced
|
||||
), 0);
|
||||
}
|
||||
|
||||
function maximumRecovery(
|
||||
result: E35DegradationRecoveryResult,
|
||||
): number {
|
||||
return Math.max(...result.scenarios.map(
|
||||
(scenario) => scenario.recoverySeconds,
|
||||
));
|
||||
}
|
||||
|
||||
function defaultFrame(
|
||||
scenario: E35RecoveryReviewScenario,
|
||||
): E35RecoveryReviewFrame | null {
|
||||
return scenario.frames.find((frame) => (
|
||||
frame.faultPhase === "during"
|
||||
&& (frame.counts.held > 0 || frame.counts.expired > 0)
|
||||
)) ?? scenario.frames[0] ?? null;
|
||||
}
|
||||
|
||||
function E35Evidence({
|
||||
result,
|
||||
}: {
|
||||
result: E35DegradationRecoveryResult;
|
||||
}) {
|
||||
const initialScenario = result.reviewScenarios[0] ?? null;
|
||||
const [scenarioId, setScenarioId] = useState<E35DegradationKind>(
|
||||
initialScenario?.scenarioId ?? "camera-loss",
|
||||
);
|
||||
const scenario = result.reviewScenarios.find(
|
||||
(item) => item.scenarioId === scenarioId,
|
||||
) ?? initialScenario;
|
||||
const initialFrame = scenario ? defaultFrame(scenario) : null;
|
||||
const [frameIndex, setFrameIndex] = useState(initialFrame?.frameIndex ?? 0);
|
||||
const frame = scenario?.frames.find(
|
||||
(item) => item.frameIndex === frameIndex,
|
||||
) ?? initialFrame;
|
||||
const [mode, setMode] = useState<E34TemporalViewMode>("3d");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const renderedFrame = useMemo(
|
||||
() => frame ? sceneFrame(frame) : null,
|
||||
[frame],
|
||||
);
|
||||
|
||||
if (!scenario || !frame || !renderedFrame) {
|
||||
return (
|
||||
<div className="laboratory-result-pending" role="status">
|
||||
Контрольные состояния деградации не опубликованы.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectScenario = (value: string) => {
|
||||
const nextId = value as E35DegradationKind;
|
||||
const nextScenario = result.reviewScenarios.find(
|
||||
(item) => item.scenarioId === nextId,
|
||||
);
|
||||
setScenarioId(nextId);
|
||||
setFrameIndex(defaultFrame(nextScenario ?? scenario)?.frameIndex ?? 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="e35-degradation-evidence">
|
||||
<LaboratoryEvidenceViewer
|
||||
label="Деградация и восстановление E35"
|
||||
mode={mode}
|
||||
modes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "План" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={(
|
||||
<div className="e35-degradation-evidence__selectors">
|
||||
<Select
|
||||
label="Сценарий отказа"
|
||||
value={scenario.scenarioId}
|
||||
options={result.reviewScenarios.map((item) => ({
|
||||
value: item.scenarioId,
|
||||
label: SCENARIO_LABELS[item.scenarioId],
|
||||
}))}
|
||||
variant="split"
|
||||
menuWidth="anchor"
|
||||
onChange={selectScenario}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
overlay={(
|
||||
<>
|
||||
<div className="e35-degradation-evidence__timeline">
|
||||
{scenario.frames.map((item, index) => (
|
||||
<Button
|
||||
key={item.frameIndex}
|
||||
size="compact"
|
||||
variant={item.frameIndex === frame.frameIndex
|
||||
? "primary"
|
||||
: "secondary"}
|
||||
onClick={() => setFrameIndex(item.frameIndex)}
|
||||
>
|
||||
{index + 1}
|
||||
{" · "}
|
||||
{PHASE_LABELS[item.faultPhase]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<dl className="e35-degradation-evidence__telemetry">
|
||||
<div>
|
||||
<dt>Сценарий / фаза</dt>
|
||||
<dd>
|
||||
{SCENARIO_LABELS[frame.kind]}
|
||||
{" · "}
|
||||
{PHASE_LABELS[frame.faultPhase]}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Кадр / действие</dt>
|
||||
<dd>
|
||||
{formatNumber(frame.frameIndex, 0)}
|
||||
{" · "}
|
||||
{frame.action}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Точки до / после</dt>
|
||||
<dd>
|
||||
{formatNumber(frame.inputPointRows, 0)}
|
||||
{" / "}
|
||||
{formatNumber(frame.transformedPointRows, 0)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Слой</dt>
|
||||
<dd>
|
||||
{frame.counts.current} current · {frame.counts.held} held · {frame.counts.expired} expired
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Каналы</dt>
|
||||
<dd>{channelSummary(frame)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<E34TemporalLayerScene frame={renderedFrame} mode={mode} />
|
||||
</LaboratoryEvidenceViewer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function E35Result({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E35DegradationRecoveryResult;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
const maximum = maximumRecovery(result);
|
||||
const unsafe = unsafeClaims(result);
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E35 · деградация и восстановление"
|
||||
description="Проверяли, теряет ли система только неподтверждённую способность при отказе камеры, LiDAR, позы или времени — и возвращается ли она в штатное состояние без скрытого успеха и ложного свободного пространства."
|
||||
status="Безопасная деградация принята"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${rigLabel} · цепочка E32–E34` },
|
||||
{
|
||||
label: "Объём проверки",
|
||||
value: `${formatNumber(metrics.variantCount, 0)} × ${formatNumber(metrics.sourceFrames, 0)} кадров`,
|
||||
},
|
||||
{
|
||||
label: "Инъекции отказов",
|
||||
value: `${formatNumber(metrics.injectionRecords, 0)} записей · полный журнал`,
|
||||
},
|
||||
{ label: "Полномочия", value: "Диагностика · команды и safety выключены" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Станет ли отказ источника явным и ограниченным, или система продолжит публиковать неподтверждённую семантику, метрику либо свободное пространство?",
|
||||
approach: "Шесть неизменяемых полных replay-вариантов получили по одному заранее объявленному отказу: потеря камеры, LiDAR, актуальной позы, просрочка, ограниченные пропуски и рассинхрон 250 мс. Каждый изменённый кадр и его восстановление записаны отдельно.",
|
||||
principalResult: `Да, для этой записи и замороженного профиля. Все ${formatNumber(metrics.variantFrameOutcomes, 0)} исходов закрыты, небезопасных утверждений — ${unsafe}, максимальное восстановление — ${maximum.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с при gate ${result.configuration.maximumRecoverySeconds.toLocaleString("ru-RU")} с.`,
|
||||
limitation: "Это source-scoped shadow qualification. Она не доказывает перенос на другой риг, свободное пространство, traversability, качество детектора, планирование или safety.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: "E32 TrackGeometry → frozen E34 layer → E35 fault variants",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: "Принятая цепочка E32 / E33 / E34",
|
||||
version: `${formatNumber(metrics.sourceFrames, 0)} кадров · map-frame`,
|
||||
role: "неизменяемый TrackGeometry, runtime envelope и temporal layer",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Deterministic degradation transforms",
|
||||
version: `${formatNumber(result.configuration.scenarioCount, 0)} сценариев · 60 кадров каждый`,
|
||||
role: "явно удаляет неподтверждённый канал или разделяет несинхронные evidence claims",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Independent frozen-layer replay",
|
||||
version: `${metrics.variantFrameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс/variant-frame p95`,
|
||||
role: "полный terminal accounting, injection journal, recovery и проверка upstream digest",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ДОКАЗАТЕЛЬСТВО ОТКАЗА И ВОССТАНОВЛЕНИЯ"
|
||||
title="До отказа, потеря способности и возврат текущего evidence"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<E35Evidence result={result} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Все шесть отказов становятся явными и восстанавливаются на следующем кадре"
|
||||
status="14 / 14 gate"
|
||||
statusTone="success"
|
||||
metrics={[
|
||||
{
|
||||
label: "Полное покрытие",
|
||||
value: formatNumber(metrics.variantFrameOutcomes, 0),
|
||||
hint: `${formatNumber(metrics.variantCount, 0)} полных replay-варианта`,
|
||||
},
|
||||
{
|
||||
label: "Макс. восстановление",
|
||||
value: `${maximum.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`,
|
||||
hint: `gate ≤ ${result.configuration.maximumRecoverySeconds.toLocaleString("ru-RU")} с`,
|
||||
},
|
||||
{
|
||||
label: "Небезопасные claims",
|
||||
value: formatNumber(unsafe, 0),
|
||||
hint: "нет hidden success, false free или late return",
|
||||
},
|
||||
{
|
||||
label: "Журнал инъекций",
|
||||
value: formatNumber(metrics.injectionRecords, 0),
|
||||
hint: "каждое изменение имеет исходный и итоговый digest",
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "На RAVNOVES00 потеря камеры, LiDAR, актуальной позы и времени явно понижает доступную способность: evidence становится geometry-only, camera-only, held/expired unknown или отбрасывается. Номинальное current evidence возвращается за 0,086–0,102 с.",
|
||||
notProved: "Работа не доказывает переносимость на другую запись или mount, detector accuracy, свободное пространство, traversability, planner input, навигацию или safety.",
|
||||
decision: "E35 принимается и закрывает A8. Следующий критический gate — A9/E36: аудит каталога и frozen-profile replay на подходящем втором mounted real source без retuning.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -67,6 +67,7 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
|
||||
e32: null,
|
||||
e33: null,
|
||||
e34: null,
|
||||
e35: null,
|
||||
};
|
||||
|
||||
function digestFromContentId(value: string | null | undefined): string | null {
|
||||
@@ -609,7 +610,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
e28.status === "rejected" ? "E28" : null,
|
||||
e29.status === "rejected" ? "E29" : null,
|
||||
e30.status === "rejected" ? "E30" : null,
|
||||
advanced.status === "rejected" ? "E31–E34" : null,
|
||||
advanced.status === "rejected" ? "E31–E35" : null,
|
||||
].filter(Boolean);
|
||||
setEvidenceError(
|
||||
failures.length
|
||||
|
||||
Reference in New Issue
Block a user