feat(lab): separate RAVNOVES transfer from public benchmark
This commit is contained in:
@@ -19,6 +19,7 @@ import { E38Result } from "./E38Result";
|
||||
import { E39Result } from "./E39Result";
|
||||
import { E40Result } from "./E40Result";
|
||||
import { L3PointPillarsResult } from "./L3PointPillarsResult";
|
||||
import { L31PointPillarsRavnovesResult } from "./L31PointPillarsRavnovesResult";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
@@ -33,9 +34,13 @@ export function advancedLaboratoryWorkOptions(
|
||||
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
|
||||
const available = new Set(index.map(({ workId }) => workId));
|
||||
const options: readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] = [
|
||||
{
|
||||
id: "l31-pointpillars-ravnoves",
|
||||
label: "L3.1 · PointPillars на RAVNOVES00",
|
||||
},
|
||||
{
|
||||
id: "l3-pointpillars-visual-audit",
|
||||
label: "L3 · визуальный аудит PointPillars",
|
||||
label: "L3 · KITTI · внешний PointPillars benchmark",
|
||||
},
|
||||
{ id: "e31-source-binding", label: "LAB E31 · source binding" },
|
||||
{ id: "e32-track-geometry", label: "LAB E32 · TrackGeometry v1" },
|
||||
@@ -90,6 +95,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "l3-pointpillars-visual-audit" && results.l3) {
|
||||
return <L3PointPillarsResult result={results.l3} />;
|
||||
}
|
||||
if (workId === "l31-pointpillars-ravnoves" && results.l31) {
|
||||
return <L31PointPillarsRavnovesResult result={results.l31} />;
|
||||
}
|
||||
if (workId === "e40-perception-product-gate" && results.e40) {
|
||||
return <E40Result rigLabel={rigLabel} result={results.e40} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type {
|
||||
L31PointPillarsRavnovesResult,
|
||||
} from "../../core/laboratory/l31PointPillarsRavnoves";
|
||||
import { L31PointPillarsRavnovesVisual } from "./L31PointPillarsRavnovesVisual";
|
||||
|
||||
function percent(value: number, digits = 1): string {
|
||||
return `${(value * 100).toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: digits,
|
||||
})}%`;
|
||||
}
|
||||
|
||||
export function L31PointPillarsRavnovesResult({
|
||||
result,
|
||||
}: {
|
||||
result: L31PointPillarsRavnovesResult;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="L3.1 · PointPillars на RAVNOVES00"
|
||||
description="Полный последовательный transfer-прогон PointPillars по 4570 реальным LiDAR-кадрам RAVNOVES00. Визуальная производная показывает точки нашего сканера и неподтверждённые боксы модели; внешний KITTI в эту работу не входит."
|
||||
status="Измерено · требуется визуальная ревизия"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{
|
||||
label: "Источник",
|
||||
value: `RAVNOVES00 · ${metrics.frameCount.toLocaleString("ru-RU")} кадров`,
|
||||
},
|
||||
{
|
||||
label: "Гипотезы Vehicle",
|
||||
value: metrics.classCounts.Vehicle.toLocaleString("ru-RU"),
|
||||
},
|
||||
{
|
||||
label: "Исполнение",
|
||||
value: "Worker 006 · 1 последовательный поток · existing Triton",
|
||||
},
|
||||
{
|
||||
label: "Полномочия",
|
||||
value: "Shadow-only · accuracy не принята",
|
||||
},
|
||||
]}
|
||||
brief={{
|
||||
question: "Работает ли текущий LiDAR-native PointPillars на реальном потоке RAVNOVES00 и выглядят ли его объектные гипотезы правдоподобно?",
|
||||
approach: "Lossless replay RAVNOVES00 проверен по SHA-256, каждая map-frame порция связана с ближайшей pose и переведена обратно в sensor-frame XYZI. Все 4570 кадров последовательно пропущены через неизменённый Triton engine; 18 route-wide кадров повторены и опубликованы в 3D/BEV.",
|
||||
principalResult: `Вход и выход прошли контракт на ${percent(metrics.inputAdmissionFraction)} кадров; p95 inference ${metrics.inferenceLatencyMs.p95.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс. Модель выдала ${metrics.predictionCount.toLocaleString("ru-RU")} гипотез, из них ${metrics.classCounts.Vehicle.toLocaleString("ru-RU")} Vehicle.`,
|
||||
limitation: "У RAVNOVES00 нет независимых ориентированных 3D truth-боксов. Поэтому здесь нельзя считать accuracy, TP/FP/FN; боксы являются только гипотезами. Повтор 18 кадров совпал лишь частично, что отдельно блокирует эксплуатационный допуск модели.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "l31-pointpillars-ravnoves/transfer-v1",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.sourcePackId,
|
||||
version: "lossless LiDAR replay v2",
|
||||
role: "RAVNOVES00 point-cloud + best-effort pose",
|
||||
identitySha256: result.sourceLogicalContentSha256,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "map-frame → sensor-frame XYZI",
|
||||
version: "nearest pose ≤ 100 ms",
|
||||
role: "восстановление входной системы координат детектора",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
name: "NVIDIA PointPillars candidate",
|
||||
version: result.model.sourceModelSha256,
|
||||
role: "неизменённый cross-domain LiDAR-native transfer",
|
||||
identitySha256: result.model.engineSha256,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Worker 006 canonical Triton",
|
||||
version: result.resultId,
|
||||
role: "последовательное shadow-исполнение без команд",
|
||||
identitySha256: result.resultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="RAVNOVES00 → ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Точки сканера и гипотезы PointPillars"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<L31PointPillarsRavnovesVisual result={result} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Runtime-контракт выполнен; модель пока не допущена"
|
||||
status="Требует доработки"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
label: "Input admission",
|
||||
value: percent(metrics.inputAdmissionFraction),
|
||||
hint: `${metrics.frameCount.toLocaleString("ru-RU")} / ${metrics.frameCount.toLocaleString("ru-RU")} frames`,
|
||||
},
|
||||
{
|
||||
label: "Inference p95",
|
||||
value: `${metrics.inferenceLatencyMs.p95.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 2,
|
||||
})} мс`,
|
||||
hint: `max ${metrics.inferenceLatencyMs.maximum.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс`,
|
||||
},
|
||||
{
|
||||
label: "Vehicle hypotheses",
|
||||
value: metrics.classCounts.Vehicle.toLocaleString("ru-RU"),
|
||||
hint: `${metrics.framesWithVehiclePredictions.toLocaleString("ru-RU")} кадров с Vehicle`,
|
||||
},
|
||||
{
|
||||
label: "Детерминизм повтора",
|
||||
value: percent(metrics.deterministicReplayFraction),
|
||||
hint: `${metrics.deterministicReplayFrames} sealed visual frames`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный поток RAVNOVES00 принимается текущим PointPillars engine без ошибок контракта; inference на Worker 006 остаётся bounded, а реальные точки сканера и боксы модели доступны для 3D/BEV-проверки.",
|
||||
notProved: "Не доказаны корректность классов, точность и полнота боксов, пригодность для навигации или safety. Без независимой разметки эти гипотезы нельзя называть детекциями.",
|
||||
decision: "Сохранить L3.1 как фактический RAVNOVES transfer baseline. Текущий кандидат не подключать к operational detector: сначала разобрать визуальные боксы, причину частичного детерминизма и измерить независимый ground truth на ограниченном наборе.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon, IconButton, Select } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
fetchL31PointPillarsRavnovesFrame,
|
||||
type L31PointPillarsRavnovesResult,
|
||||
type L31VisualFrame,
|
||||
} from "../../core/laboratory/l31PointPillarsRavnoves";
|
||||
import {
|
||||
L3PointPillarsScene,
|
||||
type L3VisualMode,
|
||||
} from "./L3PointPillarsScene";
|
||||
|
||||
function frameLabel(
|
||||
frame: L31PointPillarsRavnovesResult["frames"][number],
|
||||
): string {
|
||||
return (
|
||||
`${frame.sessionSeconds.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})} с · кадр ${frame.frameId}`
|
||||
+ ` · Vehicle ${frame.classCounts.Vehicle}`
|
||||
);
|
||||
}
|
||||
|
||||
export function L31PointPillarsRavnovesVisual({
|
||||
result,
|
||||
}: {
|
||||
result: L31PointPillarsRavnovesResult;
|
||||
}) {
|
||||
const [selectedFrameId, setSelectedFrameId] = useState(
|
||||
result.frames[0]?.frameId ?? "",
|
||||
);
|
||||
const [frame, setFrame] = useState<L31VisualFrame | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<L3VisualMode>("3d");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFrameId) return;
|
||||
const controller = new AbortController();
|
||||
setFrame(null);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchL31PointPillarsRavnovesFrame(
|
||||
result.resultId,
|
||||
selectedFrameId,
|
||||
{ signal: controller.signal },
|
||||
).then((next) => {
|
||||
if (!controller.signal.aborted) setFrame(next);
|
||||
}).catch((caught: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught.message
|
||||
: "Визуальный кадр L3.1 недоступен.",
|
||||
);
|
||||
}).finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.resultId, selectedFrameId]);
|
||||
|
||||
const selectedIndex = result.frames.findIndex(
|
||||
({ frameId }) => frameId === selectedFrameId,
|
||||
);
|
||||
const navigate = (offset: -1 | 1) => {
|
||||
if (!result.frames.length || selectedIndex < 0) return;
|
||||
const index = (
|
||||
selectedIndex + offset + result.frames.length
|
||||
) % result.frames.length;
|
||||
setSelectedFrameId(result.frames[index].frameId);
|
||||
};
|
||||
|
||||
const controls = (
|
||||
<div className="l3-visual-audit__actions">
|
||||
<div className="l3-visual-audit__pagination">
|
||||
<IconButton
|
||||
label="Предыдущий кадр RAVNOVES00"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий кадр RAVNOVES00"
|
||||
onClick={() => navigate(1)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Select
|
||||
label="Выбрать кадр L3.1 RAVNOVES00"
|
||||
value={selectedFrameId}
|
||||
options={result.frames.map((item) => ({
|
||||
value: item.frameId,
|
||||
label: frameLabel(item),
|
||||
}))}
|
||||
variant="split"
|
||||
menuWidth="anchor"
|
||||
onChange={setSelectedFrameId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const overlay = frame ? (
|
||||
<div className="l3-visual-audit__overlay">
|
||||
<div>
|
||||
<span>RAVNOVES00</span>
|
||||
<strong>
|
||||
{frame.summary.sessionSeconds.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})} с · кадр {frame.frameId}
|
||||
</strong>
|
||||
<small>
|
||||
{frame.modelRangePointCount.toLocaleString("ru-RU")} из{" "}
|
||||
{frame.sourcePointCount.toLocaleString("ru-RU")} точек в range модели
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Гипотезы модели · не ground truth</span>
|
||||
<strong>
|
||||
Vehicle {frame.summary.classCounts.Vehicle}
|
||||
{" · "}Pedestrian {frame.summary.classCounts.Pedestrian}
|
||||
{" · "}Cyclist {frame.summary.classCounts.Cyclist}
|
||||
</strong>
|
||||
<small>
|
||||
{frame.summary.inferenceMs.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 2,
|
||||
})} мс · повтор{" "}
|
||||
{frame.summary.deterministicReplay ? "совпал" : "не совпал"}
|
||||
</small>
|
||||
</div>
|
||||
<div className="l3-visual-audit__legend">
|
||||
<span data-tone="prediction">
|
||||
Бокс · неподтверждённое предсказание PointPillars
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<div className="l3-visual-audit">
|
||||
<LaboratoryEvidenceViewer
|
||||
label="PointPillars на RAVNOVES00"
|
||||
mode={mode}
|
||||
modes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "bev", label: "BEV" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={controls}
|
||||
overlay={overlay}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Открываем выбранный кадр RAVNOVES00</span>
|
||||
</div>
|
||||
) : error || !frame ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<Icon name="alert" size={18} />
|
||||
<span>{error ?? "Визуальный кадр L3.1 недоступен."}</span>
|
||||
</div>
|
||||
) : (
|
||||
<L3PointPillarsScene
|
||||
frame={frame}
|
||||
mode={mode}
|
||||
bevCenterX={0}
|
||||
bevHalfExtent={55}
|
||||
/>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function L3PointPillarsResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="L3 · визуальный аудит PointPillars"
|
||||
description="Визуальная производная полного KITTI transfer-прогона: исходные LiDAR-точки, независимые truth-боксы и предсказания модели сопоставлены тем же глобальным 3D IoU-контрактом. Производная не меняет метрики и не выдает результат за K1 accuracy."
|
||||
description="Визуальная производная полного KITTI transfer-прогона: исходные LiDAR-точки, независимые truth-боксы и предсказания модели сопоставлены тем же глобальным 3D IoU-контрактом. Производная не меняет метрики и не выдаёт результат за точность целевого сканера."
|
||||
status="Требуется визуальная проверка"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
@@ -51,7 +51,7 @@ export function L3PointPillarsResult({
|
||||
question: "Соответствуют ли измеренные провал переноса и почти сплошная ложная занятость фактической геометрии исходных LiDAR-кадров?",
|
||||
approach: "Полный sealed run проверен по hash identity. Global score-order matching повторён с исходными порогами IoU, после чего детерминированно выбраны TP-, FP-, FN- и class-coverage кадры. В браузер поступает только выбранный кадр.",
|
||||
principalResult: `Полный прогон: BEV mAP40 ${percent(metrics.bevMap40)}, 3D mAP40 ${percent(metrics.threeDMap40, 6)}, false occupied ${percent(metrics.falseOccupiedRate)}. Визуальный аудит теперь доступен в 3D и BEV.`,
|
||||
limitation: "Это cross-domain KITTI probe модели, обученной на proprietary solid-state LiDAR. Он проверяет перенос и корректность измерителя, но не доказывает точность K1, camera-first детектор, навигацию или safety.",
|
||||
limitation: "Это cross-domain KITTI probe модели, обученной на proprietary solid-state LiDAR. Он проверяет перенос и корректность измерителя, но не доказывает точность целевого сканера, camera-first детектор, навигацию или safety.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
@@ -131,7 +131,7 @@ export function L3PointPillarsResult({
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный cross-domain прогон воспроизводим, его численные артефакты связаны с исходными LiDAR-кадрами, а TP/FP/FN можно проверить в 3D и BEV без повторного inference.",
|
||||
notProved: "Не доказаны пригодность этой модели для K1, точность camera-first семантики, метрическая геометрия K1 в других условиях, навигация, команды или safety.",
|
||||
notProved: "Не доказаны пригодность этой модели для целевого сканера, точность camera-first семантики, метрическая геометрия в других условиях, навигация, команды или safety.",
|
||||
decision: "Не переносить этот публичный PointPillars-кандидат в operational pipeline. Использовать визуальный аудит для проверки природы провала и сохранить архитектуру camera-first semantics + LiDAR metric geometry как основной продуктовый путь.",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -94,9 +94,16 @@ function addBoxes(
|
||||
export function L3PointPillarsScene({
|
||||
frame,
|
||||
mode,
|
||||
bevCenterX = 30,
|
||||
bevHalfExtent = 42,
|
||||
}: {
|
||||
frame: L3VisualFrame;
|
||||
frame: Pick<
|
||||
L3VisualFrame,
|
||||
"frameId" | "pointsXyzi" | "truthBoxes" | "predictionBoxes"
|
||||
>;
|
||||
mode: L3VisualMode;
|
||||
bevCenterX?: number;
|
||||
bevHalfExtent?: number;
|
||||
}) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const [renderError, setRenderError] = useState<string | null>(null);
|
||||
@@ -138,7 +145,7 @@ export function L3PointPillarsScene({
|
||||
);
|
||||
const pointsMaterial = new THREE.PointsMaterial({
|
||||
color: tokenColor(host, "--nodedc-text-secondary", [187, 190, 196]),
|
||||
size: mode === "bev" ? 1.4 : 1.8,
|
||||
size: mode === "bev" ? 2.2 : 1.8,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
opacity: 0.52,
|
||||
@@ -163,6 +170,11 @@ export function L3PointPillarsScene({
|
||||
"--nodedc-danger-rgb",
|
||||
[255, 98, 112],
|
||||
),
|
||||
"model-prediction": tokenColor(
|
||||
host,
|
||||
"--nodedc-accent-rgb",
|
||||
[111, 181, 251],
|
||||
),
|
||||
};
|
||||
const truthLines = addBoxes(scene, frame.truthBoxes, colors, 0.9);
|
||||
const predictionLines = addBoxes(
|
||||
@@ -190,10 +202,10 @@ export function L3PointPillarsScene({
|
||||
const perspective = new THREE.PerspectiveCamera(52, 1, 0.1, 500);
|
||||
perspective.position.set(-12, 18, 36);
|
||||
const orthographic = new THREE.OrthographicCamera(-40, 40, 40, -40, 0.1, 500);
|
||||
orthographic.position.set(35, 100, 0);
|
||||
orthographic.position.set(bevCenterX + 5, 100, 0);
|
||||
orthographic.up.set(1, 0, 0);
|
||||
const camera = mode === "bev" ? orthographic : perspective;
|
||||
camera.lookAt(30, 0, 0);
|
||||
camera.lookAt(mode === "bev" ? bevCenterX : 30, 0, 0);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = false;
|
||||
@@ -201,7 +213,7 @@ export function L3PointPillarsScene({
|
||||
controls.enablePan = true;
|
||||
controls.enableZoom = true;
|
||||
controls.screenSpacePanning = true;
|
||||
controls.target.set(30, 0, 0);
|
||||
controls.target.set(mode === "bev" ? bevCenterX : 30, 0, 0);
|
||||
controls.update();
|
||||
|
||||
const render = () => renderer.render(scene, camera);
|
||||
@@ -214,7 +226,7 @@ export function L3PointPillarsScene({
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
} else {
|
||||
const horizontal = 42;
|
||||
const horizontal = bevHalfExtent;
|
||||
camera.left = -horizontal;
|
||||
camera.right = horizontal;
|
||||
camera.top = horizontal / (width / height);
|
||||
@@ -242,7 +254,7 @@ export function L3PointPillarsScene({
|
||||
renderer.dispose();
|
||||
renderer.domElement.remove();
|
||||
};
|
||||
}, [frame, mode]);
|
||||
}, [bevCenterX, bevHalfExtent, frame, mode]);
|
||||
|
||||
return (
|
||||
<div className="l3-visual-audit__scene" ref={hostRef}>
|
||||
|
||||
@@ -44,25 +44,17 @@ import {
|
||||
advancedLaboratorySourceSession,
|
||||
advancedLaboratoryWorkOptions,
|
||||
isAdvancedLaboratoryWorkId,
|
||||
type AdvancedLaboratoryWorkId,
|
||||
} from "./AdvancedLaboratoryResult";
|
||||
import {
|
||||
e28LaboratoryBrief, e29LaboratoryBrief,
|
||||
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
|
||||
} from "./laboratoryArchiveBriefs";
|
||||
import { useAdvancedLaboratoryCatalog } from "./useAdvancedLaboratoryCatalog";
|
||||
import { buildLaboratoryProfiles, workOptionsForProfile } from "./laboratoryArchiveProfiles";
|
||||
import type { LaboratoryProfileId, LaboratoryWorkId } from "./laboratoryArchiveProfiles";
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
|
||||
type LaboratoryProfileId = "sensor-fusion" | "published-perception";
|
||||
type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| AdvancedLaboratoryWorkId
|
||||
| `session:${string}`;
|
||||
|
||||
function laboratoryWorkOrdinal(value: string): number {
|
||||
const match = value.match(/\bE(\d+)\b/i);
|
||||
return match ? Number(match[1]) : -1;
|
||||
@@ -667,7 +659,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
label: "LAB E30 · evidence review A2",
|
||||
});
|
||||
}
|
||||
items.push(...advancedLaboratoryWorkOptions(advanced.index));
|
||||
items.push(
|
||||
...advancedLaboratoryWorkOptions(advanced.index).filter(
|
||||
({ id }) => id !== "l3-pointpillars-visual-audit",
|
||||
),
|
||||
);
|
||||
return items.sort(
|
||||
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
|
||||
);
|
||||
@@ -677,29 +673,28 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
e29Result,
|
||||
e30Result,
|
||||
]);
|
||||
const profiles = useMemo(() => {
|
||||
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
|
||||
if (sensorWorks.length) {
|
||||
items.push({
|
||||
id: "sensor-fusion",
|
||||
label: `${rigLabel} · камера + LiDAR · control plane`,
|
||||
});
|
||||
}
|
||||
if (publishedWorks.length) {
|
||||
items.push({
|
||||
id: "published-perception",
|
||||
label: `${rigLabel} · опубликованный perception pipeline`,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [publishedWorks.length, rigLabel, sensorWorks.length]);
|
||||
const workOptions: readonly LaboratoryOption<LaboratoryWorkId>[] =
|
||||
profileId === "sensor-fusion"
|
||||
? sensorWorks
|
||||
: publishedWorks.map((session) => ({
|
||||
const publicBenchmarkWorks = useMemo(
|
||||
() => advancedLaboratoryWorkOptions(advanced.index).filter(
|
||||
({ id }) => id === "l3-pointpillars-visual-audit",
|
||||
),
|
||||
[advanced.index],
|
||||
);
|
||||
const profiles = useMemo(
|
||||
() => buildLaboratoryProfiles({
|
||||
rigLabel,
|
||||
sensorAvailable: sensorWorks.length > 0,
|
||||
publicBenchmarkAvailable: publicBenchmarkWorks.length > 0,
|
||||
publishedAvailable: publishedWorks.length > 0,
|
||||
}),
|
||||
[publicBenchmarkWorks.length, publishedWorks.length, rigLabel, sensorWorks.length],
|
||||
);
|
||||
const publishedWorkOptions = publishedWorks.map((session) => ({
|
||||
id: `session:${session.id}` as const,
|
||||
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
|
||||
}));
|
||||
const workOptions = workOptionsForProfile(
|
||||
profileId, sensorWorks, publicBenchmarkWorks, publishedWorkOptions,
|
||||
);
|
||||
const selectedSessionId = workId.startsWith("session:")
|
||||
? workId.slice("session:".length)
|
||||
: null;
|
||||
@@ -730,6 +725,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
setWorkId(firstWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
} else if (firstProfile.id === "public-benchmarks") {
|
||||
const firstWork = publicBenchmarkWorks[0];
|
||||
if (firstWork) {
|
||||
setWorkId(firstWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
} else {
|
||||
const first = publishedWorks[0];
|
||||
if (first) {
|
||||
@@ -756,6 +757,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
profileId,
|
||||
profiles,
|
||||
publishedWorks,
|
||||
publicBenchmarkWorks,
|
||||
sensorWorks,
|
||||
sessions.state,
|
||||
workId,
|
||||
@@ -770,6 +772,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
if (first) setWorkId(first.id);
|
||||
return;
|
||||
}
|
||||
if (next === "public-benchmarks") {
|
||||
const first = publicBenchmarkWorks[0];
|
||||
if (first) setWorkId(first.id);
|
||||
return;
|
||||
}
|
||||
const first = publishedWorks[0];
|
||||
if (!first) return;
|
||||
const nextWork = `session:${first.id}` as const;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { AdvancedLaboratoryWorkId } from "../../core/laboratory/advancedIndex";
|
||||
|
||||
export type LaboratoryProfileId =
|
||||
| "sensor-fusion"
|
||||
| "public-benchmarks"
|
||||
| "published-perception";
|
||||
|
||||
export type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| AdvancedLaboratoryWorkId
|
||||
| `session:${string}`;
|
||||
|
||||
export function buildLaboratoryProfiles({
|
||||
rigLabel,
|
||||
sensorAvailable,
|
||||
publicBenchmarkAvailable,
|
||||
publishedAvailable,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
sensorAvailable: boolean;
|
||||
publicBenchmarkAvailable: boolean;
|
||||
publishedAvailable: boolean;
|
||||
}): readonly LaboratoryOption<LaboratoryProfileId>[] {
|
||||
const profiles: LaboratoryOption<LaboratoryProfileId>[] = [];
|
||||
if (sensorAvailable) {
|
||||
profiles.push({
|
||||
id: "sensor-fusion",
|
||||
label: `${rigLabel} · камера + LiDAR · control plane`,
|
||||
});
|
||||
}
|
||||
if (publicBenchmarkAvailable) {
|
||||
profiles.push({
|
||||
id: "public-benchmarks",
|
||||
label: "Публичные датасеты · внешний benchmark-контур",
|
||||
});
|
||||
}
|
||||
if (publishedAvailable) {
|
||||
profiles.push({
|
||||
id: "published-perception",
|
||||
label: `${rigLabel} · опубликованный perception pipeline`,
|
||||
});
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
export function workOptionsForProfile(
|
||||
profileId: LaboratoryProfileId,
|
||||
sensorWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
publicBenchmarkWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
publishedWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
): readonly LaboratoryOption<LaboratoryWorkId>[] {
|
||||
return profileId === "sensor-fusion"
|
||||
? sensorWorks
|
||||
: profileId === "public-benchmarks"
|
||||
? publicBenchmarkWorks
|
||||
: publishedWorks;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ function mergeResults(
|
||||
): AdvancedLaboratoryResults {
|
||||
return {
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
e31: next.e31 ?? current.e31,
|
||||
e32: next.e32 ?? current.e32,
|
||||
e33: next.e33 ?? current.e33,
|
||||
|
||||
Reference in New Issue
Block a user