feat(lab): separate RAVNOVES transfer from public benchmark
This commit is contained in:
@@ -14,9 +14,11 @@ import { fetchE34TemporalLayerResult } from "./e34TemporalLayer";
|
||||
import { fetchE35DegradationRecoveryResult } from "./e35DegradationRecovery";
|
||||
import { fetchE40ProductGateResult } from "./e40ProductGate";
|
||||
import { fetchL3PointPillarsVisualAudit } from "./l3PointPillarsVisualAudit";
|
||||
import { fetchL31PointPillarsRavnoves } from "./l31PointPillarsRavnoves";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "l3-pointpillars-visual-audit"
|
||||
| "l31-pointpillars-ravnoves"
|
||||
| "e31-source-binding"
|
||||
| "e32-track-geometry"
|
||||
| "e33-worker-shadow"
|
||||
@@ -35,6 +37,7 @@ export interface AdvancedLaboratoryIndexItem {
|
||||
|
||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"l3-pointpillars-visual-audit",
|
||||
"l31-pointpillars-ravnoves",
|
||||
"e31-source-binding",
|
||||
"e32-track-geometry",
|
||||
"e33-worker-shadow",
|
||||
@@ -48,6 +51,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
|
||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
||||
"l31-pointpillars-ravnoves": "l31-pointpillars-ravnoves",
|
||||
"e31-source-binding": "e31-source-qualification",
|
||||
"e32-track-geometry": "e32-track-geometry",
|
||||
"e33-worker-shadow": "e33-worker-shadow",
|
||||
@@ -68,6 +72,7 @@ export function isAdvancedLaboratoryWorkId(
|
||||
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
return {
|
||||
l3: null,
|
||||
l31: null,
|
||||
e31: null,
|
||||
e32: null,
|
||||
e33: null,
|
||||
@@ -169,6 +174,7 @@ export function advancedLaboratoryResultAvailable(
|
||||
results: AdvancedLaboratoryResults,
|
||||
): boolean {
|
||||
return workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
||||
: workId === "l31-pointpillars-ravnoves" ? results.l31 !== null
|
||||
: workId === "e31-source-binding" ? results.e31 !== null
|
||||
: workId === "e32-track-geometry" ? results.e32 !== null
|
||||
: workId === "e33-worker-shadow" ? results.e33 !== null
|
||||
@@ -193,6 +199,8 @@ export async function fetchAdvancedLaboratoryResult(
|
||||
const results = emptyAdvancedLaboratoryResults();
|
||||
if (workId === "l3-pointpillars-visual-audit") {
|
||||
results.l3 = await fetchL3PointPillarsVisualAudit({ fetcher, signal });
|
||||
} else if (workId === "l31-pointpillars-ravnoves") {
|
||||
results.l31 = await fetchL31PointPillarsRavnoves({ fetcher, signal });
|
||||
} else if (workId === "e31-source-binding") {
|
||||
results.e31 = await fetchOne(
|
||||
"/api/v1/laboratory/e31/results?limit=1",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type {
|
||||
E31LaboratoryResult,
|
||||
E32LaboratoryResult,
|
||||
E33LaboratoryResult,
|
||||
E37AcceptanceContractResult,
|
||||
E38PerceptionBaselineResult,
|
||||
E39PerceptionRefinementResult,
|
||||
} from "./advancedResults";
|
||||
import type { E34TemporalLayerResult } from "./e34TemporalLayer";
|
||||
import type { E35DegradationRecoveryResult } from "./e35DegradationRecovery";
|
||||
import type { E40PerceptionProductGateResult } from "./e40ProductGate";
|
||||
import type { L3PointPillarsVisualAuditResult } from "./l3PointPillarsVisualAudit";
|
||||
import type { L31PointPillarsRavnovesResult } from "./l31PointPillarsRavnoves";
|
||||
|
||||
export interface AdvancedLaboratoryResults {
|
||||
l3: L3PointPillarsVisualAuditResult | null;
|
||||
l31: L31PointPillarsRavnovesResult | null;
|
||||
e31: E31LaboratoryResult | null;
|
||||
e32: E32LaboratoryResult | null;
|
||||
e33: E33LaboratoryResult | null;
|
||||
e34: E34TemporalLayerResult | null;
|
||||
e35: E35DegradationRecoveryResult | null;
|
||||
e37: E37AcceptanceContractResult | null;
|
||||
e38: E38PerceptionBaselineResult | null;
|
||||
e39: E39PerceptionRefinementResult | null;
|
||||
e40: E40PerceptionProductGateResult | null;
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import {
|
||||
fetchE34TemporalLayerResult,
|
||||
type E34TemporalLayerResult,
|
||||
} from "./e34TemporalLayer";
|
||||
import {
|
||||
fetchE35DegradationRecoveryResult,
|
||||
type E35DegradationRecoveryResult,
|
||||
} from "./e35DegradationRecovery";
|
||||
import {
|
||||
fetchE40ProductGateResult,
|
||||
type E40PerceptionProductGateResult,
|
||||
} from "./e40ProductGate";
|
||||
import { settledCatalogValue } from "./catalogTransport";
|
||||
import type { L3PointPillarsVisualAuditResult } from "./l3PointPillarsVisualAudit";
|
||||
import type { AdvancedLaboratoryResults } from "./advancedLaboratoryResults";
|
||||
export type { AdvancedLaboratoryResults } from "./advancedLaboratoryResults";
|
||||
|
||||
export interface E31LaboratoryResult {
|
||||
resultId: string;
|
||||
@@ -238,19 +236,6 @@ export interface E39PerceptionRefinementResult {
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
export interface AdvancedLaboratoryResults {
|
||||
l3: L3PointPillarsVisualAuditResult | null;
|
||||
e31: E31LaboratoryResult | null;
|
||||
e32: E32LaboratoryResult | null;
|
||||
e33: E33LaboratoryResult | null;
|
||||
e34: E34TemporalLayerResult | null;
|
||||
e35: E35DegradationRecoveryResult | null;
|
||||
e37: E37AcceptanceContractResult | null;
|
||||
e38: E38PerceptionBaselineResult | null;
|
||||
e39: E39PerceptionRefinementResult | null;
|
||||
e40: E40PerceptionProductGateResult | null;
|
||||
}
|
||||
|
||||
export class AdvancedLaboratoryContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -990,5 +975,17 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
const e38 = settledCatalogValue(settled[6]);
|
||||
const e39 = settledCatalogValue(settled[7]);
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return { l3: null, e31, e32, e33, e34, e35, e37, e38, e39, e40 };
|
||||
return {
|
||||
l3: null,
|
||||
l31: null,
|
||||
e31,
|
||||
e32,
|
||||
e33,
|
||||
e34,
|
||||
e35,
|
||||
e37,
|
||||
e38,
|
||||
e39,
|
||||
e40,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
import {
|
||||
AdvancedLaboratoryContractError,
|
||||
type LaboratoryFetch,
|
||||
} from "./advancedResults";
|
||||
import type { L3VisualBox } from "./l3PointPillarsVisualAudit";
|
||||
|
||||
export interface L31ClassCounts {
|
||||
Vehicle: number;
|
||||
Pedestrian: number;
|
||||
Cyclist: number;
|
||||
}
|
||||
|
||||
export interface L31VisualFrameSummary {
|
||||
frameId: string;
|
||||
frameIndex: number;
|
||||
sessionSeconds: number;
|
||||
sourcePointCount: number;
|
||||
predictionCount: number;
|
||||
classCounts: L31ClassCounts;
|
||||
inferenceMs: number;
|
||||
}
|
||||
|
||||
export interface L31PointPillarsRavnovesResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string;
|
||||
status: "cross-domain-transfer-measured-visual-review-required";
|
||||
sourceSessionId: string;
|
||||
sourcePackId: string;
|
||||
sourceLogicalContentSha256: string;
|
||||
model: {
|
||||
name: "pointpillars";
|
||||
sourceModelSha256: string;
|
||||
engineSha256: string;
|
||||
embeddedScoreThreshold: number;
|
||||
};
|
||||
execution: {
|
||||
workerHostId: "worker-006";
|
||||
sequential: true;
|
||||
parallelWorkers: 1;
|
||||
sourcePaced: false;
|
||||
existingTritonOnly: true;
|
||||
};
|
||||
metrics: {
|
||||
frameCount: number;
|
||||
inputAdmissionFraction: number;
|
||||
outputSchemaValidFraction: number;
|
||||
framesWithPredictions: number;
|
||||
framesWithVehiclePredictions: number;
|
||||
predictionCount: number;
|
||||
classCounts: L31ClassCounts;
|
||||
inferenceLatencyMs: { p50: number; p95: number; maximum: number };
|
||||
poseBindingAgeMs: { p50: number; p95: number; maximum: number };
|
||||
deterministicReplayFraction: number;
|
||||
deterministicReplayFrames: number;
|
||||
};
|
||||
frames: readonly L31VisualFrameSummary[];
|
||||
limitations: readonly string[];
|
||||
}
|
||||
|
||||
export interface L31VisualFrame {
|
||||
frameId: string;
|
||||
summary: L31VisualFrameSummary & {
|
||||
deterministicReplay: boolean;
|
||||
replayInferenceMs: number;
|
||||
visualBoxCount: number;
|
||||
visualBoxTruncated: boolean;
|
||||
};
|
||||
sourcePointCount: number;
|
||||
modelRangePointCount: number;
|
||||
sampledPointCount: number;
|
||||
pointsXyzi: readonly number[];
|
||||
truthBoxes: readonly L3VisualBox[];
|
||||
predictionBoxes: readonly L3VisualBox[];
|
||||
}
|
||||
|
||||
const RESULT_ID = /^l31-pointpillars-ravnoves-[a-f0-9]{64}$/;
|
||||
const PACK_ID = /^lidar-replay-pack-[a-f0-9]{64}$/;
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const FRAME_ID = /^[0-9]{6}$/;
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function array(value: unknown, label: string): readonly unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: ожидался массив.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: ожидалась строка.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exact<T extends string>(value: unknown, expected: T, label: string): T {
|
||||
if (value !== expected) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: нарушен контракт.`);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function number(value: unknown, label: string, minimum = 0): number {
|
||||
if (
|
||||
typeof value !== "number"
|
||||
|| !Number.isFinite(value)
|
||||
|| value < minimum
|
||||
) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: неверное число.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function integer(value: unknown, label: string): number {
|
||||
const parsed = number(value, label);
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: ожидалось целое.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function boolean(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: ожидался boolean.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exactBoolean(
|
||||
value: unknown,
|
||||
expected: boolean,
|
||||
label: string,
|
||||
): boolean {
|
||||
const parsed = boolean(value, label);
|
||||
if (parsed !== expected) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: нарушен контракт.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function classCounts(value: unknown, label: string): L31ClassCounts {
|
||||
const counts = record(value, label);
|
||||
if (
|
||||
Object.keys(counts).sort().join(",") !== "Cyclist,Pedestrian,Vehicle"
|
||||
) {
|
||||
throw new AdvancedLaboratoryContractError(`${label}: классы изменились.`);
|
||||
}
|
||||
return {
|
||||
Vehicle: integer(counts.Vehicle, `${label}.Vehicle`),
|
||||
Pedestrian: integer(counts.Pedestrian, `${label}.Pedestrian`),
|
||||
Cyclist: integer(counts.Cyclist, `${label}.Cyclist`),
|
||||
};
|
||||
}
|
||||
|
||||
function distribution(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): { p50: number; p95: number; maximum: number } {
|
||||
const item = record(value, label);
|
||||
return {
|
||||
p50: number(item.p50, `${label}.p50`),
|
||||
p95: number(item.p95, `${label}.p95`),
|
||||
maximum: number(item.maximum, `${label}.maximum`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSummary(value: unknown): L31VisualFrameSummary {
|
||||
const item = record(value, "L3.1 frame");
|
||||
const frameId = string(item.frame_id, "L3.1 frame_id");
|
||||
if (!FRAME_ID.test(frameId)) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 frame_id: неверный формат.");
|
||||
}
|
||||
return {
|
||||
frameId,
|
||||
frameIndex: integer(item.frame_index, "L3.1 frame_index"),
|
||||
sessionSeconds: number(item.session_seconds, "L3.1 session_seconds"),
|
||||
sourcePointCount: integer(
|
||||
item.source_point_count,
|
||||
"L3.1 source_point_count",
|
||||
),
|
||||
predictionCount: integer(
|
||||
item.prediction_count,
|
||||
"L3.1 prediction_count",
|
||||
),
|
||||
classCounts: classCounts(item.class_counts, "L3.1 class_counts"),
|
||||
inferenceMs: number(
|
||||
item.inference_ms,
|
||||
"L3.1 inference_ms",
|
||||
Number.MIN_VALUE,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): L31PointPillarsRavnovesResult {
|
||||
const item = record(value, "L3.1 result");
|
||||
exact(
|
||||
item.schema_version,
|
||||
"missioncore.l31-pointpillars-ravnoves-result/v1",
|
||||
"L3.1 schema_version",
|
||||
);
|
||||
exact(item.access, "read-only", "L3.1 access");
|
||||
const resultId = string(item.result_id, "L3.1 result_id");
|
||||
const sourcePackId = string(item.source_pack_id, "L3.1 source_pack_id");
|
||||
const logicalSha = string(
|
||||
item.source_logical_content_sha256,
|
||||
"L3.1 logical content",
|
||||
);
|
||||
if (
|
||||
!RESULT_ID.test(resultId)
|
||||
|| !PACK_ID.test(sourcePackId)
|
||||
|| !SHA256.test(logicalSha)
|
||||
) {
|
||||
throw new AdvancedLaboratoryContractError(
|
||||
"L3.1: нарушена идентичность результата.",
|
||||
);
|
||||
}
|
||||
const model = record(item.model, "L3.1 model");
|
||||
const execution = record(item.execution, "L3.1 execution");
|
||||
const metrics = record(item.metrics, "L3.1 metrics");
|
||||
const frames = array(item.frames, "L3.1 frames").map(parseSummary);
|
||||
if (
|
||||
!frames.length
|
||||
|| frames.length > 18
|
||||
|| new Set(frames.map(({ frameId }) => frameId)).size !== frames.length
|
||||
) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 frames: неверный каталог.");
|
||||
}
|
||||
return {
|
||||
resultId,
|
||||
createdAtUtc: string(item.created_at_utc, "L3.1 created_at_utc"),
|
||||
status: exact(
|
||||
item.status,
|
||||
"cross-domain-transfer-measured-visual-review-required",
|
||||
"L3.1 status",
|
||||
),
|
||||
sourceSessionId: exact(
|
||||
item.source_session_id,
|
||||
"20260720T065719Z_viewer_live",
|
||||
"L3.1 source_session_id",
|
||||
),
|
||||
sourcePackId,
|
||||
sourceLogicalContentSha256: logicalSha,
|
||||
model: {
|
||||
name: exact(model.name, "pointpillars", "L3.1 model.name"),
|
||||
sourceModelSha256: string(
|
||||
model.source_model_sha256,
|
||||
"L3.1 model sha",
|
||||
),
|
||||
engineSha256: string(model.engine_sha256, "L3.1 engine sha"),
|
||||
embeddedScoreThreshold: number(
|
||||
model.embedded_score_threshold,
|
||||
"L3.1 score threshold",
|
||||
),
|
||||
},
|
||||
execution: {
|
||||
workerHostId: exact(
|
||||
execution.worker_host_id,
|
||||
"worker-006",
|
||||
"L3.1 worker",
|
||||
),
|
||||
sequential: exactBoolean(
|
||||
execution.sequential,
|
||||
true,
|
||||
"L3.1 sequential",
|
||||
) as true,
|
||||
parallelWorkers: (
|
||||
integer(execution.parallel_workers, "L3.1 parallel workers") === 1
|
||||
? 1
|
||||
: (() => {
|
||||
throw new AdvancedLaboratoryContractError(
|
||||
"L3.1 parallel workers: нарушен контракт.",
|
||||
);
|
||||
})()
|
||||
),
|
||||
sourcePaced: exactBoolean(
|
||||
execution.source_paced,
|
||||
false,
|
||||
"L3.1 source paced",
|
||||
) as false,
|
||||
existingTritonOnly: exactBoolean(
|
||||
execution.existing_triton_only,
|
||||
true,
|
||||
"L3.1 Triton",
|
||||
) as true,
|
||||
},
|
||||
metrics: {
|
||||
frameCount: integer(metrics.frame_count, "L3.1 frame count"),
|
||||
inputAdmissionFraction: number(
|
||||
metrics.input_admission_fraction,
|
||||
"L3.1 admission",
|
||||
),
|
||||
outputSchemaValidFraction: number(
|
||||
metrics.output_schema_valid_fraction,
|
||||
"L3.1 schema valid",
|
||||
),
|
||||
framesWithPredictions: integer(
|
||||
metrics.frames_with_predictions,
|
||||
"L3.1 frames with predictions",
|
||||
),
|
||||
framesWithVehiclePredictions: integer(
|
||||
metrics.frames_with_vehicle_predictions,
|
||||
"L3.1 frames with vehicles",
|
||||
),
|
||||
predictionCount: integer(
|
||||
metrics.prediction_count,
|
||||
"L3.1 predictions",
|
||||
),
|
||||
classCounts: classCounts(metrics.class_counts, "L3.1 metric classes"),
|
||||
inferenceLatencyMs: distribution(
|
||||
metrics.inference_latency_ms,
|
||||
"L3.1 latency",
|
||||
),
|
||||
poseBindingAgeMs: distribution(
|
||||
metrics.pose_binding_age_ms,
|
||||
"L3.1 pose age",
|
||||
),
|
||||
deterministicReplayFraction: number(
|
||||
metrics.deterministic_replay_fraction,
|
||||
"L3.1 determinism",
|
||||
),
|
||||
deterministicReplayFrames: integer(
|
||||
metrics.deterministic_replay_frames,
|
||||
"L3.1 determinism frames",
|
||||
),
|
||||
},
|
||||
frames,
|
||||
limitations: array(item.limitations, "L3.1 limitations").map(
|
||||
(entry) => string(entry, "L3.1 limitation"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseBox(value: unknown): L3VisualBox {
|
||||
const box = record(value, "L3.1 box");
|
||||
const center = [
|
||||
number(box.x_m, "L3.1 box.x", -Infinity),
|
||||
number(box.y_m, "L3.1 box.y", -Infinity),
|
||||
number(box.z_m, "L3.1 box.z", -Infinity),
|
||||
] as const;
|
||||
const size = [
|
||||
number(box.length_m, "L3.1 box.length", Number.MIN_VALUE),
|
||||
number(box.width_m, "L3.1 box.width", Number.MIN_VALUE),
|
||||
number(box.height_m, "L3.1 box.height", Number.MIN_VALUE),
|
||||
] as const;
|
||||
const modelClass = string(box.model_class, "L3.1 box.class");
|
||||
if (!["Vehicle", "Pedestrian", "Cyclist"].includes(modelClass)) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 box.class: неизвестен.");
|
||||
}
|
||||
return {
|
||||
benchmarkClass: modelClass,
|
||||
centerXyzM: center,
|
||||
sizeLwhM: size,
|
||||
yawRad: number(box.yaw_rad, "L3.1 box.yaw", -Infinity),
|
||||
status: "model-prediction",
|
||||
score: number(box.score, "L3.1 box.score"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchL31PointPillarsRavnoves({
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<L31PointPillarsRavnovesResult | null> {
|
||||
const response = await fetcher(
|
||||
"/api/v1/laboratory/l31/pointpillars-ravnoves/results?limit=1",
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new AdvancedLaboratoryContractError(
|
||||
`L3.1 RAVNOVES недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const catalog = record(await response.json(), "L3.1 catalog");
|
||||
exact(
|
||||
catalog.schema_version,
|
||||
"missioncore.l31-pointpillars-ravnoves-catalog-results/v1",
|
||||
"L3.1 catalog schema",
|
||||
);
|
||||
const items = array(catalog.items, "L3.1 catalog.items");
|
||||
if (items.length > 1) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 catalog: лишние результаты.");
|
||||
}
|
||||
return items.length ? parseResult(items[0]) : null;
|
||||
}
|
||||
|
||||
export async function fetchL31PointPillarsRavnovesFrame(
|
||||
resultId: string,
|
||||
frameId: string,
|
||||
{
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {},
|
||||
): Promise<L31VisualFrame> {
|
||||
if (!RESULT_ID.test(resultId) || !FRAME_ID.test(frameId)) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 frame: неверная identity.");
|
||||
}
|
||||
const response = await fetcher(
|
||||
`/api/v1/laboratory/l31/pointpillars-ravnoves/${resultId}/frames/${frameId}`,
|
||||
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new AdvancedLaboratoryContractError(
|
||||
`L3.1 frame недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const payload = record(await response.json(), "L3.1 visual frame");
|
||||
exact(
|
||||
payload.schema_version,
|
||||
"missioncore.l31-pointpillars-ravnoves-visual-frame/v1",
|
||||
"L3.1 visual schema",
|
||||
);
|
||||
exact(payload.frame_id, frameId, "L3.1 visual frame_id");
|
||||
const summaryRaw = record(payload.summary, "L3.1 visual summary");
|
||||
const summary = parseSummary(summaryRaw);
|
||||
const points = record(payload.points, "L3.1 points");
|
||||
exact(points.layout, "flat-xyzi", "L3.1 points.layout");
|
||||
const pointValues = array(points.values, "L3.1 points.values").map(
|
||||
(entry, index) => number(entry, `L3.1 points[${index}]`, -Infinity),
|
||||
);
|
||||
const sampledPointCount = integer(
|
||||
points.sampled_point_count,
|
||||
"L3.1 sampled points",
|
||||
);
|
||||
if (sampledPointCount > 12_000 || pointValues.length !== sampledPointCount * 4) {
|
||||
throw new AdvancedLaboratoryContractError("L3.1 points: нарушен bound.");
|
||||
}
|
||||
return {
|
||||
frameId,
|
||||
summary: {
|
||||
...summary,
|
||||
deterministicReplay: boolean(
|
||||
summaryRaw.deterministic_replay,
|
||||
"L3.1 deterministic replay",
|
||||
),
|
||||
replayInferenceMs: number(
|
||||
summaryRaw.replay_inference_ms,
|
||||
"L3.1 replay latency",
|
||||
),
|
||||
visualBoxCount: integer(
|
||||
summaryRaw.visual_box_count,
|
||||
"L3.1 visual box count",
|
||||
),
|
||||
visualBoxTruncated: boolean(
|
||||
summaryRaw.visual_box_truncated,
|
||||
"L3.1 visual box truncated",
|
||||
),
|
||||
},
|
||||
sourcePointCount: integer(
|
||||
points.source_point_count,
|
||||
"L3.1 source points",
|
||||
),
|
||||
modelRangePointCount: integer(
|
||||
points.model_range_point_count,
|
||||
"L3.1 model range points",
|
||||
),
|
||||
sampledPointCount,
|
||||
pointsXyzi: pointValues,
|
||||
truthBoxes: [],
|
||||
predictionBoxes: array(
|
||||
payload.prediction_boxes,
|
||||
"L3.1 prediction boxes",
|
||||
).map(parseBox),
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,12 @@ export interface L3VisualBox {
|
||||
centerXyzM: readonly [number, number, number];
|
||||
sizeLwhM: readonly [number, number, number];
|
||||
yawRad: number;
|
||||
status: "matched" | "false-negative" | "true-positive" | "false-positive";
|
||||
status:
|
||||
| "matched"
|
||||
| "false-negative"
|
||||
| "true-positive"
|
||||
| "false-positive"
|
||||
| "model-prediction";
|
||||
score: number | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,10 @@
|
||||
background: rgb(var(--nodedc-warning-rgb));
|
||||
}
|
||||
|
||||
.l3-visual-audit__legend span[data-tone="prediction"]::before {
|
||||
background: rgb(var(--nodedc-accent-rgb));
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.l3-visual-audit__overlay {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -19,6 +19,7 @@ import { E38Result } from "./E38Result";
|
||||
import { E39Result } from "./E39Result";
|
||||
import { E40Result } from "./E40Result";
|
||||
import { L3PointPillarsResult } from "./L3PointPillarsResult";
|
||||
import { L31PointPillarsRavnovesResult } from "./L31PointPillarsRavnovesResult";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
export { isAdvancedLaboratoryWorkId };
|
||||
@@ -33,9 +34,13 @@ export function advancedLaboratoryWorkOptions(
|
||||
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
|
||||
const available = new Set(index.map(({ workId }) => workId));
|
||||
const options: readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] = [
|
||||
{
|
||||
id: "l31-pointpillars-ravnoves",
|
||||
label: "L3.1 · PointPillars на RAVNOVES00",
|
||||
},
|
||||
{
|
||||
id: "l3-pointpillars-visual-audit",
|
||||
label: "L3 · визуальный аудит PointPillars",
|
||||
label: "L3 · KITTI · внешний PointPillars benchmark",
|
||||
},
|
||||
{ id: "e31-source-binding", label: "LAB E31 · source binding" },
|
||||
{ id: "e32-track-geometry", label: "LAB E32 · TrackGeometry v1" },
|
||||
@@ -90,6 +95,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "l3-pointpillars-visual-audit" && results.l3) {
|
||||
return <L3PointPillarsResult result={results.l3} />;
|
||||
}
|
||||
if (workId === "l31-pointpillars-ravnoves" && results.l31) {
|
||||
return <L31PointPillarsRavnovesResult result={results.l31} />;
|
||||
}
|
||||
if (workId === "e40-perception-product-gate" && results.e40) {
|
||||
return <E40Result rigLabel={rigLabel} result={results.e40} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type {
|
||||
L31PointPillarsRavnovesResult,
|
||||
} from "../../core/laboratory/l31PointPillarsRavnoves";
|
||||
import { L31PointPillarsRavnovesVisual } from "./L31PointPillarsRavnovesVisual";
|
||||
|
||||
function percent(value: number, digits = 1): string {
|
||||
return `${(value * 100).toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: digits,
|
||||
})}%`;
|
||||
}
|
||||
|
||||
export function L31PointPillarsRavnovesResult({
|
||||
result,
|
||||
}: {
|
||||
result: L31PointPillarsRavnovesResult;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="L3.1 · PointPillars на RAVNOVES00"
|
||||
description="Полный последовательный transfer-прогон PointPillars по 4570 реальным LiDAR-кадрам RAVNOVES00. Визуальная производная показывает точки нашего сканера и неподтверждённые боксы модели; внешний KITTI в эту работу не входит."
|
||||
status="Измерено · требуется визуальная ревизия"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
{
|
||||
label: "Источник",
|
||||
value: `RAVNOVES00 · ${metrics.frameCount.toLocaleString("ru-RU")} кадров`,
|
||||
},
|
||||
{
|
||||
label: "Гипотезы Vehicle",
|
||||
value: metrics.classCounts.Vehicle.toLocaleString("ru-RU"),
|
||||
},
|
||||
{
|
||||
label: "Исполнение",
|
||||
value: "Worker 006 · 1 последовательный поток · existing Triton",
|
||||
},
|
||||
{
|
||||
label: "Полномочия",
|
||||
value: "Shadow-only · accuracy не принята",
|
||||
},
|
||||
]}
|
||||
brief={{
|
||||
question: "Работает ли текущий LiDAR-native PointPillars на реальном потоке RAVNOVES00 и выглядят ли его объектные гипотезы правдоподобно?",
|
||||
approach: "Lossless replay RAVNOVES00 проверен по SHA-256, каждая map-frame порция связана с ближайшей pose и переведена обратно в sensor-frame XYZI. Все 4570 кадров последовательно пропущены через неизменённый Triton engine; 18 route-wide кадров повторены и опубликованы в 3D/BEV.",
|
||||
principalResult: `Вход и выход прошли контракт на ${percent(metrics.inputAdmissionFraction)} кадров; p95 inference ${metrics.inferenceLatencyMs.p95.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс. Модель выдала ${metrics.predictionCount.toLocaleString("ru-RU")} гипотез, из них ${metrics.classCounts.Vehicle.toLocaleString("ru-RU")} Vehicle.`,
|
||||
limitation: "У RAVNOVES00 нет независимых ориентированных 3D truth-боксов. Поэтому здесь нельзя считать accuracy, TP/FP/FN; боксы являются только гипотезами. Повтор 18 кадров совпал лишь частично, что отдельно блокирует эксплуатационный допуск модели.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "ai-inference",
|
||||
pipelineId: "l31-pointpillars-ravnoves/transfer-v1",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.sourcePackId,
|
||||
version: "lossless LiDAR replay v2",
|
||||
role: "RAVNOVES00 point-cloud + best-effort pose",
|
||||
identitySha256: result.sourceLogicalContentSha256,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "map-frame → sensor-frame XYZI",
|
||||
version: "nearest pose ≤ 100 ms",
|
||||
role: "восстановление входной системы координат детектора",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
name: "NVIDIA PointPillars candidate",
|
||||
version: result.model.sourceModelSha256,
|
||||
role: "неизменённый cross-domain LiDAR-native transfer",
|
||||
identitySha256: result.model.engineSha256,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Worker 006 canonical Triton",
|
||||
version: result.resultId,
|
||||
role: "последовательное shadow-исполнение без команд",
|
||||
identitySha256: result.resultId.split("-").at(-1) ?? null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="RAVNOVES00 → ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Точки сканера и гипотезы PointPillars"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<L31PointPillarsRavnovesVisual result={result} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Runtime-контракт выполнен; модель пока не допущена"
|
||||
status="Требует доработки"
|
||||
statusTone="warning"
|
||||
metrics={[
|
||||
{
|
||||
label: "Input admission",
|
||||
value: percent(metrics.inputAdmissionFraction),
|
||||
hint: `${metrics.frameCount.toLocaleString("ru-RU")} / ${metrics.frameCount.toLocaleString("ru-RU")} frames`,
|
||||
},
|
||||
{
|
||||
label: "Inference p95",
|
||||
value: `${metrics.inferenceLatencyMs.p95.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 2,
|
||||
})} мс`,
|
||||
hint: `max ${metrics.inferenceLatencyMs.maximum.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс`,
|
||||
},
|
||||
{
|
||||
label: "Vehicle hypotheses",
|
||||
value: metrics.classCounts.Vehicle.toLocaleString("ru-RU"),
|
||||
hint: `${metrics.framesWithVehiclePredictions.toLocaleString("ru-RU")} кадров с Vehicle`,
|
||||
},
|
||||
{
|
||||
label: "Детерминизм повтора",
|
||||
value: percent(metrics.deterministicReplayFraction),
|
||||
hint: `${metrics.deterministicReplayFrames} sealed visual frames`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный поток RAVNOVES00 принимается текущим PointPillars engine без ошибок контракта; inference на Worker 006 остаётся bounded, а реальные точки сканера и боксы модели доступны для 3D/BEV-проверки.",
|
||||
notProved: "Не доказаны корректность классов, точность и полнота боксов, пригодность для навигации или safety. Без независимой разметки эти гипотезы нельзя называть детекциями.",
|
||||
decision: "Сохранить L3.1 как фактический RAVNOVES transfer baseline. Текущий кандидат не подключать к operational detector: сначала разобрать визуальные боксы, причину частичного детерминизма и измерить независимый ground truth на ограниченном наборе.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Icon, IconButton, Select } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import {
|
||||
fetchL31PointPillarsRavnovesFrame,
|
||||
type L31PointPillarsRavnovesResult,
|
||||
type L31VisualFrame,
|
||||
} from "../../core/laboratory/l31PointPillarsRavnoves";
|
||||
import {
|
||||
L3PointPillarsScene,
|
||||
type L3VisualMode,
|
||||
} from "./L3PointPillarsScene";
|
||||
|
||||
function frameLabel(
|
||||
frame: L31PointPillarsRavnovesResult["frames"][number],
|
||||
): string {
|
||||
return (
|
||||
`${frame.sessionSeconds.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})} с · кадр ${frame.frameId}`
|
||||
+ ` · Vehicle ${frame.classCounts.Vehicle}`
|
||||
);
|
||||
}
|
||||
|
||||
export function L31PointPillarsRavnovesVisual({
|
||||
result,
|
||||
}: {
|
||||
result: L31PointPillarsRavnovesResult;
|
||||
}) {
|
||||
const [selectedFrameId, setSelectedFrameId] = useState(
|
||||
result.frames[0]?.frameId ?? "",
|
||||
);
|
||||
const [frame, setFrame] = useState<L31VisualFrame | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<L3VisualMode>("3d");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedFrameId) return;
|
||||
const controller = new AbortController();
|
||||
setFrame(null);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
void fetchL31PointPillarsRavnovesFrame(
|
||||
result.resultId,
|
||||
selectedFrameId,
|
||||
{ signal: controller.signal },
|
||||
).then((next) => {
|
||||
if (!controller.signal.aborted) setFrame(next);
|
||||
}).catch((caught: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setError(
|
||||
caught instanceof Error
|
||||
? caught.message
|
||||
: "Визуальный кадр L3.1 недоступен.",
|
||||
);
|
||||
}).finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.resultId, selectedFrameId]);
|
||||
|
||||
const selectedIndex = result.frames.findIndex(
|
||||
({ frameId }) => frameId === selectedFrameId,
|
||||
);
|
||||
const navigate = (offset: -1 | 1) => {
|
||||
if (!result.frames.length || selectedIndex < 0) return;
|
||||
const index = (
|
||||
selectedIndex + offset + result.frames.length
|
||||
) % result.frames.length;
|
||||
setSelectedFrameId(result.frames[index].frameId);
|
||||
};
|
||||
|
||||
const controls = (
|
||||
<div className="l3-visual-audit__actions">
|
||||
<div className="l3-visual-audit__pagination">
|
||||
<IconButton
|
||||
label="Предыдущий кадр RAVNOVES00"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<Icon name="chevron-left" size={16} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
label="Следующий кадр RAVNOVES00"
|
||||
onClick={() => navigate(1)}
|
||||
>
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Select
|
||||
label="Выбрать кадр L3.1 RAVNOVES00"
|
||||
value={selectedFrameId}
|
||||
options={result.frames.map((item) => ({
|
||||
value: item.frameId,
|
||||
label: frameLabel(item),
|
||||
}))}
|
||||
variant="split"
|
||||
menuWidth="anchor"
|
||||
onChange={setSelectedFrameId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const overlay = frame ? (
|
||||
<div className="l3-visual-audit__overlay">
|
||||
<div>
|
||||
<span>RAVNOVES00</span>
|
||||
<strong>
|
||||
{frame.summary.sessionSeconds.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})} с · кадр {frame.frameId}
|
||||
</strong>
|
||||
<small>
|
||||
{frame.modelRangePointCount.toLocaleString("ru-RU")} из{" "}
|
||||
{frame.sourcePointCount.toLocaleString("ru-RU")} точек в range модели
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Гипотезы модели · не ground truth</span>
|
||||
<strong>
|
||||
Vehicle {frame.summary.classCounts.Vehicle}
|
||||
{" · "}Pedestrian {frame.summary.classCounts.Pedestrian}
|
||||
{" · "}Cyclist {frame.summary.classCounts.Cyclist}
|
||||
</strong>
|
||||
<small>
|
||||
{frame.summary.inferenceMs.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 2,
|
||||
})} мс · повтор{" "}
|
||||
{frame.summary.deterministicReplay ? "совпал" : "не совпал"}
|
||||
</small>
|
||||
</div>
|
||||
<div className="l3-visual-audit__legend">
|
||||
<span data-tone="prediction">
|
||||
Бокс · неподтверждённое предсказание PointPillars
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<div className="l3-visual-audit">
|
||||
<LaboratoryEvidenceViewer
|
||||
label="PointPillars на RAVNOVES00"
|
||||
mode={mode}
|
||||
modes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "bev", label: "BEV" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={controls}
|
||||
overlay={overlay}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Открываем выбранный кадр RAVNOVES00</span>
|
||||
</div>
|
||||
) : error || !frame ? (
|
||||
<div className="l3-visual-audit__state" role="status">
|
||||
<Icon name="alert" size={18} />
|
||||
<span>{error ?? "Визуальный кадр L3.1 недоступен."}</span>
|
||||
</div>
|
||||
) : (
|
||||
<L3PointPillarsScene
|
||||
frame={frame}
|
||||
mode={mode}
|
||||
bevCenterX={0}
|
||||
bevHalfExtent={55}
|
||||
/>
|
||||
)}
|
||||
</LaboratoryEvidenceViewer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function L3PointPillarsResult({
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="L3 · визуальный аудит PointPillars"
|
||||
description="Визуальная производная полного KITTI transfer-прогона: исходные LiDAR-точки, независимые truth-боксы и предсказания модели сопоставлены тем же глобальным 3D IoU-контрактом. Производная не меняет метрики и не выдает результат за K1 accuracy."
|
||||
description="Визуальная производная полного KITTI transfer-прогона: исходные LiDAR-точки, независимые truth-боксы и предсказания модели сопоставлены тем же глобальным 3D IoU-контрактом. Производная не меняет метрики и не выдаёт результат за точность целевого сканера."
|
||||
status="Требуется визуальная проверка"
|
||||
statusTone="warning"
|
||||
facts={[
|
||||
@@ -51,7 +51,7 @@ export function L3PointPillarsResult({
|
||||
question: "Соответствуют ли измеренные провал переноса и почти сплошная ложная занятость фактической геометрии исходных LiDAR-кадров?",
|
||||
approach: "Полный sealed run проверен по hash identity. Global score-order matching повторён с исходными порогами IoU, после чего детерминированно выбраны TP-, FP-, FN- и class-coverage кадры. В браузер поступает только выбранный кадр.",
|
||||
principalResult: `Полный прогон: BEV mAP40 ${percent(metrics.bevMap40)}, 3D mAP40 ${percent(metrics.threeDMap40, 6)}, false occupied ${percent(metrics.falseOccupiedRate)}. Визуальный аудит теперь доступен в 3D и BEV.`,
|
||||
limitation: "Это cross-domain KITTI probe модели, обученной на proprietary solid-state LiDAR. Он проверяет перенос и корректность измерителя, но не доказывает точность K1, camera-first детектор, навигацию или safety.",
|
||||
limitation: "Это cross-domain KITTI probe модели, обученной на proprietary solid-state LiDAR. Он проверяет перенос и корректность измерителя, но не доказывает точность целевого сканера, camera-first детектор, навигацию или safety.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
@@ -131,7 +131,7 @@ export function L3PointPillarsResult({
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Полный cross-domain прогон воспроизводим, его численные артефакты связаны с исходными LiDAR-кадрами, а TP/FP/FN можно проверить в 3D и BEV без повторного inference.",
|
||||
notProved: "Не доказаны пригодность этой модели для K1, точность camera-first семантики, метрическая геометрия K1 в других условиях, навигация, команды или safety.",
|
||||
notProved: "Не доказаны пригодность этой модели для целевого сканера, точность camera-first семантики, метрическая геометрия в других условиях, навигация, команды или safety.",
|
||||
decision: "Не переносить этот публичный PointPillars-кандидат в operational pipeline. Использовать визуальный аудит для проверки природы провала и сохранить архитектуру camera-first semantics + LiDAR metric geometry как основной продуктовый путь.",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -94,9 +94,16 @@ function addBoxes(
|
||||
export function L3PointPillarsScene({
|
||||
frame,
|
||||
mode,
|
||||
bevCenterX = 30,
|
||||
bevHalfExtent = 42,
|
||||
}: {
|
||||
frame: L3VisualFrame;
|
||||
frame: Pick<
|
||||
L3VisualFrame,
|
||||
"frameId" | "pointsXyzi" | "truthBoxes" | "predictionBoxes"
|
||||
>;
|
||||
mode: L3VisualMode;
|
||||
bevCenterX?: number;
|
||||
bevHalfExtent?: number;
|
||||
}) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const [renderError, setRenderError] = useState<string | null>(null);
|
||||
@@ -138,7 +145,7 @@ export function L3PointPillarsScene({
|
||||
);
|
||||
const pointsMaterial = new THREE.PointsMaterial({
|
||||
color: tokenColor(host, "--nodedc-text-secondary", [187, 190, 196]),
|
||||
size: mode === "bev" ? 1.4 : 1.8,
|
||||
size: mode === "bev" ? 2.2 : 1.8,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
opacity: 0.52,
|
||||
@@ -163,6 +170,11 @@ export function L3PointPillarsScene({
|
||||
"--nodedc-danger-rgb",
|
||||
[255, 98, 112],
|
||||
),
|
||||
"model-prediction": tokenColor(
|
||||
host,
|
||||
"--nodedc-accent-rgb",
|
||||
[111, 181, 251],
|
||||
),
|
||||
};
|
||||
const truthLines = addBoxes(scene, frame.truthBoxes, colors, 0.9);
|
||||
const predictionLines = addBoxes(
|
||||
@@ -190,10 +202,10 @@ export function L3PointPillarsScene({
|
||||
const perspective = new THREE.PerspectiveCamera(52, 1, 0.1, 500);
|
||||
perspective.position.set(-12, 18, 36);
|
||||
const orthographic = new THREE.OrthographicCamera(-40, 40, 40, -40, 0.1, 500);
|
||||
orthographic.position.set(35, 100, 0);
|
||||
orthographic.position.set(bevCenterX + 5, 100, 0);
|
||||
orthographic.up.set(1, 0, 0);
|
||||
const camera = mode === "bev" ? orthographic : perspective;
|
||||
camera.lookAt(30, 0, 0);
|
||||
camera.lookAt(mode === "bev" ? bevCenterX : 30, 0, 0);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = false;
|
||||
@@ -201,7 +213,7 @@ export function L3PointPillarsScene({
|
||||
controls.enablePan = true;
|
||||
controls.enableZoom = true;
|
||||
controls.screenSpacePanning = true;
|
||||
controls.target.set(30, 0, 0);
|
||||
controls.target.set(mode === "bev" ? bevCenterX : 30, 0, 0);
|
||||
controls.update();
|
||||
|
||||
const render = () => renderer.render(scene, camera);
|
||||
@@ -214,7 +226,7 @@ export function L3PointPillarsScene({
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
} else {
|
||||
const horizontal = 42;
|
||||
const horizontal = bevHalfExtent;
|
||||
camera.left = -horizontal;
|
||||
camera.right = horizontal;
|
||||
camera.top = horizontal / (width / height);
|
||||
@@ -242,7 +254,7 @@ export function L3PointPillarsScene({
|
||||
renderer.dispose();
|
||||
renderer.domElement.remove();
|
||||
};
|
||||
}, [frame, mode]);
|
||||
}, [bevCenterX, bevHalfExtent, frame, mode]);
|
||||
|
||||
return (
|
||||
<div className="l3-visual-audit__scene" ref={hostRef}>
|
||||
|
||||
@@ -44,25 +44,17 @@ import {
|
||||
advancedLaboratorySourceSession,
|
||||
advancedLaboratoryWorkOptions,
|
||||
isAdvancedLaboratoryWorkId,
|
||||
type AdvancedLaboratoryWorkId,
|
||||
} from "./AdvancedLaboratoryResult";
|
||||
import {
|
||||
e28LaboratoryBrief, e29LaboratoryBrief,
|
||||
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
|
||||
} from "./laboratoryArchiveBriefs";
|
||||
import { useAdvancedLaboratoryCatalog } from "./useAdvancedLaboratoryCatalog";
|
||||
import { buildLaboratoryProfiles, workOptionsForProfile } from "./laboratoryArchiveProfiles";
|
||||
import type { LaboratoryProfileId, LaboratoryWorkId } from "./laboratoryArchiveProfiles";
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
|
||||
type LaboratoryProfileId = "sensor-fusion" | "published-perception";
|
||||
type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| AdvancedLaboratoryWorkId
|
||||
| `session:${string}`;
|
||||
|
||||
function laboratoryWorkOrdinal(value: string): number {
|
||||
const match = value.match(/\bE(\d+)\b/i);
|
||||
return match ? Number(match[1]) : -1;
|
||||
@@ -667,7 +659,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
label: "LAB E30 · evidence review A2",
|
||||
});
|
||||
}
|
||||
items.push(...advancedLaboratoryWorkOptions(advanced.index));
|
||||
items.push(
|
||||
...advancedLaboratoryWorkOptions(advanced.index).filter(
|
||||
({ id }) => id !== "l3-pointpillars-visual-audit",
|
||||
),
|
||||
);
|
||||
return items.sort(
|
||||
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
|
||||
);
|
||||
@@ -677,29 +673,28 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
e29Result,
|
||||
e30Result,
|
||||
]);
|
||||
const profiles = useMemo(() => {
|
||||
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
|
||||
if (sensorWorks.length) {
|
||||
items.push({
|
||||
id: "sensor-fusion",
|
||||
label: `${rigLabel} · камера + LiDAR · control plane`,
|
||||
});
|
||||
}
|
||||
if (publishedWorks.length) {
|
||||
items.push({
|
||||
id: "published-perception",
|
||||
label: `${rigLabel} · опубликованный perception pipeline`,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [publishedWorks.length, rigLabel, sensorWorks.length]);
|
||||
const workOptions: readonly LaboratoryOption<LaboratoryWorkId>[] =
|
||||
profileId === "sensor-fusion"
|
||||
? sensorWorks
|
||||
: publishedWorks.map((session) => ({
|
||||
const publicBenchmarkWorks = useMemo(
|
||||
() => advancedLaboratoryWorkOptions(advanced.index).filter(
|
||||
({ id }) => id === "l3-pointpillars-visual-audit",
|
||||
),
|
||||
[advanced.index],
|
||||
);
|
||||
const profiles = useMemo(
|
||||
() => buildLaboratoryProfiles({
|
||||
rigLabel,
|
||||
sensorAvailable: sensorWorks.length > 0,
|
||||
publicBenchmarkAvailable: publicBenchmarkWorks.length > 0,
|
||||
publishedAvailable: publishedWorks.length > 0,
|
||||
}),
|
||||
[publicBenchmarkWorks.length, publishedWorks.length, rigLabel, sensorWorks.length],
|
||||
);
|
||||
const publishedWorkOptions = publishedWorks.map((session) => ({
|
||||
id: `session:${session.id}` as const,
|
||||
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
|
||||
}));
|
||||
const workOptions = workOptionsForProfile(
|
||||
profileId, sensorWorks, publicBenchmarkWorks, publishedWorkOptions,
|
||||
);
|
||||
const selectedSessionId = workId.startsWith("session:")
|
||||
? workId.slice("session:".length)
|
||||
: null;
|
||||
@@ -730,6 +725,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
setWorkId(firstWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
} else if (firstProfile.id === "public-benchmarks") {
|
||||
const firstWork = publicBenchmarkWorks[0];
|
||||
if (firstWork) {
|
||||
setWorkId(firstWork.id);
|
||||
initialWorkSelectedRef.current = true;
|
||||
}
|
||||
} else {
|
||||
const first = publishedWorks[0];
|
||||
if (first) {
|
||||
@@ -756,6 +757,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
profileId,
|
||||
profiles,
|
||||
publishedWorks,
|
||||
publicBenchmarkWorks,
|
||||
sensorWorks,
|
||||
sessions.state,
|
||||
workId,
|
||||
@@ -770,6 +772,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
if (first) setWorkId(first.id);
|
||||
return;
|
||||
}
|
||||
if (next === "public-benchmarks") {
|
||||
const first = publicBenchmarkWorks[0];
|
||||
if (first) setWorkId(first.id);
|
||||
return;
|
||||
}
|
||||
const first = publishedWorks[0];
|
||||
if (!first) return;
|
||||
const nextWork = `session:${first.id}` as const;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { AdvancedLaboratoryWorkId } from "../../core/laboratory/advancedIndex";
|
||||
|
||||
export type LaboratoryProfileId =
|
||||
| "sensor-fusion"
|
||||
| "public-benchmarks"
|
||||
| "published-perception";
|
||||
|
||||
export type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| AdvancedLaboratoryWorkId
|
||||
| `session:${string}`;
|
||||
|
||||
export function buildLaboratoryProfiles({
|
||||
rigLabel,
|
||||
sensorAvailable,
|
||||
publicBenchmarkAvailable,
|
||||
publishedAvailable,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
sensorAvailable: boolean;
|
||||
publicBenchmarkAvailable: boolean;
|
||||
publishedAvailable: boolean;
|
||||
}): readonly LaboratoryOption<LaboratoryProfileId>[] {
|
||||
const profiles: LaboratoryOption<LaboratoryProfileId>[] = [];
|
||||
if (sensorAvailable) {
|
||||
profiles.push({
|
||||
id: "sensor-fusion",
|
||||
label: `${rigLabel} · камера + LiDAR · control plane`,
|
||||
});
|
||||
}
|
||||
if (publicBenchmarkAvailable) {
|
||||
profiles.push({
|
||||
id: "public-benchmarks",
|
||||
label: "Публичные датасеты · внешний benchmark-контур",
|
||||
});
|
||||
}
|
||||
if (publishedAvailable) {
|
||||
profiles.push({
|
||||
id: "published-perception",
|
||||
label: `${rigLabel} · опубликованный perception pipeline`,
|
||||
});
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
export function workOptionsForProfile(
|
||||
profileId: LaboratoryProfileId,
|
||||
sensorWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
publicBenchmarkWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
publishedWorks: readonly LaboratoryOption<LaboratoryWorkId>[],
|
||||
): readonly LaboratoryOption<LaboratoryWorkId>[] {
|
||||
return profileId === "sensor-fusion"
|
||||
? sensorWorks
|
||||
: profileId === "public-benchmarks"
|
||||
? publicBenchmarkWorks
|
||||
: publishedWorks;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ function mergeResults(
|
||||
): AdvancedLaboratoryResults {
|
||||
return {
|
||||
l3: next.l3 ?? current.l3,
|
||||
l31: next.l31 ?? current.l31,
|
||||
e31: next.e31 ?? current.e31,
|
||||
e32: next.e32 ?? current.e32,
|
||||
e33: next.e33 ?? current.e33,
|
||||
|
||||
Reference in New Issue
Block a user