feat(lab): separate RAVNOVES transfer from public benchmark

This commit is contained in:
DCCONSTRUCTIONS
2026-07-31 14:35:51 +03:00
parent 55b7821a5c
commit c1b0f6f8a3
15 changed files with 1018 additions and 61 deletions
@@ -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,