feat(lidar): admit and benchmark GOOSE baseline
This commit is contained in:
@@ -8,6 +8,8 @@ export interface DatasetGatewayCatalog {
|
||||
configured: boolean;
|
||||
admitted: boolean;
|
||||
status: "ready" | "blocked-storage-policy";
|
||||
attestation: "worker-manifest" | "local-worker-path" | "none";
|
||||
manifestValid: boolean;
|
||||
requiredWindowsRoot: string;
|
||||
requiredWslRoot: string;
|
||||
};
|
||||
@@ -21,7 +23,31 @@ export interface DatasetGatewayCatalog {
|
||||
platforms: string[];
|
||||
superclasses: string[];
|
||||
validationArchiveGb: number;
|
||||
admissionStatus: "ready-for-download" | "blocked-storage-policy";
|
||||
admissionStatus:
|
||||
| "blocked-storage-policy"
|
||||
| "ready-for-download"
|
||||
| "downloading"
|
||||
| "downloaded"
|
||||
| "verifying"
|
||||
| "verified"
|
||||
| "frame-ready";
|
||||
archive: {
|
||||
bytesTransferred: number;
|
||||
totalBytes: number;
|
||||
sizeBytes: number | null;
|
||||
sha256: string | null;
|
||||
integrity: string;
|
||||
vendorChecksumAvailable: false;
|
||||
} | null;
|
||||
frame: {
|
||||
frameId: string;
|
||||
pointCount: number;
|
||||
semanticClassCount: number;
|
||||
groundTruthGroundFraction: number;
|
||||
previewPointCount: number;
|
||||
previewSha256: string;
|
||||
previewAvailable: true;
|
||||
} | null;
|
||||
};
|
||||
representations: Array<{
|
||||
id: DatasetRepresentationId;
|
||||
@@ -45,6 +71,46 @@ export interface DatasetGatewayCatalog {
|
||||
nextAction: string;
|
||||
}
|
||||
|
||||
export interface DatasetNativeScanPreview {
|
||||
sourceId: "goose-3d/v2025-08-22";
|
||||
frameId: string;
|
||||
sourcePointCount: number;
|
||||
pointCount: number;
|
||||
pointsXyzM: Array<[number, number, number]>;
|
||||
remission0To255: number[];
|
||||
semanticLabelIds: number[];
|
||||
semanticRgb0To255: number[];
|
||||
groundTruthGround: number[];
|
||||
classes: Array<{
|
||||
labelId: number;
|
||||
className: string;
|
||||
hex: string;
|
||||
challengeCategoryId: number;
|
||||
challengeCategoryName: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DatasetGroundComparison {
|
||||
sourceId: "goose-3d/v2025-08-22";
|
||||
frameId: string;
|
||||
pointCount: number;
|
||||
currentGround: number[];
|
||||
groundTruthGround: number[];
|
||||
evaluated: number[];
|
||||
disagreement: number[];
|
||||
metrics: {
|
||||
precision: number;
|
||||
recall: number;
|
||||
f1: number;
|
||||
groundIou: number;
|
||||
accuracy: number;
|
||||
artificialGroundRecall: number;
|
||||
naturalGroundRecall: number;
|
||||
obstacleNonGroundRecall: number;
|
||||
};
|
||||
latencyMs: number;
|
||||
}
|
||||
|
||||
export class DatasetGatewayContractError extends Error {}
|
||||
|
||||
type DatasetFetch = (
|
||||
@@ -105,12 +171,43 @@ function number(value: unknown, label: string): number {
|
||||
return value;
|
||||
}
|
||||
|
||||
function nullableNumber(value: unknown, label: string): number | null {
|
||||
return value === null ? null : number(value, label);
|
||||
}
|
||||
|
||||
function nullableDigest(value: unknown, label: string): string | null {
|
||||
if (value === null) return null;
|
||||
const digest = string(value, label);
|
||||
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
||||
throw new DatasetGatewayContractError(`${label}: некорректный SHA-256`);
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
function integers(
|
||||
value: unknown,
|
||||
label: string,
|
||||
maximum: number,
|
||||
): number[] {
|
||||
return array(value, label).map((item, index) => {
|
||||
if (
|
||||
typeof item !== "number"
|
||||
|| !Number.isInteger(item)
|
||||
|| item < 0
|
||||
|| item > maximum
|
||||
) {
|
||||
throw new DatasetGatewayContractError(`${label}[${index}]: некорректное число`);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export function parseDatasetGatewayCatalog(
|
||||
value: unknown,
|
||||
): DatasetGatewayCatalog {
|
||||
const source = record(value, "Dataset Gateway");
|
||||
if (
|
||||
source.schema_version !== "missioncore.dataset-gateway-catalog/v1"
|
||||
source.schema_version !== "missioncore.dataset-gateway-catalog/v2"
|
||||
|| source.access !== "read-only"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Dataset Gateway contract несовместим");
|
||||
@@ -123,6 +220,14 @@ export function parseDatasetGatewayCatalog(
|
||||
if (storage.path_exposed !== false) {
|
||||
throw new DatasetGatewayContractError("Dataset Gateway раскрыл локальный путь");
|
||||
}
|
||||
const storageAttestation = storage.attestation;
|
||||
if (
|
||||
storageAttestation !== "worker-manifest"
|
||||
&& storageAttestation !== "local-worker-path"
|
||||
&& storageAttestation !== "none"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("storage.attestation: неизвестное значение");
|
||||
}
|
||||
const sources = array(source.sources, "sources");
|
||||
if (sources.length !== 1) {
|
||||
throw new DatasetGatewayContractError("Ожидался один первичный dataset source");
|
||||
@@ -137,9 +242,72 @@ export function parseDatasetGatewayCatalog(
|
||||
if (
|
||||
admissionStatus !== "ready-for-download"
|
||||
&& admissionStatus !== "blocked-storage-policy"
|
||||
&& admissionStatus !== "downloading"
|
||||
&& admissionStatus !== "downloaded"
|
||||
&& admissionStatus !== "verifying"
|
||||
&& admissionStatus !== "verified"
|
||||
&& admissionStatus !== "frame-ready"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("source admission status неизвестен");
|
||||
}
|
||||
const archive = admission.archive === null
|
||||
? null
|
||||
: record(admission.archive, "source.admission.archive");
|
||||
const archiveValue = archive
|
||||
? {
|
||||
bytesTransferred: number(
|
||||
archive.bytes_transferred,
|
||||
"archive.bytes_transferred",
|
||||
),
|
||||
totalBytes: number(archive.total_bytes, "archive.total_bytes"),
|
||||
sizeBytes: nullableNumber(archive.size_bytes, "archive.size_bytes"),
|
||||
sha256: nullableDigest(archive.sha256, "archive.sha256"),
|
||||
integrity: string(archive.integrity, "archive.integrity", true),
|
||||
vendorChecksumAvailable: (() => {
|
||||
if (archive.vendor_checksum_available !== false) {
|
||||
throw new DatasetGatewayContractError(
|
||||
"archive.vendor_checksum_available: ожидался false",
|
||||
);
|
||||
}
|
||||
return false as const;
|
||||
})(),
|
||||
}
|
||||
: null;
|
||||
const admittedFrame = admission.frame === null
|
||||
? null
|
||||
: record(admission.frame, "source.admission.frame");
|
||||
const frameValue = admittedFrame
|
||||
? {
|
||||
frameId: string(admittedFrame.frame_id, "frame.frame_id", true),
|
||||
pointCount: number(admittedFrame.point_count, "frame.point_count"),
|
||||
semanticClassCount: number(
|
||||
admittedFrame.semantic_class_count,
|
||||
"frame.semantic_class_count",
|
||||
),
|
||||
groundTruthGroundFraction: number(
|
||||
admittedFrame.ground_truth_ground_fraction,
|
||||
"frame.ground_truth_ground_fraction",
|
||||
),
|
||||
previewPointCount: number(
|
||||
admittedFrame.preview_point_count,
|
||||
"frame.preview_point_count",
|
||||
),
|
||||
previewSha256: nullableDigest(
|
||||
admittedFrame.preview_sha256,
|
||||
"frame.preview_sha256",
|
||||
) ?? (() => {
|
||||
throw new DatasetGatewayContractError("frame.preview_sha256 отсутствует");
|
||||
})(),
|
||||
previewAvailable: (() => {
|
||||
if (admittedFrame.preview_available !== true) {
|
||||
throw new DatasetGatewayContractError(
|
||||
"frame.preview_available: ожидался true",
|
||||
);
|
||||
}
|
||||
return true as const;
|
||||
})(),
|
||||
}
|
||||
: null;
|
||||
const representations = array(
|
||||
source.representations,
|
||||
"representations",
|
||||
@@ -188,6 +356,8 @@ export function parseDatasetGatewayCatalog(
|
||||
configured: boolean(storage.configured, "storage.configured"),
|
||||
admitted: boolean(storage.admitted, "storage.admitted"),
|
||||
status: storageStatus,
|
||||
attestation: storageAttestation,
|
||||
manifestValid: boolean(storage.manifest_valid, "storage.manifest_valid"),
|
||||
requiredWindowsRoot: string(
|
||||
storage.required_windows_root,
|
||||
"storage.required_windows_root",
|
||||
@@ -208,6 +378,8 @@ export function parseDatasetGatewayCatalog(
|
||||
"validation_archive_gb",
|
||||
),
|
||||
admissionStatus,
|
||||
archive: archiveValue,
|
||||
frame: frameValue,
|
||||
},
|
||||
representations,
|
||||
pipeline,
|
||||
@@ -223,6 +395,183 @@ export function parseDatasetGatewayCatalog(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseDatasetNativeScanPreview(
|
||||
value: unknown,
|
||||
): DatasetNativeScanPreview {
|
||||
const source = record(value, "Dataset preview");
|
||||
if (
|
||||
source.schema_version !== "missioncore.dataset-native-scan-preview/v1"
|
||||
|| source.source_id !== "goose-3d/v2025-08-22"
|
||||
|| source.representation !== "native-scan"
|
||||
|| source.sampling !== "deterministic-even-index"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Dataset preview contract несовместим");
|
||||
}
|
||||
const sourcePointCount = number(source.source_point_count, "source_point_count");
|
||||
const pointCount = number(source.point_count, "point_count");
|
||||
const pointsXyzM = array(source.points_xyz_m, "points_xyz_m").map(
|
||||
(value, index): [number, number, number] => {
|
||||
const point = array(value, `points_xyz_m[${index}]`);
|
||||
if (
|
||||
point.length !== 3
|
||||
|| point.some((coordinate) =>
|
||||
typeof coordinate !== "number" || !Number.isFinite(coordinate)
|
||||
)
|
||||
) {
|
||||
throw new DatasetGatewayContractError(
|
||||
`points_xyz_m[${index}]: некорректная точка`,
|
||||
);
|
||||
}
|
||||
return [point[0] as number, point[1] as number, point[2] as number];
|
||||
},
|
||||
);
|
||||
const remission0To255 = integers(
|
||||
source.remission_0_to_255,
|
||||
"remission_0_to_255",
|
||||
255,
|
||||
);
|
||||
const semanticLabelIds = integers(
|
||||
source.semantic_label_ids,
|
||||
"semantic_label_ids",
|
||||
65_535,
|
||||
);
|
||||
const semanticRgb0To255 = integers(
|
||||
source.semantic_rgb_0_to_255,
|
||||
"semantic_rgb_0_to_255",
|
||||
255,
|
||||
);
|
||||
const groundTruthGround = integers(
|
||||
source.ground_truth_ground,
|
||||
"ground_truth_ground",
|
||||
1,
|
||||
);
|
||||
if (
|
||||
pointCount < 1
|
||||
|| pointCount > 50_000
|
||||
|| pointCount > sourcePointCount
|
||||
|| pointsXyzM.length !== pointCount
|
||||
|| remission0To255.length !== pointCount
|
||||
|| semanticLabelIds.length !== pointCount
|
||||
|| semanticRgb0To255.length !== pointCount * 3
|
||||
|| groundTruthGround.length !== pointCount
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Dataset preview arrays не выровнены");
|
||||
}
|
||||
const classes = array(source.classes, "classes").map((value, index) => {
|
||||
const item = record(value, `classes[${index}]`);
|
||||
return {
|
||||
labelId: number(item.label_id, "class.label_id"),
|
||||
className: string(item.class_name, "class.class_name", true),
|
||||
hex: string(item.hex, "class.hex"),
|
||||
challengeCategoryId: number(
|
||||
item.challenge_category_id,
|
||||
"class.challenge_category_id",
|
||||
),
|
||||
challengeCategoryName: string(
|
||||
item.challenge_category_name,
|
||||
"class.challenge_category_name",
|
||||
true,
|
||||
),
|
||||
};
|
||||
});
|
||||
const safety = record(source.safety, "safety");
|
||||
if (
|
||||
safety.visualization_only !== true
|
||||
|| safety.navigation_or_safety_accepted !== false
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Dataset preview safety boundary нарушен");
|
||||
}
|
||||
return {
|
||||
sourceId: "goose-3d/v2025-08-22",
|
||||
frameId: string(source.frame_id, "frame_id", true),
|
||||
sourcePointCount,
|
||||
pointCount,
|
||||
pointsXyzM,
|
||||
remission0To255,
|
||||
semanticLabelIds,
|
||||
semanticRgb0To255,
|
||||
groundTruthGround,
|
||||
classes,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseDatasetGroundComparison(
|
||||
value: unknown,
|
||||
): DatasetGroundComparison {
|
||||
const source = record(value, "Ground comparison");
|
||||
if (
|
||||
source.schema_version !== "missioncore.dataset-ground-comparison-preview/v1"
|
||||
|| source.source_id !== "goose-3d/v2025-08-22"
|
||||
|| source.sampling !== "deterministic-even-index"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Ground comparison contract несовместим");
|
||||
}
|
||||
const pointCount = number(source.point_count, "point_count");
|
||||
const currentGround = integers(source.current_ground, "current_ground", 1);
|
||||
const groundTruthGround = integers(
|
||||
source.ground_truth_ground,
|
||||
"ground_truth_ground",
|
||||
1,
|
||||
);
|
||||
const evaluated = integers(source.evaluated, "evaluated", 1);
|
||||
const disagreement = integers(source.disagreement, "disagreement", 1);
|
||||
if (
|
||||
pointCount < 1
|
||||
|| pointCount > 50_000
|
||||
|| currentGround.length !== pointCount
|
||||
|| groundTruthGround.length !== pointCount
|
||||
|| evaluated.length !== pointCount
|
||||
|| disagreement.length !== pointCount
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Ground comparison arrays не выровнены");
|
||||
}
|
||||
const metrics = record(source.metrics, "metrics");
|
||||
const fraction = (key: string): number => {
|
||||
const value = number(metrics[key], `metrics.${key}`);
|
||||
if (value > 1) {
|
||||
throw new DatasetGatewayContractError(`metrics.${key}: ожидалась доля`);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const provider = record(source.provider, "provider");
|
||||
if (
|
||||
provider.provider_id !== "missioncore-local-percentile-ground/v1"
|
||||
|| provider.ground_truth !== false
|
||||
|| !/^[a-f0-9]{64}$/.test(
|
||||
string(provider.implementation_sha256, "provider.implementation_sha256"),
|
||||
)
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Ground comparison provider несовместим");
|
||||
}
|
||||
const safety = record(source.safety, "safety");
|
||||
if (
|
||||
safety.qualification_only !== true
|
||||
|| safety.navigation_or_safety_accepted !== false
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Ground comparison safety boundary нарушен");
|
||||
}
|
||||
return {
|
||||
sourceId: "goose-3d/v2025-08-22",
|
||||
frameId: string(source.frame_id, "frame_id", true),
|
||||
pointCount,
|
||||
currentGround,
|
||||
groundTruthGround,
|
||||
evaluated,
|
||||
disagreement,
|
||||
metrics: {
|
||||
precision: fraction("precision"),
|
||||
recall: fraction("recall"),
|
||||
f1: fraction("f1"),
|
||||
groundIou: fraction("ground_iou"),
|
||||
accuracy: fraction("accuracy"),
|
||||
artificialGroundRecall: fraction("artificial_ground_recall"),
|
||||
naturalGroundRecall: fraction("natural_ground_recall"),
|
||||
obstacleNonGroundRecall: fraction("obstacle_non_ground_recall"),
|
||||
},
|
||||
latencyMs: number(source.latency_ms, "latency_ms"),
|
||||
};
|
||||
}
|
||||
|
||||
async function responseJson(response: Response): Promise<unknown> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Dataset Gateway HTTP ${response.status}`);
|
||||
@@ -241,3 +590,30 @@ export async function fetchDatasetGatewayCatalog(
|
||||
});
|
||||
return parseDatasetGatewayCatalog(await responseJson(response));
|
||||
}
|
||||
|
||||
export async function fetchDatasetNativeScanPreview(
|
||||
options: { signal?: AbortSignal; fetcher?: DatasetFetch } = {},
|
||||
): Promise<DatasetNativeScanPreview> {
|
||||
const fetcher = options.fetcher ?? fetch;
|
||||
const response = await fetcher("/api/v1/lidar/dataset-gateway/preview", {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: options.signal,
|
||||
});
|
||||
return parseDatasetNativeScanPreview(await responseJson(response));
|
||||
}
|
||||
|
||||
export async function fetchDatasetGroundComparison(
|
||||
options: { signal?: AbortSignal; fetcher?: DatasetFetch } = {},
|
||||
): Promise<DatasetGroundComparison> {
|
||||
const fetcher = options.fetcher ?? fetch;
|
||||
const response = await fetcher(
|
||||
"/api/v1/lidar/dataset-gateway/ground-comparison",
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return parseDatasetGroundComparison(await responseJson(response));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user