feat(polygon): qualify GOOSE ground providers

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 16:02:20 +03:00
parent 60ba64004b
commit d6decbe05c
14 changed files with 2718 additions and 22 deletions
@@ -275,10 +275,11 @@ export function DatasetGatewayWorkspace() {
</Button>
<Button
size="compact"
disabled
title="Прогон станет доступен после импорта датасета"
variant="secondary"
title="Открыть read-only архив квалификационных прогонов"
onClick={() => window.location.assign("/?workspace=polygon-run")}
>
Создать прогон
Открыть прогоны
</Button>
</div>
</footer>
@@ -5,6 +5,12 @@ import {
StatusBadge,
} from "@nodedc/ui-react";
import {
fetchGroundFailurePreview,
fetchGroundQualification,
type GroundFailurePreview,
type GroundQualification,
} from "../core/polygon/groundQualification";
import {
fetchPolygonRunCatalog,
fetchPolygonRunDetail,
@@ -13,6 +19,10 @@ import {
type PolygonRunRoute,
type PolygonRunState,
} from "../core/polygon/runArchive";
import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
import { PolygonLivePanel } from "./PolygonLivePanel";
interface PolygonRunWorkspaceProps {
@@ -62,6 +72,17 @@ function errorMessage(error: unknown): string {
return "Не удалось прочитать доказательства прогона.";
}
function formatPercent(value: number): string {
return new Intl.NumberFormat("ru-RU", {
style: "percent",
maximumFractionDigits: 1,
}).format(value);
}
function formatMilliseconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс`;
}
export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [catalog, setCatalog] = useState<PolygonRunCatalog | null>(null);
const [detail, setDetail] = useState<PolygonRunDetail | null>(null);
@@ -69,6 +90,12 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [loading, setLoading] = useState(route.error === null);
const [error, setError] = useState<string | null>(route.error);
const [reloadGeneration, setReloadGeneration] = useState(0);
const [qualification, setQualification] = useState<GroundQualification | null>(null);
const [qualificationError, setQualificationError] = useState<string | null>(null);
const [failurePreview, setFailurePreview] = useState<GroundFailurePreview | null>(null);
const [selectedFailureFrameId, setSelectedFailureFrameId] = useState<string | null>(null);
const [failureMode, setFailureMode] =
useState<LidarGroundViewMode>("candidate-disagreement");
useEffect(() => {
if (route.error) {
@@ -106,10 +133,71 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
return () => controller.abort();
}, [reloadGeneration, route.error, route.runId, selectedRunId]);
useEffect(() => {
const runId = detail?.run.runId;
if (!runId) {
setQualification(null);
setQualificationError(null);
return;
}
const controller = new AbortController();
setQualification(null);
setQualificationError(null);
setFailurePreview(null);
setSelectedFailureFrameId(null);
void fetchGroundQualification(runId, { signal: controller.signal })
.then((result) => {
if (controller.signal.aborted) return;
setQualification(result);
setSelectedFailureFrameId(result?.worstFrames[0]?.frameId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setQualificationError(errorMessage(loadError));
});
return () => controller.abort();
}, [detail?.run.runId]);
useEffect(() => {
const runId = qualification?.runId;
if (!runId || !selectedFailureFrameId) {
setFailurePreview(null);
return;
}
const controller = new AbortController();
setFailurePreview(null);
void fetchGroundFailurePreview(runId, selectedFailureFrameId, {
signal: controller.signal,
})
.then((result) => {
if (!controller.signal.aborted) setFailurePreview(result);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setQualificationError(errorMessage(loadError));
});
return () => controller.abort();
}, [qualification?.runId, selectedFailureFrameId]);
const visibleEvents = useMemo(
() => detail ? [...detail.events].reverse() : [],
[detail],
);
const failureFrame = useMemo(() => {
if (!failurePreview) return null;
return {
pointCount: failurePreview.pointCount,
pointsXyzM: failurePreview.pointsXyzM,
intensity0To255: null,
masks: {
currentGround: failurePreview.currentGround,
currentAssigned: failurePreview.evaluated,
candidateGround: failurePreview.patchworkGround,
candidateAssigned: failurePreview.evaluated,
disagreement: failurePreview.currentDisagreement,
candidateDisagreement: failurePreview.patchworkDisagreement,
groundTruthGround: failurePreview.groundTruthGround,
},
};
}, [failurePreview]);
if (loading && !detail) {
return (
@@ -199,6 +287,133 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
</div>
</section>
{qualification ? (
<section className="polygon-qualification" aria-label="Квалификация ground provider">
<header className="polygon-qualification__heading">
<div>
<span className="section-eyebrow">GOOSE VALIDATION · {qualification.frameCount} КАДРОВ</span>
<h3>Current против Patchwork++</h3>
<p>
Публичная разметка проверяет переносимость ground pipeline. Результат остаётся
shadow-only и не даёт права на навигацию или safety.
</p>
</div>
<StatusBadge tone={qualification.decision.passed ? "success" : "danger"}>
{qualification.decision.passed ? "Shadow candidate" : "Gate не пройден"}
</StatusBadge>
</header>
<div className="polygon-qualification__providers">
{([
["Current", qualification.current],
["Patchwork++", qualification.patchwork],
] as const).map(([label, aggregate]) => (
<article key={label}>
<strong>{label}</strong>
<dl>
<div><dt>Ground IoU</dt><dd>{formatPercent(aggregate.micro.groundIou)}</dd></div>
<div><dt>Natural ground</dt><dd>{formatPercent(aggregate.micro.naturalGroundRecall)}</dd></div>
<div><dt>Obstacle recall</dt><dd>{formatPercent(aggregate.micro.obstacleNonGroundRecall)}</dd></div>
<div><dt>Latency p95</dt><dd>{formatMilliseconds(aggregate.latencyMs.p95)}</dd></div>
</dl>
</article>
))}
</div>
<div className="polygon-qualification__grid">
<article className="polygon-qualification__checks">
<span className="section-eyebrow">ЗАРАНЕЕ ЗАФИКСИРОВАННЫЕ GATES</span>
<div>
{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>
))}
</div>
</article>
<article className="polygon-qualification__degradations">
<span className="section-eyebrow">ДЕТЕРМИНИРОВАННЫЕ ДЕГРАДАЦИИ</span>
<div>
{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 {formatMilliseconds(aggregate.latencyMs.p95)}</span>
</p>
))}
</div>
</article>
</div>
<div className="polygon-qualification__failures">
<article>
<span className="section-eyebrow">ХУДШИЕ КАДРЫ</span>
<div>
{qualification.worstFrames.slice(0, 5).map((frame) => (
<button
type="button"
key={frame.frameId}
data-active={frame.frameId === selectedFailureFrameId ? "true" : undefined}
onClick={() => setSelectedFailureFrameId(frame.frameId)}
>
<span>{frame.frameId}</span>
<code>
PW {formatPercent(frame.patchworkGroundIou)} ·
Δ {formatPercent(frame.groundIouDelta)}
</code>
</button>
))}
</div>
</article>
<article className="polygon-qualification__viewer">
<header>
<div>
<span className="section-eyebrow">РАЗБОР ОШИБКИ</span>
<strong>{failurePreview?.frameId ?? "Загружаем кадр"}</strong>
</div>
<div className="lidar-ground-modes" aria-label="Режим отображения ошибки">
{([
["ground-truth", "Ground truth"],
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибка Current"],
["candidate-disagreement", "Ошибка PW++"],
] as const).map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={failureMode === mode ? "true" : undefined}
onClick={() => setFailureMode(mode)}
>
{label}
</button>
))}
</div>
</header>
{failureFrame ? (
<LidarGroundPointCloud frame={failureFrame} mode={failureMode} />
) : (
<div className="polygon-qualification__viewer-empty">
Читаем bounded preview из sealed-артефакта прогона
</div>
)}
</article>
</div>
</section>
) : qualificationError ? (
<div className="polygon-qualification__error">
Квалификационный отчёт недоступен: {qualificationError}
</div>
) : null}
<div className="polygon-run-layout">
<GlassSurface className="polygon-run-catalog" padding="lg">
<header className="polygon-run-panel-heading">