fix(polygon): unify dataset review workflow
This commit is contained in:
parent
e97573e221
commit
13c35ff446
|
|
@ -150,7 +150,7 @@ function mergeViewerSettings(
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const runtime = useMissionRuntime();
|
const runtime = useMissionRuntime();
|
||||||
const { selection } = useDevicePluginHost();
|
const { selection } = useDevicePluginHost();
|
||||||
const polygonRunRoute = useMemo(
|
const polygonDatasetRoute = useMemo(
|
||||||
() => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search),
|
() => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
@ -160,7 +160,7 @@ export default function App() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const [activeRoot, setActiveRoot] = useState<RootId | null>(
|
const [activeRoot, setActiveRoot] = useState<RootId | null>(
|
||||||
polygonRunRoute.active ? "polygon" : null,
|
polygonDatasetRoute.active ? "polygon" : null,
|
||||||
);
|
);
|
||||||
const [sourceUrl, setSourceUrl] = useState("");
|
const [sourceUrl, setSourceUrl] = useState("");
|
||||||
const [recordedReplay, setRecordedReplay] = useState<ObservationSessionReplayLaunch | null>(null);
|
const [recordedReplay, setRecordedReplay] = useState<ObservationSessionReplayLaunch | null>(null);
|
||||||
|
|
@ -194,7 +194,7 @@ export default function App() {
|
||||||
const replayActiveRef = useRef(false);
|
const replayActiveRef = useRef(false);
|
||||||
const sceneSettingsCommitterActiveRef = useRef(true);
|
const sceneSettingsCommitterActiveRef = useRef(true);
|
||||||
const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);
|
const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);
|
||||||
const polygonRunRouteOpenedRef = useRef(false);
|
const polygonDatasetRouteOpenedRef = useRef(false);
|
||||||
|
|
||||||
const currentRoot = rootById(activeRoot);
|
const currentRoot = rootById(activeRoot);
|
||||||
const visibleRoots = roots;
|
const visibleRoots = roots;
|
||||||
|
|
@ -214,10 +214,10 @@ export default function App() {
|
||||||
replayActiveRef.current = replayActive;
|
replayActiveRef.current = replayActive;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!polygonRunRoute.active || polygonRunRouteOpenedRef.current) return;
|
if (!polygonDatasetRoute.active || polygonDatasetRouteOpenedRef.current) return;
|
||||||
polygonRunRouteOpenedRef.current = true;
|
polygonDatasetRouteOpenedRef.current = true;
|
||||||
workspace.openView("polygon-run");
|
workspace.openView("polygon-datasets");
|
||||||
}, [polygonRunRoute.active, workspace]);
|
}, [polygonDatasetRoute.active, workspace]);
|
||||||
|
|
||||||
if (!sceneSettingsCommitterRef.current) {
|
if (!sceneSettingsCommitterRef.current) {
|
||||||
sceneSettingsCommitterRef.current = createLatestAsyncCommitter<SceneSettings>({
|
sceneSettingsCommitterRef.current = createLatestAsyncCommitter<SceneSettings>({
|
||||||
|
|
@ -415,7 +415,7 @@ export default function App() {
|
||||||
const selectRoot = (rootId: RootId) => {
|
const selectRoot = (rootId: RootId) => {
|
||||||
setActiveRoot(rootId);
|
setActiveRoot(rootId);
|
||||||
if (rootId === "polygon") {
|
if (rootId === "polygon") {
|
||||||
workspace.openView("polygon-run");
|
workspace.openView("polygon-datasets");
|
||||||
workspace.openNavigation();
|
workspace.openNavigation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -694,7 +694,12 @@ export default function App() {
|
||||||
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
|
<span className="nodedc-admin-panel__nav-icon" aria-hidden="true">
|
||||||
<Icon name="activity" />
|
<Icon name="activity" />
|
||||||
</span>
|
</span>
|
||||||
<span>{rootWorkspaces.length} рабочих поверхностей</span>
|
<span>
|
||||||
|
{rootWorkspaces.length}{" "}
|
||||||
|
{rootWorkspaces.length === 1
|
||||||
|
? "рабочая поверхность"
|
||||||
|
: "рабочих поверхностей"}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -740,8 +745,6 @@ export default function App() {
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : activeDefinition.kind === "polygon-run" ? (
|
|
||||||
<StatusBadge tone="accent">Virtual only</StatusBadge>
|
|
||||||
) : activeDefinition.kind === "polygon-datasets" ? (
|
) : activeDefinition.kind === "polygon-datasets" ? (
|
||||||
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -772,7 +775,6 @@ export default function App() {
|
||||||
livePerceptionLayers={livePerceptionLayers}
|
livePerceptionLayers={livePerceptionLayers}
|
||||||
onLivePerceptionLayersChange={changeLivePerceptionLayers}
|
onLivePerceptionLayersChange={changeLivePerceptionLayers}
|
||||||
observationLayout={observationLayout}
|
observationLayout={observationLayout}
|
||||||
polygonRunRoute={polygonRunRoute}
|
|
||||||
deviceLabel={selection?.model.displayName ?? null}
|
deviceLabel={selection?.model.displayName ?? null}
|
||||||
spatialControls={selection?.SpatialControlsView
|
spatialControls={selection?.SpatialControlsView
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,10 @@ export function decodePolygonRunDetail(payload: unknown): PolygonRunDetail {
|
||||||
export function resolvePolygonRunRoute(search: string): PolygonRunRoute {
|
export function resolvePolygonRunRoute(search: string): PolygonRunRoute {
|
||||||
const parameters = new URLSearchParams(search);
|
const parameters = new URLSearchParams(search);
|
||||||
const workspaceValues = parameters.getAll("workspace");
|
const workspaceValues = parameters.getAll("workspace");
|
||||||
if (workspaceValues.length !== 1 || workspaceValues[0] !== "polygon-run") {
|
if (
|
||||||
|
workspaceValues.length !== 1
|
||||||
|
|| !["polygon-datasets", "polygon-run"].includes(workspaceValues[0])
|
||||||
|
) {
|
||||||
return { active: false, runId: null, error: null };
|
return { active: false, runId: null, error: null };
|
||||||
}
|
}
|
||||||
const runValues = parameters.getAll("run");
|
const runValues = parameters.getAll("run");
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ export type WorkspaceKind =
|
||||||
| "missions"
|
| "missions"
|
||||||
| "catalog"
|
| "catalog"
|
||||||
| "lidar-quality"
|
| "lidar-quality"
|
||||||
| "polygon-datasets"
|
| "polygon-datasets";
|
||||||
| "polygon-run";
|
|
||||||
|
|
||||||
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
|
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
|
||||||
|
|
||||||
|
|
@ -134,7 +133,7 @@ export const roots: RootDefinition[] = [
|
||||||
label: "Полигон",
|
label: "Полигон",
|
||||||
title: "Полигон",
|
title: "Полигон",
|
||||||
eyebrow: "СИМУЛЯЦИЯ И КВАЛИФИКАЦИЯ",
|
eyebrow: "СИМУЛЯЦИЯ И КВАЛИФИКАЦИЯ",
|
||||||
description: "Симуляционные, replay и shadow-прогоны с воспроизводимыми доказательствами.",
|
description: "Датасеты и виртуальные среды для воспроизводимой проверки алгоритмов.",
|
||||||
statement: "Проверять алгоритмы и поведение машины на версионированных входах.",
|
statement: "Проверять алгоритмы и поведение машины на версионированных входах.",
|
||||||
accent: "КВАЛИФИКАЦИЯ И ДОКАЗАТЕЛЬСТВА",
|
accent: "КВАЛИФИКАЦИЯ И ДОКАЗАТЕЛЬСТВА",
|
||||||
},
|
},
|
||||||
|
|
@ -150,17 +149,6 @@ export const roots: RootDefinition[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const workspaces: WorkspaceDefinition[] = [
|
export const workspaces: WorkspaceDefinition[] = [
|
||||||
{
|
|
||||||
id: "polygon-run",
|
|
||||||
root: "polygon",
|
|
||||||
label: "Прогоны",
|
|
||||||
title: "Прогоны",
|
|
||||||
eyebrow: "ПОЛИГОН / ПРОГОНЫ",
|
|
||||||
description: "История simulation, dataset replay и shadow-запусков с выбором конкретного прогона.",
|
|
||||||
icon: "activity",
|
|
||||||
kind: "polygon-run",
|
|
||||||
groups: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "polygon-datasets",
|
id: "polygon-datasets",
|
||||||
root: "polygon",
|
root: "polygon",
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,8 @@
|
||||||
.polygon-review-lead,
|
.polygon-review-lead,
|
||||||
.polygon-review-player__header,
|
.polygon-review-player__header,
|
||||||
.polygon-review-order,
|
.polygon-review-order,
|
||||||
.polygon-review-summary > header {
|
.polygon-review-summary > header,
|
||||||
|
.dataset-fragment-analysis > header {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +218,16 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis__verdict {
|
||||||
|
min-width: 0;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison,
|
||||||
|
.dataset-fragment-risks {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.polygon-run-picker,
|
.polygon-run-picker,
|
||||||
.dataset-result-select,
|
.dataset-result-select,
|
||||||
.dataset-sequence-select,
|
.dataset-sequence-select,
|
||||||
|
|
|
||||||
|
|
@ -1441,6 +1441,151 @@
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--station-panel);
|
||||||
|
padding: 0.9rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis h3,
|
||||||
|
.dataset-fragment-analysis p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis h3 {
|
||||||
|
margin-top: 0.28rem;
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis p {
|
||||||
|
max-width: 48rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis__verdict {
|
||||||
|
display: grid;
|
||||||
|
min-width: 14rem;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 0.12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis__verdict strong {
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-analysis__verdict span,
|
||||||
|
.dataset-fragment-analysis__verdict small {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.54rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison article {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(7rem, 1fr) auto auto auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-radius: 0.78rem;
|
||||||
|
background: rgb(255 255 255 / 0.035);
|
||||||
|
padding: 0.7rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison article > span,
|
||||||
|
.dataset-fragment-comparison small {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.52rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison article > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison strong {
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison i {
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison em {
|
||||||
|
color: rgb(var(--nodedc-danger-rgb));
|
||||||
|
font-size: 0.58rem;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-comparison em[data-positive="true"] {
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks button {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.18rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.78rem;
|
||||||
|
outline: 0;
|
||||||
|
background: rgb(255 255 255 / 0.025);
|
||||||
|
color: inherit;
|
||||||
|
padding: 0.68rem 0.75rem;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks button:hover,
|
||||||
|
.dataset-fragment-risks button:focus-visible {
|
||||||
|
background: rgb(255 255 255 / 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks span,
|
||||||
|
.dataset-fragment-risks small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--nodedc-text-muted);
|
||||||
|
font-size: 0.52rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-fragment-risks strong {
|
||||||
|
color: var(--nodedc-text-primary);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
.polygon-review-summary,
|
.polygon-review-summary,
|
||||||
.polygon-review-unavailable,
|
.polygon-review-unavailable,
|
||||||
.polygon-review-analysis,
|
.polygon-review-analysis,
|
||||||
|
|
|
||||||
|
|
@ -125,18 +125,17 @@ export function DatasetGatewayWorkspace() {
|
||||||
<span className="section-eyebrow">ЗАЧЕМ ЭТО НУЖНО</span>
|
<span className="section-eyebrow">ЗАЧЕМ ЭТО НУЖНО</span>
|
||||||
<h2>Независимая проверка алгоритмов</h2>
|
<h2>Независимая проверка алгоритмов</h2>
|
||||||
<p>
|
<p>
|
||||||
Датасет — это вход квалификационного прогона. Мы подаём известные
|
Датасет содержит известные размеченные данные. Мы показываем
|
||||||
размеченные данные в выбранный pipeline, измеряем ошибки и сохраняем
|
конкретный фрагмент, результаты каждого алгоритма и ошибки
|
||||||
воспроизводимое решение. Он не подмешивается в диагностику
|
относительно разметки. Он не подмешивается в диагностику реального
|
||||||
реального сенсора и не
|
сенсора и не заменяет closed-loop симуляцию.
|
||||||
заменяет closed-loop симуляцию.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ol aria-label="Последовательность квалификации">
|
<ol aria-label="Как использовать датасет">
|
||||||
<li><span>01</span>Выбрать данные</li>
|
<li><span>01</span>Выбрать фрагмент</li>
|
||||||
<li><span>02</span>Запустить pipeline</li>
|
<li><span>02</span>Посмотреть сканы</li>
|
||||||
<li><span>03</span>Сравнить с разметкой</li>
|
<li><span>03</span>Сравнить алгоритмы</li>
|
||||||
<li><span>04</span>Принять решение</li>
|
<li><span>04</span>Проверить риски</li>
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -179,9 +178,10 @@ export function DatasetGatewayWorkspace() {
|
||||||
<span>PUBLIC · OFF-ROAD · LABELED</span>
|
<span>PUBLIC · OFF-ROAD · LABELED</span>
|
||||||
<h3>{catalog.source.displayName}</h3>
|
<h3>{catalog.source.displayName}</h3>
|
||||||
<p>
|
<p>
|
||||||
Validation split: восемь отдельных поездок и 961 разреженный
|
Validation split: восемь фрагментов исходных записей и 961
|
||||||
размеченный оборот VLS-128. Это коллекция native scans для
|
разреженный размеченный оборот VLS-128. Это коллекция native
|
||||||
perception, а не непрерывное видео или траектория машины.
|
scans для perception, а не непрерывное видео или траектория
|
||||||
|
машины.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,20 +232,12 @@ export function DatasetGatewayWorkspace() {
|
||||||
disabled={catalog.source.admissionStatus !== "frame-ready"}
|
disabled={catalog.source.admissionStatus !== "frame-ready"}
|
||||||
title={
|
title={
|
||||||
catalog.source.admissionStatus === "frame-ready"
|
catalog.source.admissionStatus === "frame-ready"
|
||||||
? "Открыть все доступные последовательности и scans"
|
? "Открыть фрагменты, сканы и результаты анализа"
|
||||||
: "Размеченные scans ещё не импортированы"
|
: "Размеченные сканы ещё не импортированы"
|
||||||
}
|
}
|
||||||
onClick={() => setPreviewOpen((value) => !value)}
|
onClick={() => setPreviewOpen((value) => !value)}
|
||||||
>
|
>
|
||||||
{previewOpen ? "Закрыть источник" : "Открыть источник"}
|
{previewOpen ? "Закрыть датасет" : "Открыть датасет"}
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="compact"
|
|
||||||
variant="secondary"
|
|
||||||
title="Открыть read-only архив квалификационных прогонов"
|
|
||||||
onClick={() => window.location.assign("/?workspace=polygon-run")}
|
|
||||||
>
|
|
||||||
Открыть прогоны
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -254,20 +246,20 @@ export function DatasetGatewayWorkspace() {
|
||||||
|
|
||||||
{previewOpen && selectedRunId ? (
|
{previewOpen && selectedRunId ? (
|
||||||
<>
|
<>
|
||||||
{gooseRuns.length > 0 ? (
|
{gooseRuns.length > 1 ? (
|
||||||
<div className="dataset-result-select">
|
<div className="dataset-result-select">
|
||||||
<div>
|
<div>
|
||||||
<span className="section-eyebrow">НАЛОЖЕНИЕ РЕЗУЛЬТАТА</span>
|
<span className="section-eyebrow">РЕЗУЛЬТАТЫ ОБРАБОТКИ</span>
|
||||||
<strong>Квалификационный прогон</strong>
|
<strong>Версия анализа</strong>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
aria-label="Выбрать прогон для наложения"
|
aria-label="Выбрать версию анализа"
|
||||||
value={selectedRunId}
|
value={selectedRunId}
|
||||||
onChange={(event) => setSelectedRunId(event.target.value)}
|
onChange={(event) => setSelectedRunId(event.target.value)}
|
||||||
>
|
>
|
||||||
{gooseRuns.map((item) => (
|
{gooseRuns.map((item) => (
|
||||||
<option key={item.runId} value={item.runId}>
|
<option key={item.runId} value={item.runId}>
|
||||||
{item.scenarioGeneration} · {item.runId}
|
{item.profileGeneration}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -278,8 +270,8 @@ export function DatasetGatewayWorkspace() {
|
||||||
) : previewOpen ? (
|
) : previewOpen ? (
|
||||||
<section className="polygon-review-unavailable">
|
<section className="polygon-review-unavailable">
|
||||||
<StatusBadge tone="warning">Review недоступен</StatusBadge>
|
<StatusBadge tone="warning">Review недоступен</StatusBadge>
|
||||||
<h3>Источник принят, но покадровый preview не опубликован</h3>
|
<h3>Датасет принят, но результаты анализа не опубликованы</h3>
|
||||||
<p>{runError ?? "Нет квалификационного прогона с bounded frame-pack."}</p>
|
<p>{runError ?? "Нет совместимой версии покадрового анализа."}</p>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,37 @@ export interface GooseDatasetSequence {
|
||||||
maximumGapSeconds: number;
|
maximumGapSeconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SequenceMetricComparison {
|
||||||
|
current: number;
|
||||||
|
patchwork: number;
|
||||||
|
delta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GooseDatasetSequenceAnalysis {
|
||||||
|
groundIou: SequenceMetricComparison;
|
||||||
|
naturalGroundRecall: SequenceMetricComparison;
|
||||||
|
obstacleNonGroundRecall: SequenceMetricComparison;
|
||||||
|
patchworkBetterFrames: number;
|
||||||
|
currentBetterFrames: number;
|
||||||
|
unchangedFrames: number;
|
||||||
|
weakestPatchworkFrame: GroundReviewFrameSummary;
|
||||||
|
worstRegressionFrame: GroundReviewFrameSummary;
|
||||||
|
largestGapFrame: GroundReviewFrameSummary | null;
|
||||||
|
largestGapSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
|
const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
|
||||||
["intensity", "Исходный скан"],
|
["intensity", "Исходный скан"],
|
||||||
["ground-truth", "Ground truth"],
|
["ground-truth", "Ground truth"],
|
||||||
["current", "Current"],
|
["current", "Текущий алгоритм"],
|
||||||
["candidate", "Patchwork++"],
|
["candidate", "Patchwork++"],
|
||||||
["disagreement", "Ошибки Current"],
|
["disagreement", "Ошибки текущего"],
|
||||||
["candidate-disagreement", "Ошибки Patchwork++"],
|
["candidate-disagreement", "Ошибки Patchwork++"],
|
||||||
];
|
];
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
function errorMessage(error: unknown): string {
|
||||||
if (error instanceof Error && error.message.trim()) return error.message;
|
if (error instanceof Error && error.message.trim()) return error.message;
|
||||||
return "Не удалось открыть последовательности GOOSE.";
|
return "Не удалось открыть фрагменты GOOSE.";
|
||||||
}
|
}
|
||||||
|
|
||||||
function secondsBetween(
|
function secondsBetween(
|
||||||
|
|
@ -78,6 +97,83 @@ export function groupGooseReviewFrames(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function average(
|
||||||
|
frames: GroundReviewFrameSummary[],
|
||||||
|
value: (frame: GroundReviewFrameSummary) => number,
|
||||||
|
): number {
|
||||||
|
return frames.reduce((total, frame) => total + value(frame), 0) / frames.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparison(
|
||||||
|
frames: GroundReviewFrameSummary[],
|
||||||
|
value: (frame: GroundReviewFrameSummary) => {
|
||||||
|
current: number;
|
||||||
|
patchwork: number;
|
||||||
|
},
|
||||||
|
): SequenceMetricComparison {
|
||||||
|
const current = average(frames, (frame) => value(frame).current);
|
||||||
|
const patchwork = average(frames, (frame) => value(frame).patchwork);
|
||||||
|
return { current, patchwork, delta: patchwork - current };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyzeGooseDatasetSequence(
|
||||||
|
sequence: GooseDatasetSequence,
|
||||||
|
): GooseDatasetSequenceAnalysis {
|
||||||
|
const first = sequence.frames[0];
|
||||||
|
if (!first) {
|
||||||
|
throw new Error("Фрагмент датасета не содержит сканов.");
|
||||||
|
}
|
||||||
|
let weakestPatchworkFrame = first;
|
||||||
|
let worstRegressionFrame = first;
|
||||||
|
let largestGapFrame: GroundReviewFrameSummary | null = null;
|
||||||
|
let largestGapSeconds = 0;
|
||||||
|
let patchworkBetterFrames = 0;
|
||||||
|
let currentBetterFrames = 0;
|
||||||
|
let unchangedFrames = 0;
|
||||||
|
|
||||||
|
sequence.frames.forEach((frame, index) => {
|
||||||
|
if (frame.patchwork.groundIou < weakestPatchworkFrame.patchwork.groundIou) {
|
||||||
|
weakestPatchworkFrame = frame;
|
||||||
|
}
|
||||||
|
if (frame.groundIouDelta < worstRegressionFrame.groundIouDelta) {
|
||||||
|
worstRegressionFrame = frame;
|
||||||
|
}
|
||||||
|
if (frame.groundIouDelta > 0.000_001) patchworkBetterFrames += 1;
|
||||||
|
else if (frame.groundIouDelta < -0.000_001) currentBetterFrames += 1;
|
||||||
|
else unchangedFrames += 1;
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const gapSeconds = secondsBetween(sequence.frames[index - 1], frame);
|
||||||
|
if (gapSeconds > largestGapSeconds) {
|
||||||
|
largestGapSeconds = gapSeconds;
|
||||||
|
largestGapFrame = frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
groundIou: comparison(sequence.frames, (frame) => ({
|
||||||
|
current: frame.current.groundIou,
|
||||||
|
patchwork: frame.patchwork.groundIou,
|
||||||
|
})),
|
||||||
|
naturalGroundRecall: comparison(sequence.frames, (frame) => ({
|
||||||
|
current: frame.current.naturalGroundRecall,
|
||||||
|
patchwork: frame.patchwork.naturalGroundRecall,
|
||||||
|
})),
|
||||||
|
obstacleNonGroundRecall: comparison(sequence.frames, (frame) => ({
|
||||||
|
current: frame.current.obstacleNonGroundRecall,
|
||||||
|
patchwork: frame.patchwork.obstacleNonGroundRecall,
|
||||||
|
})),
|
||||||
|
patchworkBetterFrames,
|
||||||
|
currentBetterFrames,
|
||||||
|
unchangedFrames,
|
||||||
|
weakestPatchworkFrame,
|
||||||
|
worstRegressionFrame,
|
||||||
|
largestGapFrame,
|
||||||
|
largestGapSeconds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function sequenceLabel(sequenceId: string): string {
|
function sequenceLabel(sequenceId: string): string {
|
||||||
const date = sequenceId.slice(0, 10);
|
const date = sequenceId.slice(0, 10);
|
||||||
const title = sequenceId.slice(11).replaceAll("_", " ");
|
const title = sequenceId.slice(11).replaceAll("_", " ");
|
||||||
|
|
@ -130,9 +226,11 @@ function frameScene(frame: GroundReviewFrame | null) {
|
||||||
function modeExplanation(mode: LidarGroundViewMode): string {
|
function modeExplanation(mode: LidarGroundViewMode): string {
|
||||||
if (mode === "intensity") return "Один sensor-frame оборот VLS-128; цвет — remission.";
|
if (mode === "intensity") return "Один sensor-frame оборот VLS-128; цвет — remission.";
|
||||||
if (mode === "ground-truth") return "Публичная point-aligned разметка GOOSE.";
|
if (mode === "ground-truth") return "Публичная point-aligned разметка GOOSE.";
|
||||||
if (mode === "current") return "Результат Current для этого исходного скана.";
|
if (mode === "current") return "Результат текущего алгоритма для этого исходного скана.";
|
||||||
if (mode === "candidate") return "Результат Patchwork++ для этого исходного скана.";
|
if (mode === "candidate") return "Результат Patchwork++ для этого исходного скана.";
|
||||||
if (mode === "disagreement") return "Красным отмечены ошибки Current относительно labels.";
|
if (mode === "disagreement") {
|
||||||
|
return "Красным отмечены ошибки текущего алгоритма относительно разметки.";
|
||||||
|
}
|
||||||
return "Красным отмечены ошибки Patchwork++ относительно labels.";
|
return "Красным отмечены ошибки Patchwork++ относительно labels.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +244,7 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
const [frameLoading, setFrameLoading] = useState(false);
|
const [frameLoading, setFrameLoading] = useState(false);
|
||||||
const frameCache = useRef(new Map<string, GroundReviewFrame>());
|
const frameCache = useRef(new Map<string, GroundReviewFrame>());
|
||||||
|
const playerRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -175,6 +274,10 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
const selectedSequence = sequences.find(
|
const selectedSequence = sequences.find(
|
||||||
(sequence) => sequence.datasetSequenceId === selectedSequenceId,
|
(sequence) => sequence.datasetSequenceId === selectedSequenceId,
|
||||||
) ?? sequences[0] ?? null;
|
) ?? sequences[0] ?? null;
|
||||||
|
const sequenceAnalysis = useMemo(
|
||||||
|
() => selectedSequence ? analyzeGooseDatasetSequence(selectedSequence) : null,
|
||||||
|
[selectedSequence],
|
||||||
|
);
|
||||||
const selectedPosition = Math.max(
|
const selectedPosition = Math.max(
|
||||||
0,
|
0,
|
||||||
selectedSequence?.frames.findIndex((frame) => frame.frameId === selectedFrameId) ?? 0,
|
selectedSequence?.frames.findIndex((frame) => frame.frameId === selectedFrameId) ?? 0,
|
||||||
|
|
@ -252,6 +355,18 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
setSelectedFrameId(selectedSequence.frames[bounded]?.frameId ?? null);
|
setSelectedFrameId(selectedSequence.frames[bounded]?.frameId ?? null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inspectFrame = (
|
||||||
|
frame: GroundReviewFrameSummary | null,
|
||||||
|
viewMode: LidarGroundViewMode,
|
||||||
|
) => {
|
||||||
|
if (!selectedSequence || !frame) return;
|
||||||
|
selectPosition(selectedSequence.frames.indexOf(frame));
|
||||||
|
setMode(viewMode);
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
playerRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const selectSequence = (sequenceId: string) => {
|
const selectSequence = (sequenceId: string) => {
|
||||||
const sequence = sequences.find((candidate) => (
|
const sequence = sequences.find((candidate) => (
|
||||||
candidate.datasetSequenceId === sequenceId
|
candidate.datasetSequenceId === sequenceId
|
||||||
|
|
@ -276,7 +391,7 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
return (
|
return (
|
||||||
<section className="polygon-review-unavailable">
|
<section className="polygon-review-unavailable">
|
||||||
<StatusBadge tone="accent">Читаем индекс</StatusBadge>
|
<StatusBadge tone="accent">Читаем индекс</StatusBadge>
|
||||||
<h3>Открываем последовательности GOOSE</h3>
|
<h3>Открываем фрагменты GOOSE</h3>
|
||||||
<p>Загружаем только bounded preview, исходный архив остаётся на worker.</p>
|
<p>Загружаем только bounded preview, исходный архив остаётся на worker.</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
@ -286,23 +401,24 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
<section className="dataset-sequence-review" aria-label="Просмотр GOOSE validation">
|
<section className="dataset-sequence-review" aria-label="Просмотр GOOSE validation">
|
||||||
<header className="dataset-sequence-review__truth">
|
<header className="dataset-sequence-review__truth">
|
||||||
<div>
|
<div>
|
||||||
<span className="section-eyebrow">СТРУКТУРА ИСХОДНИКА</span>
|
<span className="section-eyebrow">СОДЕРЖИМОЕ ДАТАСЕТА</span>
|
||||||
<h2>8 отдельных поездок, а не одно видео</h2>
|
<h2>8 отдельных фрагментов исходных записей</h2>
|
||||||
<p>
|
<p>
|
||||||
Validation ZIP содержит {review.frameCount} разреженных размеченных
|
Validation ZIP содержит {review.frameCount} разреженных размеченных
|
||||||
LiDAR-оборотов. Между ними нет непрерывной траектории машины. Play ниже —
|
LiDAR-оборотов. Каждый фрагмент относится к отдельному эпизоду съёмки.
|
||||||
ускоренный просмотр выбранной поездки, а не воспроизведение реального времени.
|
Play ниже — ускоренный просмотр доступных сканов выбранного фрагмента,
|
||||||
|
а не воспроизведение движения машины в реальном времени.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>Последовательностей</dt><dd>{sequences.length}</dd></div>
|
<div><dt>Фрагментов</dt><dd>{sequences.length}</dd></div>
|
||||||
<div><dt>Размеченных сканов</dt><dd>{review.frameCount}</dd></div>
|
<div><dt>Размеченных сканов</dt><dd>{review.frameCount}</dd></div>
|
||||||
<div><dt>Preview</dt><dd>{review.previewPoints.toLocaleString("ru-RU")} точек</dd></div>
|
<div><dt>Preview</dt><dd>{review.previewPoints.toLocaleString("ru-RU")} точек</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="dataset-sequence-select">
|
<div className="dataset-sequence-select">
|
||||||
<label htmlFor="goose-sequence">Последовательность</label>
|
<label htmlFor="goose-sequence">Фрагмент датасета</label>
|
||||||
<select
|
<select
|
||||||
id="goose-sequence"
|
id="goose-sequence"
|
||||||
value={selectedSequence.datasetSequenceId}
|
value={selectedSequence.datasetSequenceId}
|
||||||
|
|
@ -314,19 +430,19 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
value={sequence.datasetSequenceId}
|
value={sequence.datasetSequenceId}
|
||||||
>
|
>
|
||||||
{sequenceLabel(sequence.datasetSequenceId)}
|
{sequenceLabel(sequence.datasetSequenceId)}
|
||||||
{" · "}{sequence.frames.length} scans
|
{" · "}{sequence.frames.length} сканов
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span>
|
<span>
|
||||||
{selectedSequence.frames.length} размеченных сканов · интервал{" "}
|
{selectedSequence.frames.length} сканов · интервалы{" "}
|
||||||
{formatGap(selectedSequence.minimumGapSeconds)}–{
|
{formatGap(selectedSequence.minimumGapSeconds)}–{
|
||||||
formatGap(selectedSequence.maximumGapSeconds)
|
formatGap(selectedSequence.maximumGapSeconds)
|
||||||
} · охват {formatDuration(selectedSequence.durationSeconds)}
|
} · охват {formatDuration(selectedSequence.durationSeconds)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="polygon-review-player">
|
<section className="polygon-review-player" ref={playerRef}>
|
||||||
<header className="polygon-review-player__header">
|
<header className="polygon-review-player__header">
|
||||||
<div>
|
<div>
|
||||||
<span className="section-eyebrow">
|
<span className="section-eyebrow">
|
||||||
|
|
@ -352,7 +468,7 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
<div className="polygon-review-controls">
|
<div className="polygon-review-controls">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={playing ? "Пауза" : "Воспроизвести последовательность"}
|
aria-label={playing ? "Пауза" : "Воспроизвести фрагмент"}
|
||||||
onClick={() => setPlaying((value) => !value)}
|
onClick={() => setPlaying((value) => !value)}
|
||||||
>
|
>
|
||||||
{playing ? "Пауза" : "Play review"}
|
{playing ? "Пауза" : "Play review"}
|
||||||
|
|
@ -378,7 +494,7 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
min={0}
|
min={0}
|
||||||
max={Math.max(0, selectedSequence.frames.length - 1)}
|
max={Math.max(0, selectedSequence.frames.length - 1)}
|
||||||
value={selectedPosition}
|
value={selectedPosition}
|
||||||
aria-label="Позиция в последовательности"
|
aria-label="Позиция во фрагменте"
|
||||||
onChange={(event) => selectPosition(Number(event.target.value))}
|
onChange={(event) => selectPosition(Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
<span>{selectedPosition + 1} / {selectedSequence.frames.length}</span>
|
<span>{selectedPosition + 1} / {selectedSequence.frames.length}</span>
|
||||||
|
|
@ -411,26 +527,26 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
<small>реальный timestamp, не cadence playback</small>
|
<small>реальный timestamp, не cadence playback</small>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<span>Current · Ground IoU</span>
|
<span>Текущий алгоритм · Ground IoU</span>
|
||||||
<strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
|
<strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
|
||||||
<small>overlay выбранного прогона</small>
|
<small>только выбранный скан</small>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<span>Patchwork++ · Ground IoU</span>
|
<span>Patchwork++ · Ground IoU</span>
|
||||||
<strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
|
<strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
|
||||||
<small>overlay выбранного прогона</small>
|
<small>только выбранный скан</small>
|
||||||
</article>
|
</article>
|
||||||
<article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
|
<article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
|
||||||
<span>Разница</span>
|
<span>Разница</span>
|
||||||
<strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
|
<strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
|
||||||
<small>Patchwork++ минус Current</small>
|
<small>Patchwork++ минус текущий алгоритм</small>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="polygon-review-timeline dataset-sequence-timeline"
|
className="polygon-review-timeline dataset-sequence-timeline"
|
||||||
aria-label={`Все ${selectedSequence.frames.length} сканов последовательности`}
|
aria-label={`Все ${selectedSequence.frames.length} сканов фрагмента`}
|
||||||
>
|
>
|
||||||
{selectedSequence.frames.map((frame, index) => (
|
{selectedSequence.frames.map((frame, index) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -445,11 +561,110 @@ export function GooseDatasetReview({ runId }: GooseDatasetReviewProps) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="polygon-review-timeline-note">
|
<p className="polygon-review-timeline-note">
|
||||||
Риски показывают только доступные размеченные scans этой поездки.
|
Полоса показывает только доступные размеченные сканы этого фрагмента.
|
||||||
Пропуски времени не интерполируются; ракурс сохраняется между соседними scans.
|
Пропуски времени не интерполируются; ракурс сохраняется между соседними сканами.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{sequenceAnalysis ? (
|
||||||
|
<section
|
||||||
|
className="dataset-fragment-analysis"
|
||||||
|
aria-label="Результаты выбранного фрагмента"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span className="section-eyebrow">РЕЗУЛЬТАТЫ ЭТОГО ФРАГМЕНТА</span>
|
||||||
|
<h3>Что изменилось после Patchwork++</h3>
|
||||||
|
<p>
|
||||||
|
Средние значения рассчитаны только по{" "}
|
||||||
|
{selectedSequence.frames.length} сканам выбранного фрагмента.
|
||||||
|
Они не смешиваются с остальными частями датасета.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="dataset-fragment-analysis__verdict">
|
||||||
|
<strong>{formatDelta(sequenceAnalysis.groundIou.delta)}</strong>
|
||||||
|
<span>средний Ground IoU</span>
|
||||||
|
<small>
|
||||||
|
лучше на {sequenceAnalysis.patchworkBetterFrames} · хуже на{" "}
|
||||||
|
{sequenceAnalysis.currentBetterFrames} · без изменения на{" "}
|
||||||
|
{sequenceAnalysis.unchangedFrames}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="dataset-fragment-comparison">
|
||||||
|
{([
|
||||||
|
["Ground IoU", sequenceAnalysis.groundIou],
|
||||||
|
["Natural ground recall", sequenceAnalysis.naturalGroundRecall],
|
||||||
|
["Obstacle preservation", sequenceAnalysis.obstacleNonGroundRecall],
|
||||||
|
] as const).map(([label, metric]) => (
|
||||||
|
<article key={label}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<div>
|
||||||
|
<small>Текущий</small>
|
||||||
|
<strong>{formatPercent(metric.current)}</strong>
|
||||||
|
</div>
|
||||||
|
<i>→</i>
|
||||||
|
<div>
|
||||||
|
<small>Patchwork++</small>
|
||||||
|
<strong>{formatPercent(metric.patchwork)}</strong>
|
||||||
|
</div>
|
||||||
|
<em data-positive={metric.delta >= 0 ? "true" : "false"}>
|
||||||
|
{formatDelta(metric.delta)}
|
||||||
|
</em>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dataset-fragment-risks">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inspectFrame(
|
||||||
|
sequenceAnalysis.worstRegressionFrame,
|
||||||
|
"candidate-disagreement",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Самое сильное ухудшение</span>
|
||||||
|
<strong>{formatDelta(sequenceAnalysis.worstRegressionFrame.groundIouDelta)}</strong>
|
||||||
|
<small>
|
||||||
|
source frame {sequenceAnalysis.worstRegressionFrame.datasetFrameNumber}
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inspectFrame(
|
||||||
|
sequenceAnalysis.weakestPatchworkFrame,
|
||||||
|
"candidate",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Самый слабый результат Patchwork++</span>
|
||||||
|
<strong>
|
||||||
|
{formatPercent(sequenceAnalysis.weakestPatchworkFrame.patchwork.groundIou)}
|
||||||
|
</strong>
|
||||||
|
<small>
|
||||||
|
source frame {sequenceAnalysis.weakestPatchworkFrame.datasetFrameNumber}
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!sequenceAnalysis.largestGapFrame}
|
||||||
|
onClick={() => inspectFrame(
|
||||||
|
sequenceAnalysis.largestGapFrame,
|
||||||
|
"intensity",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Самый большой пропуск записи</span>
|
||||||
|
<strong>{formatGap(sequenceAnalysis.largestGapSeconds)}</strong>
|
||||||
|
<small>
|
||||||
|
{sequenceAnalysis.largestGapFrame
|
||||||
|
? `перед source frame ${sequenceAnalysis.largestGapFrame.datasetFrameNumber}`
|
||||||
|
: "фрагмент содержит один скан"}
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<p className="dataset-sequence-review__raw-note">
|
<p className="dataset-sequence-review__raw-note">
|
||||||
Для настоящего движения машины, odometry и синхронного sensor playback нужен
|
Для настоящего движения машины, odometry и синхронного sensor playback нужен
|
||||||
отдельный raw ROS bag GOOSE с localization. Он не входит в принятый 3.3 ГБ
|
отдельный raw ROS bag GOOSE с localization. Он не входит в принятый 3.3 ГБ
|
||||||
|
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button, StatusBadge } from "@nodedc/ui-react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
fetchGroundQualification,
|
|
||||||
type GroundQualification,
|
|
||||||
} from "../core/polygon/groundQualification";
|
|
||||||
import {
|
|
||||||
fetchPolygonRunCatalog,
|
|
||||||
fetchPolygonRunDetail,
|
|
||||||
type PolygonRunCatalog,
|
|
||||||
type PolygonRunDetail,
|
|
||||||
type PolygonRunRoute,
|
|
||||||
type PolygonRunState,
|
|
||||||
} from "../core/polygon/runArchive";
|
|
||||||
|
|
||||||
interface PolygonRunWorkspaceProps {
|
|
||||||
route: PolygonRunRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateLabels: Record<PolygonRunState, string> = {
|
|
||||||
admitted: "Допущен",
|
|
||||||
starting: "Запускается",
|
|
||||||
running: "Выполняется",
|
|
||||||
paused: "На паузе",
|
|
||||||
stopping: "Останавливается",
|
|
||||||
completed: "Завершён",
|
|
||||||
failed: "Ошибка",
|
|
||||||
aborted: "Прерван",
|
|
||||||
};
|
|
||||||
|
|
||||||
function stateTone(
|
|
||||||
state: PolygonRunState,
|
|
||||||
): "success" | "accent" | "warning" | "danger" | "neutral" {
|
|
||||||
if (state === "completed") return "success";
|
|
||||||
if (state === "running") return "accent";
|
|
||||||
if (state === "failed" || state === "aborted") return "danger";
|
|
||||||
if (state === "starting" || state === "stopping" || state === "paused") return "warning";
|
|
||||||
return "neutral";
|
|
||||||
}
|
|
||||||
|
|
||||||
function runKindLabel(kind: string): string {
|
|
||||||
if (kind === "simulation-closed-loop" || kind === "simulation_closed_loop") {
|
|
||||||
return "Closed-loop simulation";
|
|
||||||
}
|
|
||||||
if (kind === "dataset-replay" || kind === "replay_shadow") {
|
|
||||||
return "Dataset replay";
|
|
||||||
}
|
|
||||||
if (kind === "shadow") return "Live shadow";
|
|
||||||
return kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(value: string | null): string {
|
|
||||||
if (!value) return "—";
|
|
||||||
return new Intl.DateTimeFormat("ru-RU", {
|
|
||||||
dateStyle: "medium",
|
|
||||||
timeStyle: "medium",
|
|
||||||
}).format(new Date(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes(value: number): string {
|
|
||||||
if (value === 0) return "0 Б";
|
|
||||||
const units = ["Б", "КБ", "МБ", "ГБ"];
|
|
||||||
const exponent = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
|
||||||
return `${(value / 1024 ** exponent).toLocaleString("ru-RU", {
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
})} ${units[exponent]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPercent(value: number): string {
|
|
||||||
return new Intl.NumberFormat("ru-RU", {
|
|
||||||
style: "percent",
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDelta(value: number): string {
|
|
||||||
const points = value * 100;
|
|
||||||
return `${points >= 0 ? "+" : ""}${points.toLocaleString("ru-RU", {
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
})} п.п.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
|
||||||
if (error instanceof Error && error.message.trim()) return error.message;
|
|
||||||
return "Не удалось прочитать прогон.";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
|
|
||||||
const [catalog, setCatalog] = useState<PolygonRunCatalog | null>(null);
|
|
||||||
const [detail, setDetail] = useState<PolygonRunDetail | null>(null);
|
|
||||||
const [qualification, setQualification] = useState<GroundQualification | null>(null);
|
|
||||||
const [selectedRunId, setSelectedRunId] = useState<string | null>(route.runId);
|
|
||||||
const [loading, setLoading] = useState(route.error === null);
|
|
||||||
const [error, setError] = useState<string | null>(route.error);
|
|
||||||
const [reloadGeneration, setReloadGeneration] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (route.error) {
|
|
||||||
setLoading(false);
|
|
||||||
setError(route.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const controller = new AbortController();
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const nextCatalog = await fetchPolygonRunCatalog({ signal: controller.signal });
|
|
||||||
if (controller.signal.aborted) return;
|
|
||||||
setCatalog(nextCatalog);
|
|
||||||
const targetRunId = selectedRunId
|
|
||||||
?? route.runId
|
|
||||||
?? nextCatalog.items[0]?.runId
|
|
||||||
?? null;
|
|
||||||
if (!targetRunId) {
|
|
||||||
setDetail(null);
|
|
||||||
setQualification(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextDetail = await fetchPolygonRunDetail(targetRunId, {
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
if (controller.signal.aborted) return;
|
|
||||||
const nextQualification = await fetchGroundQualification(targetRunId, {
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
if (controller.signal.aborted) return;
|
|
||||||
setSelectedRunId(targetRunId);
|
|
||||||
setDetail(nextDetail);
|
|
||||||
setQualification(nextQualification);
|
|
||||||
} catch (loadError) {
|
|
||||||
if (controller.signal.aborted) return;
|
|
||||||
setDetail(null);
|
|
||||||
setQualification(null);
|
|
||||||
setError(errorMessage(loadError));
|
|
||||||
} finally {
|
|
||||||
if (!controller.signal.aborted) setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => controller.abort();
|
|
||||||
}, [reloadGeneration, route.error, route.runId, selectedRunId]);
|
|
||||||
|
|
||||||
if (loading && !detail) {
|
|
||||||
return (
|
|
||||||
<div className="standard-workspace polygon-run-workspace">
|
|
||||||
<section className="polygon-run-message">
|
|
||||||
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
|
|
||||||
<h2>Открываем историю прогонов</h2>
|
|
||||||
<p>Читаем каталог и выбранный воспроизводимый запуск.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="standard-workspace polygon-run-workspace">
|
|
||||||
<section className="polygon-run-message">
|
|
||||||
<StatusBadge tone="danger">Данные недоступны</StatusBadge>
|
|
||||||
<h2>Не удалось открыть прогон</h2>
|
|
||||||
<p>{error}</p>
|
|
||||||
<Button
|
|
||||||
size="compact"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setReloadGeneration((value) => value + 1)}
|
|
||||||
>
|
|
||||||
Повторить
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detail) {
|
|
||||||
return (
|
|
||||||
<div className="standard-workspace polygon-run-workspace">
|
|
||||||
<section className="polygon-run-message">
|
|
||||||
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
|
|
||||||
<h2>Прогонов пока нет</h2>
|
|
||||||
<p>Здесь появятся simulation, dataset replay и shadow-запуски.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { run } = detail;
|
|
||||||
return (
|
|
||||||
<div className="standard-workspace polygon-run-workspace">
|
|
||||||
<section className="polygon-run-picker">
|
|
||||||
<div>
|
|
||||||
<span className="section-eyebrow">ИСТОРИЯ ПОЛИГОНА</span>
|
|
||||||
<strong>Выбранный прогон</strong>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
aria-label="Выбрать прогон"
|
|
||||||
value={run.runId}
|
|
||||||
disabled={!catalog?.items.length}
|
|
||||||
onChange={(event) => setSelectedRunId(event.target.value)}
|
|
||||||
>
|
|
||||||
{(catalog?.items ?? []).map((item) => (
|
|
||||||
<option key={item.runId} value={item.runId}>
|
|
||||||
{runKindLabel(item.kind)} · {item.scenarioGeneration} · {item.runId}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<small>{catalog?.total ?? 0} в архиве</small>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="polygon-review-lead">
|
|
||||||
<div>
|
|
||||||
<span className="section-eyebrow">{runKindLabel(run.kind).toUpperCase()}</span>
|
|
||||||
<h2>{qualification ? "GOOSE · Ground segmentation" : run.scenarioGeneration}</h2>
|
|
||||||
<p>
|
|
||||||
{qualification
|
|
||||||
? "Результат алгоритмического replay. Сам исходный датасет и его sequences открываются в разделе «Датасеты»."
|
|
||||||
: "Воспроизводимая запись запуска Полигона."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<StatusBadge tone={stateTone(run.state)}>{stateLabels[run.state]}</StatusBadge>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{qualification ? (
|
|
||||||
<>
|
|
||||||
<section className="polygon-run-input">
|
|
||||||
<div>
|
|
||||||
<span className="section-eyebrow">ВХОД ПРОГОНА</span>
|
|
||||||
<h3>GOOSE 3D · validation</h3>
|
|
||||||
<p>
|
|
||||||
Восемь отдельных поездок, 961 разреженный размеченный LiDAR scan.
|
|
||||||
Это не closed-loop simulation и не непрерывная запись движения машины.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="compact"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => window.location.assign(
|
|
||||||
`/?workspace=polygon-datasets&run=${encodeURIComponent(run.runId)}`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Открыть исходник
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="polygon-review-summary">
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<span className="section-eyebrow">РЕЗУЛЬТАТ ПРОГОНА</span>
|
|
||||||
<h3>Current против Patchwork++</h3>
|
|
||||||
</div>
|
|
||||||
<StatusBadge tone={qualification.decision.passed ? "success" : "danger"}>
|
|
||||||
{qualification.decision.passed ? "27 / 27 проверок" : "Есть провалы"}
|
|
||||||
</StatusBadge>
|
|
||||||
</header>
|
|
||||||
<p>
|
|
||||||
Оба алгоритма независимо обработали одни и те же 961 scans.
|
|
||||||
Покадровая визуальная проверка теперь находится рядом с самим входом
|
|
||||||
в «Датасетах», а не притворяется симуляцией машины.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<article>
|
|
||||||
<span>Ground IoU</span>
|
|
||||||
<strong>{formatPercent(qualification.current.micro.groundIou)}</strong>
|
|
||||||
<i>→</i>
|
|
||||||
<strong>{formatPercent(qualification.patchwork.micro.groundIou)}</strong>
|
|
||||||
<small>{formatDelta(
|
|
||||||
qualification.patchwork.micro.groundIou
|
|
||||||
- qualification.current.micro.groundIou,
|
|
||||||
)}</small>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>Natural ground recall</span>
|
|
||||||
<strong>{formatPercent(
|
|
||||||
qualification.current.micro.naturalGroundRecall,
|
|
||||||
)}</strong>
|
|
||||||
<i>→</i>
|
|
||||||
<strong>{formatPercent(
|
|
||||||
qualification.patchwork.micro.naturalGroundRecall,
|
|
||||||
)}</strong>
|
|
||||||
<small>{formatDelta(
|
|
||||||
qualification.patchwork.micro.naturalGroundRecall
|
|
||||||
- qualification.current.micro.naturalGroundRecall,
|
|
||||||
)}</small>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>Latency p95</span>
|
|
||||||
<strong>{qualification.current.latencyMs.p95.toLocaleString("ru-RU", {
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
})} мс</strong>
|
|
||||||
<i>→</i>
|
|
||||||
<strong>{qualification.patchwork.latencyMs.p95.toLocaleString("ru-RU", {
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
})} мс</strong>
|
|
||||||
<small>на scan</small>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<details className="polygon-review-analysis">
|
|
||||||
<summary>Acceptance и деградации</summary>
|
|
||||||
<div className="polygon-review-analysis__content">
|
|
||||||
<section>
|
|
||||||
<span className="section-eyebrow">ACCEPTANCE GATES</span>
|
|
||||||
{qualification.checks.map((check) => (
|
|
||||||
<p key={check.checkId} data-passed={check.passed ? "true" : "false"}>
|
|
||||||
<i aria-hidden="true" />
|
|
||||||
<span>{check.checkId}</span>
|
|
||||||
<code>
|
|
||||||
{check.observed.toLocaleString("ru-RU", { maximumFractionDigits: 3 })}
|
|
||||||
{" "}{check.operator}{" "}
|
|
||||||
{check.threshold.toLocaleString("ru-RU", { maximumFractionDigits: 3 })}
|
|
||||||
</code>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<span className="section-eyebrow">ДЕГРАДАЦИИ</span>
|
|
||||||
{Object.entries(qualification.degradations).map(([profileId, aggregate]) => (
|
|
||||||
<p key={profileId}>
|
|
||||||
<strong>{profileId}</strong>
|
|
||||||
<span>IoU {formatPercent(aggregate.micro.groundIou)}</span>
|
|
||||||
<span>Natural {formatPercent(aggregate.micro.naturalGroundRecall)}</span>
|
|
||||||
<span>p95 {aggregate.latencyMs.p95.toLocaleString("ru-RU", {
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
})} мс</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<section className="polygon-review-unavailable">
|
|
||||||
<span className="section-eyebrow">АРХИВНЫЙ ПРОГОН</span>
|
|
||||||
<h3>Для этого запуска нет perception qualification</h3>
|
|
||||||
<p>Техническая идентичность доступна ниже.</p>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<details className="polygon-run-technical">
|
|
||||||
<summary>Технические детали прогона</summary>
|
|
||||||
<div className="polygon-run-technical__content">
|
|
||||||
<dl>
|
|
||||||
<div><dt>Run ID</dt><dd>{run.runId}</dd></div>
|
|
||||||
<div><dt>Тип</dt><dd>{runKindLabel(run.kind)}</dd></div>
|
|
||||||
<div><dt>Сценарий</dt><dd>{run.scenarioGeneration}</dd></div>
|
|
||||||
<div><dt>Профиль</dt><dd>{run.profileGeneration}</dd></div>
|
|
||||||
<div><dt>Mission Core</dt><dd>{run.missionCoreCommit.slice(0, 12)}</dd></div>
|
|
||||||
<div><dt>Начало</dt><dd>{formatTimestamp(run.startedAtUtc)}</dd></div>
|
|
||||||
<div><dt>Завершение</dt><dd>{formatTimestamp(run.endedAtUtc)}</dd></div>
|
|
||||||
</dl>
|
|
||||||
<section>
|
|
||||||
<h4>Провайдеры</h4>
|
|
||||||
{run.providers.map((provider) => (
|
|
||||||
<p key={provider.identifier}>
|
|
||||||
<strong>{provider.identifier}</strong>
|
|
||||||
<span>{provider.version} · {provider.revision.slice(0, 16)}</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h4>Артефакты</h4>
|
|
||||||
{detail.artifacts.map((artifact) => (
|
|
||||||
<p key={artifact.artifactId}>
|
|
||||||
<strong>{artifact.kind}</strong>
|
|
||||||
<span>{formatBytes(artifact.byteLength)} · {artifact.sha256.slice(0, 12)}</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,6 @@ import {
|
||||||
import { ObservationTimeline } from "../components/ObservationTimeline";
|
import { ObservationTimeline } from "../components/ObservationTimeline";
|
||||||
import { FloatingObservationWindow } from "../components/FloatingObservationWindow";
|
import { FloatingObservationWindow } from "../components/FloatingObservationWindow";
|
||||||
import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive";
|
import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive";
|
||||||
import type { PolygonRunRoute } from "../core/polygon/runArchive";
|
|
||||||
import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
|
import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
|
||||||
import type {
|
import type {
|
||||||
RecordedAdmissionPhase,
|
RecordedAdmissionPhase,
|
||||||
|
|
@ -56,7 +55,6 @@ import {
|
||||||
} from "../productModel";
|
} from "../productModel";
|
||||||
import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "../presentation";
|
import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "../presentation";
|
||||||
import type { SceneSettings } from "../sceneSettings";
|
import type { SceneSettings } from "../sceneSettings";
|
||||||
import { PolygonRunWorkspace } from "./PolygonRunWorkspace";
|
|
||||||
import { LidarQualityWorkspace } from "./LidarQualityWorkspace";
|
import { LidarQualityWorkspace } from "./LidarQualityWorkspace";
|
||||||
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";
|
import { DatasetGatewayWorkspace } from "./DatasetGatewayWorkspace";
|
||||||
|
|
||||||
|
|
@ -140,7 +138,6 @@ export interface WorkspaceRendererProps {
|
||||||
cuboids3d: boolean;
|
cuboids3d: boolean;
|
||||||
}) => void;
|
}) => void;
|
||||||
observationLayout: ObservationLayoutController;
|
observationLayout: ObservationLayoutController;
|
||||||
polygonRunRoute: PolygonRunRoute;
|
|
||||||
deviceLabel: string | null;
|
deviceLabel: string | null;
|
||||||
navigation: WorkspaceNavigation;
|
navigation: WorkspaceNavigation;
|
||||||
spatialControls: {
|
spatialControls: {
|
||||||
|
|
@ -1318,8 +1315,6 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
|
||||||
);
|
);
|
||||||
case "polygon-datasets":
|
case "polygon-datasets":
|
||||||
return <DatasetGatewayWorkspace />;
|
return <DatasetGatewayWorkspace />;
|
||||||
case "polygon-run":
|
|
||||||
return <PolygonRunWorkspace route={props.polygonRunRoute} />;
|
|
||||||
case "device":
|
case "device":
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ let fetchGroundQualification;
|
||||||
let GroundQualificationContractError;
|
let GroundQualificationContractError;
|
||||||
let parseGooseFrameId;
|
let parseGooseFrameId;
|
||||||
let groupGooseReviewFrames;
|
let groupGooseReviewFrames;
|
||||||
|
let analyzeGooseDatasetSequence;
|
||||||
|
|
||||||
const aggregate = {
|
const aggregate = {
|
||||||
micro: {
|
micro: {
|
||||||
|
|
@ -81,6 +82,7 @@ before(async () => {
|
||||||
parseGooseFrameId,
|
parseGooseFrameId,
|
||||||
} = await server.ssrLoadModule("/src/core/polygon/groundQualification.ts"));
|
} = await server.ssrLoadModule("/src/core/polygon/groundQualification.ts"));
|
||||||
({
|
({
|
||||||
|
analyzeGooseDatasetSequence,
|
||||||
groupGooseReviewFrames,
|
groupGooseReviewFrames,
|
||||||
} = await server.ssrLoadModule("/src/workspaces/GooseDatasetReview.tsx"));
|
} = await server.ssrLoadModule("/src/workspaces/GooseDatasetReview.tsx"));
|
||||||
});
|
});
|
||||||
|
|
@ -230,6 +232,70 @@ test("keeps independent GOOSE recordings in separate playback sequences", () =>
|
||||||
assert.ok(groups[0].durationSeconds > 1.6);
|
assert.ok(groups[0].durationSeconds > 1.6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("summarizes only the selected dataset fragment", () => {
|
||||||
|
const frames = [
|
||||||
|
{
|
||||||
|
sequence: 0,
|
||||||
|
frameId: "2022-07-22_flight__0071_1658494234334310308",
|
||||||
|
datasetSequenceId: "2022-07-22_flight",
|
||||||
|
datasetFrameNumber: 71,
|
||||||
|
sensorTimestampNs: "1658494234334310308",
|
||||||
|
sourcePointCount: 100_000,
|
||||||
|
pointCount: 12_000,
|
||||||
|
current: {
|
||||||
|
groundIou: 0.4,
|
||||||
|
naturalGroundRecall: 0.5,
|
||||||
|
obstacleNonGroundRecall: 0.9,
|
||||||
|
latencyMs: 100,
|
||||||
|
},
|
||||||
|
patchwork: {
|
||||||
|
groundIou: 0.6,
|
||||||
|
naturalGroundRecall: 0.7,
|
||||||
|
obstacleNonGroundRecall: 0.95,
|
||||||
|
latencyMs: 20,
|
||||||
|
},
|
||||||
|
groundIouDelta: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequence: 1,
|
||||||
|
frameId: "2022-07-22_flight__0072_1658494244334310308",
|
||||||
|
datasetSequenceId: "2022-07-22_flight",
|
||||||
|
datasetFrameNumber: 72,
|
||||||
|
sensorTimestampNs: "1658494244334310308",
|
||||||
|
sourcePointCount: 100_000,
|
||||||
|
pointCount: 12_000,
|
||||||
|
current: {
|
||||||
|
groundIou: 0.7,
|
||||||
|
naturalGroundRecall: 0.8,
|
||||||
|
obstacleNonGroundRecall: 0.94,
|
||||||
|
latencyMs: 100,
|
||||||
|
},
|
||||||
|
patchwork: {
|
||||||
|
groundIou: 0.6,
|
||||||
|
naturalGroundRecall: 0.75,
|
||||||
|
obstacleNonGroundRecall: 0.93,
|
||||||
|
latencyMs: 20,
|
||||||
|
},
|
||||||
|
groundIouDelta: -0.1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const analysis = analyzeGooseDatasetSequence({
|
||||||
|
datasetSequenceId: "2022-07-22_flight",
|
||||||
|
frames,
|
||||||
|
durationSeconds: 10,
|
||||||
|
minimumGapSeconds: 10,
|
||||||
|
maximumGapSeconds: 10,
|
||||||
|
});
|
||||||
|
assert.equal(analysis.groundIou.current, 0.55);
|
||||||
|
assert.equal(analysis.groundIou.patchwork, 0.6);
|
||||||
|
assert.ok(Math.abs(analysis.groundIou.delta - 0.05) < 1e-12);
|
||||||
|
assert.equal(analysis.patchworkBetterFrames, 1);
|
||||||
|
assert.equal(analysis.currentBetterFrames, 1);
|
||||||
|
assert.equal(analysis.worstRegressionFrame.datasetFrameNumber, 72);
|
||||||
|
assert.equal(analysis.largestGapFrame.datasetFrameNumber, 72);
|
||||||
|
assert.equal(analysis.largestGapSeconds, 10);
|
||||||
|
});
|
||||||
|
|
||||||
test("decodes a point-aligned review frame with intensity", () => {
|
test("decodes a point-aligned review frame with intensity", () => {
|
||||||
const decoded = decodeGroundReviewFrame({
|
const decoded = decodeGroundReviewFrame({
|
||||||
schema_version: "missioncore.polygon-ground-review-frame/v1",
|
schema_version: "missioncore.polygon-ground-review-frame/v1",
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ function commandAcceptance(overrides = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Polygon stays visible while worker capability gates live actions", () => {
|
test("Polygon exposes one dataset surface and keeps legacy links compatible", () => {
|
||||||
assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), {
|
assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), {
|
||||||
active: true,
|
active: true,
|
||||||
runId: null,
|
runId: null,
|
||||||
|
|
@ -304,18 +304,27 @@ test("Polygon stays visible while worker capability gates live actions", () => {
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-datasets"), {
|
||||||
|
active: true,
|
||||||
|
runId: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
assert.equal(resolvePolygonRunRoute("?workspace=missions").active, false);
|
assert.equal(resolvePolygonRunRoute("?workspace=missions").active, false);
|
||||||
assert.match(
|
assert.match(
|
||||||
resolvePolygonRunRoute("?workspace=polygon-run&run=../../etc").error,
|
resolvePolygonRunRoute("?workspace=polygon-run&run=../../etc").error,
|
||||||
/некорректный/,
|
/некорректный/,
|
||||||
);
|
);
|
||||||
assert.equal(workspaceById("polygon-run").root, "polygon");
|
assert.equal(workspaceById("polygon-run"), null);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
workspacesForRoot("polygon").some(({ id }) => id === "polygon-run"),
|
workspacesForRoot("polygon").some(({ id }) => id === "polygon-run"),
|
||||||
true,
|
false,
|
||||||
);
|
);
|
||||||
assert.equal(workspaceById("polygon-datasets").root, "polygon");
|
assert.equal(workspaceById("polygon-datasets").root, "polygon");
|
||||||
assert.equal(workspaceById("polygon-datasets").kind, "polygon-datasets");
|
assert.equal(workspaceById("polygon-datasets").kind, "polygon-datasets");
|
||||||
|
assert.deepEqual(
|
||||||
|
workspacesForRoot("polygon").map(({ id }) => id),
|
||||||
|
["polygon-datasets"],
|
||||||
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
|
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
|
||||||
false,
|
false,
|
||||||
|
|
|
||||||
|
|
@ -191,9 +191,10 @@ The archival surface consumes the same append-only run repository through
|
||||||
does not create, chmod or mutate the configured repository and rejects every
|
does not create, chmod or mutate the configured repository and rejects every
|
||||||
write transition. When `/api/v1/polygon/worker` returns an admitted available
|
write transition. When `/api/v1/polygon/worker` returns an admitted available
|
||||||
worker, the Control Station adds `Полигон` as a seventh header root. One header
|
worker, the Control Station adds `Полигон` as a seventh header root. One header
|
||||||
click opens the direct `polygon-run` workspace without a System launcher panel.
|
click opens the single `polygon-datasets` workspace without a System launcher
|
||||||
The old `?workspace=polygon-run[&run=<id>]` URL remains a compatible deep-link
|
panel. The old `?workspace=polygon-run[&run=<id>]` URL remains a compatibility
|
||||||
for review and exact-run selection, not the primary operator navigation.
|
redirect into the dataset workspace; there is no separate Runs item in the
|
||||||
|
operator navigation.
|
||||||
Browser QA loaded the accepted archive and the S1C live worker, rotated and
|
Browser QA loaded the accepted archive and the S1C live worker, rotated and
|
||||||
zoomed the 3D rover, observed live run/provider status, Gazebo sim time and
|
zoomed the 3D rover, observed live run/provider status, Gazebo sim time and
|
||||||
ENU/FLU pose, then completed a clean stop. Start/stop stays behind the backend
|
ENU/FLU pose, then completed a clean stop. Start/stop stays behind the backend
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ Polygon is now a parallel product branch. As of this document:
|
||||||
lifecycle gate;
|
lifecycle gate;
|
||||||
- UI-2 exact generation `60e7916` established `Полигон` as a dedicated header
|
- UI-2 exact generation `60e7916` established `Полигон` as a dedicated header
|
||||||
root only while Mission Core confirms an available Simulation Worker. The
|
root only while Mission Core confirms an available Simulation Worker. The
|
||||||
current product surface keeps that direct entry and exposes a bounded
|
current product surface keeps that direct entry. The operator navigation has
|
||||||
two-item navigation: `Прогоны` and `Датасеты`; it has no launcher or link
|
one implemented item, `Датасеты`; immutable run evidence remains an internal
|
||||||
directory;
|
backend contract rather than a second page;
|
||||||
- the browser-native Three.js scene renders a recognisable procedural Ackermann
|
- the browser-native Three.js scene renders a recognisable procedural Ackermann
|
||||||
Rover, ground grid and ENU trajectory. Orbit supports 360° azimuth, bounded
|
Rover, ground grid and ENU trajectory. Orbit supports 360° azimuth, bounded
|
||||||
vertical inspection from overhead to horizon, zoom, pan, follow and camera
|
vertical inspection from overhead to horizon, zoom, pan, follow and camera
|
||||||
|
|
@ -845,46 +845,50 @@ The product surface and execution capability are intentionally distinct.
|
||||||
capability gates only live lifecycle and command actions, which fail closed and
|
capability gates only live lifecycle and command actions, which fail closed and
|
||||||
show an explicit offline state.
|
show an explicit offline state.
|
||||||
|
|
||||||
The Polygon navigation is task-oriented:
|
The Polygon navigation is task-oriented and currently has one operator
|
||||||
|
surface:
|
||||||
|
|
||||||
- `Прогоны` owns selectable recorded simulation/dataset-replay/shadow history,
|
- `Датасеты` owns admitted replay inputs, their real source structure,
|
||||||
aggregate outcomes, provenance and decisions;
|
operator inspection of source scans and analysis scoped to the selected
|
||||||
- `Датасеты` owns admitted replay inputs, their real source structure and
|
dataset fragment;
|
||||||
operator inspection of source scans. A selected run may be overlaid on the
|
- immutable run identity, aggregate qualification, provenance and artifacts
|
||||||
source, but the source does not move into the run archive.
|
remain available to backend evidence consumers but do not create a separate
|
||||||
|
operator page.
|
||||||
|
|
||||||
Dataset bytes and source artifacts remain in Data storage, but algorithm
|
Dataset bytes and source artifacts remain in Data storage, but algorithm
|
||||||
evaluation does not live under `Данные`. Device-specific scanner diagnostics
|
evaluation does not live under `Данные`. Device-specific scanner diagnostics
|
||||||
remain under Fleet. A dataset preview is not a qualification result; the
|
remain under Fleet. The operator sees metrics in the context of the exact
|
||||||
result is a versioned run with pipeline, sensor profile, metrics, provenance
|
fragment being inspected; global gates, provider pins and artifact identities
|
||||||
and decision.
|
are not mixed into that workflow.
|
||||||
|
|
||||||
Live simulation is a different operator task. A rover scene, worker health and
|
Live simulation is a different operator task. A rover scene, worker health and
|
||||||
start/control actions must not be mounted above an archived dataset replay.
|
start/control actions must not be mounted above an archived dataset replay.
|
||||||
They require a separate live surface that appears only when the Simulation
|
They require a separate future live surface that appears only when the
|
||||||
Worker capability is available. The current `Прогоны` implementation is
|
Simulation Worker capability is available.
|
||||||
read-only and remains fully usable while that worker is offline.
|
|
||||||
|
|
||||||
### UI-0 — read-only run history and dataset inspection
|
### UI-0 — read-only dataset evidence inspection
|
||||||
|
|
||||||
Target S1B proves persisted start/health/stop history. The first direct
|
Target S1B proves persisted start/health/stop history. The first direct
|
||||||
developer-oriented view exposed lifecycle, events, provider pins and artifact
|
developer-oriented view exposed lifecycle, events, provider pins and artifact
|
||||||
hashes on the main screen. Operator review rejected that composition and then
|
hashes on the main screen. Operator review rejected that composition, rejected
|
||||||
rejected treating the GOOSE validation split as a single recording. The
|
treating the GOOSE validation split as a single recording and rejected a
|
||||||
current UI-0 follows two separate operator tasks:
|
separate aggregate Runs page. The current UI-0 is one coherent dataset
|
||||||
|
workflow:
|
||||||
|
|
||||||
- `Прогоны` always shows a run selector, even when the archive currently has
|
- `Датасеты → Открыть датасет` shows the admitted source. GOOSE validation is
|
||||||
only one item. The selected run exposes its kind (`simulation`,
|
explicitly grouped into eight independent fragments; Play is bounded to one
|
||||||
`dataset-replay` or `shadow`), input identity, aggregate result and collapsed
|
fragment and described as accelerated review of sparse annotated scans.
|
||||||
technical evidence.
|
|
||||||
- `Датасеты → Открыть источник` shows the admitted source. GOOSE validation is
|
|
||||||
explicitly grouped into eight independent sequences; Play is bounded to one
|
|
||||||
sequence and described as accelerated review of sparse annotated scans.
|
|
||||||
- The dataset viewer exposes source frame number, nanosecond timestamp gaps,
|
- The dataset viewer exposes source frame number, nanosecond timestamp gaps,
|
||||||
ground truth and optional Current/Patchwork++ overlays from a selected run.
|
ground truth and Current/Patchwork++ overlays.
|
||||||
- The fixed sensor-centric coordinate frame and operator camera are preserved
|
- The fixed sensor-centric coordinate frame and operator camera are preserved
|
||||||
within a sequence. Missing scans, ego trajectory and cross-sequence motion
|
within a fragment. Missing scans, ego trajectory and cross-fragment motion
|
||||||
are never synthesized.
|
are never synthesized.
|
||||||
|
- Below the viewer, metrics are calculated only across the selected fragment:
|
||||||
|
mean Ground IoU, natural-ground recall, obstacle preservation, improved and
|
||||||
|
regressed frame counts, weakest Patchwork++ frame, strongest regression and
|
||||||
|
largest capture gap. Every risk item opens its exact source frame.
|
||||||
|
- Global acceptance gates, run IDs, event journals, provider pins and artifact
|
||||||
|
lists are not part of the operator screen.
|
||||||
|
|
||||||
The accepted `3.3 GB` annotated validation ZIP is not a continuous recording.
|
The accepted `3.3 GB` annotated validation ZIP is not a continuous recording.
|
||||||
Continuous ego-motion requires a separately admitted raw ROS bag and
|
Continuous ego-motion requires a separately admitted raw ROS bag and
|
||||||
|
|
@ -907,7 +911,8 @@ fails closed. The review index exposes no paths or digests. A selected frame is
|
||||||
verified against the content-bound review manifest before its bounded point
|
verified against the content-bound review manifest before its bounded point
|
||||||
preview is decoded. Responses never contain the configured root, raw dataset
|
preview is decoded. Responses never contain the configured root, raw dataset
|
||||||
bytes or command payloads. `?workspace=polygon-run[&run=<run-id>]` remains a
|
bytes or command payloads. `?workspace=polygon-run[&run=<run-id>]` remains a
|
||||||
compatible deep-link into the same product workspace.
|
compatibility redirect into `polygon-datasets`; it does not restore a separate
|
||||||
|
Runs page.
|
||||||
|
|
||||||
UI-0 itself has no PX4 transport or lifecycle/command operations. It remains a
|
UI-0 itself has no PX4 transport or lifecycle/command operations. It remains a
|
||||||
read-only evidence contract backed by the server-owned run repository and is
|
read-only evidence contract backed by the server-owned run repository and is
|
||||||
|
|
|
||||||
|
|
@ -65,18 +65,18 @@ The gateway is not part of device quality diagnostics.
|
||||||
dataset frames into the selected device evidence.
|
dataset frames into the selected device evidence.
|
||||||
- **Наблюдение** opens a concrete live or recorded spatial scene.
|
- **Наблюдение** opens a concrete live or recorded spatial scene.
|
||||||
- **Данные** owns source-of-record, retention, replay preparation and export.
|
- **Данные** owns source-of-record, retention, replay preparation and export.
|
||||||
- **Полигон → Датасеты** lists admitted evaluation inputs and owns source
|
- **Полигон → Датасеты** is the single operator surface for admitted evaluation
|
||||||
inspection: source sequences, sparse annotated scans and optional overlays
|
inputs. Opening one dataset exposes its source fragments, sparse annotated
|
||||||
from a selected qualification run.
|
scans, visual overlays and useful analysis calculated only for the selected
|
||||||
- **Полигон → Прогоны** owns the resulting algorithm comparison, metrics,
|
fragment.
|
||||||
provenance and decision. It does not present the dataset itself as a
|
- Internal replay-run identity, provenance and immutable artifacts remain a
|
||||||
simulation or continuous vehicle recording.
|
backend evidence contract. They are not a second operator navigation item
|
||||||
|
and are not presented as vehicle motion.
|
||||||
|
|
||||||
Before real dataset bytes exist, the catalog explains the blocked storage gate
|
Before real dataset bytes exist, the catalog explains the blocked storage gate
|
||||||
and keeps `Открыть` / `Создать прогон` disabled. `Открыть` becomes available
|
and keeps `Открыть датасет` disabled. It becomes available
|
||||||
only after the worker manifest says `frame-ready` and the bounded preview passes
|
only after the worker manifest says `frame-ready` and the bounded preview passes
|
||||||
its own point-alignment and safety checks. `Создать прогон` remains disabled
|
its own point-alignment and safety checks.
|
||||||
until an executable comparison profile exists.
|
|
||||||
|
|
||||||
## Why public recordings look different
|
## Why public recordings look different
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,10 @@ http://127.0.0.1:8765/
|
||||||
```
|
```
|
||||||
|
|
||||||
After Mission Core confirms the worker, `Полигон` must appear in the header and
|
After Mission Core confirms the worker, `Полигон` must appear in the header and
|
||||||
open the live workspace directly. The
|
open the implemented Polygon workspace directly. The legacy
|
||||||
`?workspace=polygon-run[&run=<qualification-run-id>]` URL remains a compatible
|
`?workspace=polygon-run[&run=<qualification-run-id>]` URL redirects to
|
||||||
deep-link. The live panel must show `Worker готов`. Start/stop calls require an
|
`polygon-datasets`; it is not a separate operator surface. The live panel must
|
||||||
|
show `Worker готов`. Start/stop calls require an
|
||||||
`Idempotency-Key`; the React client generates one per operator intent. Removing
|
`Idempotency-Key`; the React client generates one per operator intent. Removing
|
||||||
the `internal-virtual-only` environment gate must leave status visible while
|
the `internal-virtual-only` environment gate must leave status visible while
|
||||||
disabling lifecycle actions.
|
disabling lifecycle actions.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue