feat(perception): add E34 temporal occupied layer
This commit is contained in:
@@ -93,6 +93,7 @@ export interface AdvancedLaboratoryResults {
|
||||
e31: E31LaboratoryResult | null;
|
||||
e32: E32LaboratoryResult | null;
|
||||
e33: E33LaboratoryResult | null;
|
||||
e34: E34TemporalLayerResult | null;
|
||||
}
|
||||
|
||||
export class AdvancedLaboratoryContractError extends Error {
|
||||
@@ -371,10 +372,15 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<AdvancedLaboratoryResults> {
|
||||
const [e31, e32, e33] = await Promise.all([
|
||||
const [e31, e32, e33, e34] = await Promise.all([
|
||||
fetchOne("/api/v1/laboratory/e31/results?limit=1", parseE31, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e32/results?limit=1", parseE32, fetcher, signal),
|
||||
fetchOne("/api/v1/laboratory/e33/results?limit=1", parseE33, fetcher, signal),
|
||||
fetchE34TemporalLayerResult({ fetcher, signal }),
|
||||
]);
|
||||
return { e31, e32, e33 };
|
||||
return { e31, e32, e33, e34 };
|
||||
}
|
||||
import {
|
||||
fetchE34TemporalLayerResult,
|
||||
type E34TemporalLayerResult,
|
||||
} from "./e34TemporalLayer";
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
export type E34TemporalState = "current" | "held" | "expired";
|
||||
export type E34OccupancyState = "occupied" | "unknown";
|
||||
export type E34OwnerKind = "camera-track" | "geometry-cluster";
|
||||
export type E34Point3 = readonly [number, number, number];
|
||||
|
||||
export interface E34TemporalHistoryPoint {
|
||||
frameIndex: number;
|
||||
sessionSeconds: number;
|
||||
centroidMapXyzM: E34Point3;
|
||||
}
|
||||
|
||||
export interface E34TemporalComponent {
|
||||
temporalId: number;
|
||||
state: E34TemporalState;
|
||||
occupancyState: E34OccupancyState;
|
||||
ownerKind: E34OwnerKind;
|
||||
centroidMapXyzM: E34Point3;
|
||||
lastObservedAgeSeconds: number;
|
||||
associationReason: string;
|
||||
history: readonly E34TemporalHistoryPoint[];
|
||||
}
|
||||
|
||||
export interface E34TemporalReviewFrame {
|
||||
frameIndex: number;
|
||||
sessionSeconds: number;
|
||||
sourceAvailable: boolean;
|
||||
layerState: "current" | "held" | "unknown";
|
||||
counts: {
|
||||
current: number;
|
||||
held: number;
|
||||
expired: number;
|
||||
};
|
||||
cellCentersMapXyzM: readonly E34Point3[];
|
||||
components: readonly E34TemporalComponent[];
|
||||
}
|
||||
|
||||
export interface E34TemporalLayerResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
sourceSessionId: string;
|
||||
status: "accepted-bounded-occupied-unknown-temporal-layer";
|
||||
e32ResultId: string;
|
||||
e33ResultId: string;
|
||||
profileId: string;
|
||||
pipelineId: string;
|
||||
coordinateFrame: "map";
|
||||
configuration: {
|
||||
voxelSizeM: number;
|
||||
occupiedTtlSeconds: number;
|
||||
maximumActiveComponents: number;
|
||||
maximumCellsPerComponent: number;
|
||||
geometryMaximumGapSeconds: number;
|
||||
geometryMaximumCentroidDistanceM: number;
|
||||
};
|
||||
metrics: {
|
||||
sourceFrames: number;
|
||||
processedFrames: number;
|
||||
framesWithCurrentLayer: number;
|
||||
heldOnlyFrames: number;
|
||||
unknownEmptyFrames: number;
|
||||
createdComponents: number;
|
||||
cameraCreatedComponents: number;
|
||||
geometryCreatedComponents: number;
|
||||
geometryReassociatedComponents: number;
|
||||
exactCameraAssociations: number;
|
||||
geometrySpatialReassociations: number;
|
||||
heldPublications: number;
|
||||
expiredComponents: number;
|
||||
peakActiveComponents: number;
|
||||
maximumObservedSpanSeconds: number;
|
||||
continuityFraction: number;
|
||||
geometryOnlyReassociationFraction: number;
|
||||
e32CurrentPointRows: number;
|
||||
e34ConsumedCurrentPointRows: number;
|
||||
currentCellRows: number;
|
||||
heldCellRows: number;
|
||||
storedCellRows: number;
|
||||
freeCellRows: number;
|
||||
peakCellsPerComponent: number;
|
||||
maximumHeldAgeSeconds: number;
|
||||
maximumExpiryDelaySeconds: number;
|
||||
maximumReplayMaterializationDelaySeconds: number;
|
||||
mapFrameJumpCandidates: number;
|
||||
frameProcessingP95Ms: number;
|
||||
buildElapsedMs: number;
|
||||
};
|
||||
reviewFrames: readonly E34TemporalReviewFrame[];
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
type LaboratoryFetch = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
|
||||
class E34TemporalLayerContractError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "E34TemporalLayerContractError";
|
||||
}
|
||||
}
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function list(value: unknown, label: string): readonly unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидался массив.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидалась строка.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalString(value: unknown, label: string): string | null {
|
||||
return value === null ? null : stringValue(value, label);
|
||||
}
|
||||
|
||||
function finiteNumber(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидалось число.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function numberValue(value: unknown, label: string): number {
|
||||
const parsed = finiteNumber(value, label);
|
||||
if (parsed < 0) {
|
||||
throw new E34TemporalLayerContractError(`${label}: число отрицательно.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function integerValue(value: unknown, label: string): number {
|
||||
const parsed = numberValue(value, label);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидалось целое число.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидался boolean.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exactString<T extends string>(
|
||||
value: unknown,
|
||||
expected: T,
|
||||
label: string,
|
||||
): T {
|
||||
if (value !== expected) {
|
||||
throw new E34TemporalLayerContractError(`${label}: неверное значение.`);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function oneOf<T extends string>(
|
||||
value: unknown,
|
||||
expected: readonly T[],
|
||||
label: string,
|
||||
): T {
|
||||
if (typeof value !== "string" || !expected.includes(value as T)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: неверное значение.`);
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function contentId(value: unknown, prefix: string, label: string): string {
|
||||
const parsed = stringValue(value, label);
|
||||
if (!new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(parsed)) {
|
||||
throw new E34TemporalLayerContractError(`${label}: неверный content id.`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function point(value: unknown, label: string): E34Point3 {
|
||||
const coordinates = list(value, label);
|
||||
if (coordinates.length !== 3) {
|
||||
throw new E34TemporalLayerContractError(`${label}: ожидались XYZ.`);
|
||||
}
|
||||
return [
|
||||
finiteNumber(coordinates[0], `${label}[0]`),
|
||||
finiteNumber(coordinates[1], `${label}[1]`),
|
||||
finiteNumber(coordinates[2], `${label}[2]`),
|
||||
];
|
||||
}
|
||||
|
||||
function parseHistory(value: unknown, label: string): E34TemporalHistoryPoint {
|
||||
const item = record(value, label);
|
||||
return {
|
||||
frameIndex: integerValue(item.frame_index, `${label}.frame_index`),
|
||||
sessionSeconds: numberValue(
|
||||
item.session_seconds,
|
||||
`${label}.session_seconds`,
|
||||
),
|
||||
centroidMapXyzM: point(
|
||||
item.centroid_map_xyz_m,
|
||||
`${label}.centroid_map_xyz_m`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseComponent(value: unknown, label: string): E34TemporalComponent {
|
||||
const item = record(value, label);
|
||||
return {
|
||||
temporalId: integerValue(item.temporal_id, `${label}.temporal_id`),
|
||||
state: oneOf(
|
||||
item.state,
|
||||
["current", "held", "expired"] as const,
|
||||
`${label}.state`,
|
||||
),
|
||||
occupancyState: oneOf(
|
||||
item.occupancy_state,
|
||||
["occupied", "unknown"] as const,
|
||||
`${label}.occupancy_state`,
|
||||
),
|
||||
ownerKind: oneOf(
|
||||
item.owner_kind,
|
||||
["camera-track", "geometry-cluster"] as const,
|
||||
`${label}.owner_kind`,
|
||||
),
|
||||
centroidMapXyzM: point(
|
||||
item.centroid_map_xyz_m,
|
||||
`${label}.centroid_map_xyz_m`,
|
||||
),
|
||||
lastObservedAgeSeconds: numberValue(
|
||||
item.last_observed_age_seconds,
|
||||
`${label}.last_observed_age_seconds`,
|
||||
),
|
||||
associationReason: stringValue(
|
||||
item.association_reason,
|
||||
`${label}.association_reason`,
|
||||
),
|
||||
history: list(item.history_tail, `${label}.history_tail`).map(
|
||||
(entry, index) => parseHistory(entry, `${label}.history_tail[${index}]`),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseReviewFrame(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): E34TemporalReviewFrame {
|
||||
const item = record(value, label);
|
||||
const counts = record(item.counts, `${label}.counts`);
|
||||
return {
|
||||
frameIndex: integerValue(item.frame_index, `${label}.frame_index`),
|
||||
sessionSeconds: numberValue(
|
||||
item.session_seconds,
|
||||
`${label}.session_seconds`,
|
||||
),
|
||||
sourceAvailable: booleanValue(
|
||||
item.source_available,
|
||||
`${label}.source_available`,
|
||||
),
|
||||
layerState: oneOf(
|
||||
item.layer_state,
|
||||
["current", "held", "unknown"] as const,
|
||||
`${label}.layer_state`,
|
||||
),
|
||||
counts: {
|
||||
current: integerValue(counts.current, `${label}.counts.current`),
|
||||
held: integerValue(counts.held, `${label}.counts.held`),
|
||||
expired: integerValue(counts.expired, `${label}.counts.expired`),
|
||||
},
|
||||
cellCentersMapXyzM: list(
|
||||
item.cell_centers_map_xyz_m,
|
||||
`${label}.cell_centers_map_xyz_m`,
|
||||
).map((entry, index) => point(
|
||||
entry,
|
||||
`${label}.cell_centers_map_xyz_m[${index}]`,
|
||||
)),
|
||||
components: list(item.components, `${label}.components`).map(
|
||||
(entry, index) => parseComponent(
|
||||
entry,
|
||||
`${label}.components[${index}]`,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function diagnosticAuthority(value: unknown, label: string): void {
|
||||
const authority = record(value, label);
|
||||
if (
|
||||
authority.commands_enabled !== false
|
||||
|| authority.navigation_or_safety_accepted !== false
|
||||
) {
|
||||
throw new E34TemporalLayerContractError(
|
||||
`${label}: запрещённые полномочия.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function metricRecord(
|
||||
value: Record<string, unknown>,
|
||||
): E34TemporalLayerResult["metrics"] {
|
||||
const metric = (key: string) => numberValue(
|
||||
value[key],
|
||||
`E34.metrics.${key}`,
|
||||
);
|
||||
const integer = (key: string) => integerValue(
|
||||
value[key],
|
||||
`E34.metrics.${key}`,
|
||||
);
|
||||
return {
|
||||
sourceFrames: integer("source_frames"),
|
||||
processedFrames: integer("processed_frames"),
|
||||
framesWithCurrentLayer: integer("frames_with_current_layer"),
|
||||
heldOnlyFrames: integer("held_only_frames"),
|
||||
unknownEmptyFrames: integer("unknown_empty_frames"),
|
||||
createdComponents: integer("created_components"),
|
||||
cameraCreatedComponents: integer("camera_created_components"),
|
||||
geometryCreatedComponents: integer("geometry_created_components"),
|
||||
geometryReassociatedComponents: integer("geometry_reassociated_components"),
|
||||
exactCameraAssociations: integer("exact_camera_associations"),
|
||||
geometrySpatialReassociations: integer("geometry_spatial_reassociations"),
|
||||
heldPublications: integer("held_publications"),
|
||||
expiredComponents: integer("expired_components"),
|
||||
peakActiveComponents: integer("peak_active_components"),
|
||||
maximumObservedSpanSeconds: metric("maximum_observed_span_seconds"),
|
||||
continuityFraction: metric("continuity_fraction"),
|
||||
geometryOnlyReassociationFraction: metric(
|
||||
"geometry_only_reassociation_fraction",
|
||||
),
|
||||
e32CurrentPointRows: integer("e32_current_point_rows"),
|
||||
e34ConsumedCurrentPointRows: integer("e34_consumed_current_point_rows"),
|
||||
currentCellRows: integer("current_cell_rows"),
|
||||
heldCellRows: integer("held_cell_rows"),
|
||||
storedCellRows: integer("stored_cell_rows"),
|
||||
freeCellRows: integer("free_cell_rows"),
|
||||
peakCellsPerComponent: integer("peak_cells_per_component"),
|
||||
maximumHeldAgeSeconds: metric("maximum_held_age_seconds"),
|
||||
maximumExpiryDelaySeconds: metric("maximum_expiry_delay_seconds"),
|
||||
maximumReplayMaterializationDelaySeconds: metric(
|
||||
"maximum_replay_materialization_delay_seconds",
|
||||
),
|
||||
mapFrameJumpCandidates: integer("map_frame_jump_candidates"),
|
||||
frameProcessingP95Ms: metric("frame_processing_p95_ms"),
|
||||
buildElapsedMs: metric("build_elapsed_ms"),
|
||||
};
|
||||
}
|
||||
|
||||
function parseResult(value: unknown): E34TemporalLayerResult {
|
||||
const item = record(value, "E34");
|
||||
const configuration = record(item.configuration, "E34.configuration");
|
||||
const review = record(item.review, "E34.review");
|
||||
const acceptance = record(item.acceptance, "E34.acceptance");
|
||||
diagnosticAuthority(item.authority, "E34.authority");
|
||||
if (acceptance.accepted !== true) {
|
||||
throw new E34TemporalLayerContractError("E34.acceptance: результат отклонён.");
|
||||
}
|
||||
exactString(
|
||||
review.schema_version,
|
||||
"missioncore.e34-temporal-review-timeline/v1",
|
||||
"E34.review.schema_version",
|
||||
);
|
||||
return {
|
||||
resultId: contentId(
|
||||
item.result_id,
|
||||
"e34-temporal-occupied",
|
||||
"E34.result_id",
|
||||
),
|
||||
createdAtUtc: optionalString(item.created_at_utc, "E34.created_at_utc"),
|
||||
sourceSessionId: stringValue(
|
||||
item.source_session_id,
|
||||
"E34.source_session_id",
|
||||
),
|
||||
status: exactString(
|
||||
item.status,
|
||||
"accepted-bounded-occupied-unknown-temporal-layer",
|
||||
"E34.status",
|
||||
),
|
||||
e32ResultId: contentId(
|
||||
item.e32_result_id,
|
||||
"e32-track-geometry",
|
||||
"E34.e32_result_id",
|
||||
),
|
||||
e33ResultId: contentId(
|
||||
item.e33_result_id,
|
||||
"e33-worker-shadow",
|
||||
"E34.e33_result_id",
|
||||
),
|
||||
profileId: stringValue(item.profile_id, "E34.profile_id"),
|
||||
pipelineId: stringValue(item.pipeline_id, "E34.pipeline_id"),
|
||||
coordinateFrame: exactString(
|
||||
item.coordinate_frame,
|
||||
"map",
|
||||
"E34.coordinate_frame",
|
||||
),
|
||||
configuration: {
|
||||
voxelSizeM: numberValue(
|
||||
configuration.voxel_size_m,
|
||||
"E34.configuration.voxel_size_m",
|
||||
),
|
||||
occupiedTtlSeconds: numberValue(
|
||||
configuration.occupied_ttl_seconds,
|
||||
"E34.configuration.occupied_ttl_seconds",
|
||||
),
|
||||
maximumActiveComponents: integerValue(
|
||||
configuration.maximum_active_components,
|
||||
"E34.configuration.maximum_active_components",
|
||||
),
|
||||
maximumCellsPerComponent: integerValue(
|
||||
configuration.maximum_cells_per_component,
|
||||
"E34.configuration.maximum_cells_per_component",
|
||||
),
|
||||
geometryMaximumGapSeconds: numberValue(
|
||||
configuration.geometry_maximum_gap_seconds,
|
||||
"E34.configuration.geometry_maximum_gap_seconds",
|
||||
),
|
||||
geometryMaximumCentroidDistanceM: numberValue(
|
||||
configuration.geometry_maximum_centroid_distance_m,
|
||||
"E34.configuration.geometry_maximum_centroid_distance_m",
|
||||
),
|
||||
},
|
||||
metrics: metricRecord(record(item.metrics, "E34.metrics")),
|
||||
reviewFrames: list(review.frames, "E34.review.frames").map(
|
||||
(entry, index) => parseReviewFrame(
|
||||
entry,
|
||||
`E34.review.frames[${index}]`,
|
||||
),
|
||||
),
|
||||
access: exactString(item.access, "read-only", "E34.access"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchE34TemporalLayerResult({
|
||||
fetcher = fetch,
|
||||
signal,
|
||||
}: {
|
||||
fetcher?: LaboratoryFetch;
|
||||
signal?: AbortSignal;
|
||||
} = {}): Promise<E34TemporalLayerResult | null> {
|
||||
const response = await fetcher(
|
||||
"/api/v1/laboratory/e34/results?limit=1",
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new E34TemporalLayerContractError(
|
||||
`Каталог LAB E34 недоступен: HTTP ${response.status}.`,
|
||||
);
|
||||
}
|
||||
const catalog = record(await response.json(), "Каталог LAB E34");
|
||||
exactString(
|
||||
catalog.schema_version,
|
||||
"missioncore.laboratory-advanced-catalog/v1",
|
||||
"Каталог LAB E34.schema_version",
|
||||
);
|
||||
booleanValue(catalog.configured, "Каталог LAB E34.configured");
|
||||
integerValue(catalog.candidate_total, "Каталог LAB E34.candidate_total");
|
||||
integerValue(catalog.invalid_total, "Каталог LAB E34.invalid_total");
|
||||
exactString(
|
||||
catalog.access,
|
||||
"read-only",
|
||||
"Каталог LAB E34.access",
|
||||
);
|
||||
const items = list(catalog.items, "Каталог LAB E34.items");
|
||||
if (items.length > 1) {
|
||||
throw new E34TemporalLayerContractError(
|
||||
"Каталог LAB E34: нарушен limit=1.",
|
||||
);
|
||||
}
|
||||
return items.length ? parseResult(items[0]) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user