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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./styles/workspaces.css";
|
||||
@import "./styles/laboratory.css";
|
||||
@import "./styles/laboratory-reporting.css";
|
||||
@import "./styles/e34-temporal-layer.css";
|
||||
@import "./styles/e30-human-review.css";
|
||||
@import "./styles/spatial.css";
|
||||
@import "./styles/device.css";
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
.e34-temporal-evidence,
|
||||
.e34-temporal-scene {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.e34-temporal-scene {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--nodedc-canvas);
|
||||
}
|
||||
|
||||
.e34-temporal-scene__viewport {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__viewport canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__viewport canvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__toolbar {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 0.6rem;
|
||||
left: 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__toolbar .nodedc-button {
|
||||
background: var(--nodedc-floating-surface);
|
||||
backdrop-filter: blur(var(--nodedc-blur-control));
|
||||
}
|
||||
|
||||
.e34-temporal-scene__gestures,
|
||||
.e34-temporal-scene__legend,
|
||||
.e34-temporal-evidence__telemetry {
|
||||
border-radius: var(--nodedc-radius-control-compact);
|
||||
background: var(--nodedc-floating-surface);
|
||||
color: var(--nodedc-text-secondary);
|
||||
backdrop-filter: blur(var(--nodedc-blur-control));
|
||||
}
|
||||
|
||||
.e34-temporal-scene__gestures {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
padding: 0.43rem 0.55rem;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__gestures span,
|
||||
.e34-temporal-scene__legend span,
|
||||
.e34-temporal-evidence__telemetry dt,
|
||||
.e34-temporal-evidence__telemetry dd {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend {
|
||||
position: absolute;
|
||||
right: 0.6rem;
|
||||
bottom: 0.6rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem;
|
||||
padding: 0.42rem 0.55rem;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend span::before {
|
||||
width: 0.38rem;
|
||||
height: 0.38rem;
|
||||
border-radius: 50%;
|
||||
background: var(--nodedc-text-muted);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend span[data-state="current"]::before {
|
||||
background: rgb(var(--nodedc-accent-rgb));
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend span[data-state="held"]::before {
|
||||
background: rgb(var(--nodedc-warning-rgb));
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend span[data-state="expired"]::before {
|
||||
background: rgb(var(--nodedc-danger-rgb));
|
||||
}
|
||||
|
||||
.e34-temporal-evidence__telemetry {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
left: 0.6rem;
|
||||
bottom: 0.6rem;
|
||||
display: grid;
|
||||
width: min(13rem, calc(100% - 1.2rem));
|
||||
gap: 0.35rem;
|
||||
margin: 0;
|
||||
padding: 0.55rem 0.65rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.e34-temporal-evidence__telemetry > div {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
|
||||
.e34-temporal-evidence__telemetry dt,
|
||||
.e34-temporal-evidence__telemetry dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.e34-temporal-evidence__telemetry dt {
|
||||
color: var(--nodedc-text-muted);
|
||||
}
|
||||
|
||||
.e34-temporal-evidence__telemetry dd {
|
||||
overflow: hidden;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-weight: 650;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e34-temporal-evidence .laboratory-evidence-viewer__controls
|
||||
.nodedc-select-anchor {
|
||||
width: clamp(12rem, 24vw, 20rem);
|
||||
}
|
||||
|
||||
.e34-temporal-scene__error {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.e34-temporal-scene__gestures {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.e34-temporal-scene__legend {
|
||||
left: 0.6rem;
|
||||
right: auto;
|
||||
bottom: 5.7rem;
|
||||
}
|
||||
|
||||
.e34-temporal-evidence .laboratory-evidence-viewer__controls
|
||||
.nodedc-select-anchor {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
|
||||
import type { AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
|
||||
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
|
||||
import type { WorkspaceRendererProps } from "../contracts";
|
||||
import { E31Result } from "./E31Result";
|
||||
import { E32Result } from "./E32Result";
|
||||
import { E33Result } from "./E33Result";
|
||||
import { E34Result } from "./E34Result";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "e31-source-binding"
|
||||
| "e32-track-geometry"
|
||||
| "e33-worker-shadow"
|
||||
| "e34-temporal-layer";
|
||||
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
|
||||
export function isAdvancedLaboratoryWorkId(
|
||||
value: string,
|
||||
): value is AdvancedLaboratoryWorkId {
|
||||
return (
|
||||
value === "e31-source-binding"
|
||||
|| value === "e32-track-geometry"
|
||||
|| value === "e33-worker-shadow"
|
||||
|| value === "e34-temporal-layer"
|
||||
);
|
||||
}
|
||||
|
||||
export function advancedLaboratoryWorkOptions(
|
||||
results: AdvancedLaboratoryResults,
|
||||
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
|
||||
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
|
||||
const options: LaboratoryOption<AdvancedLaboratoryWorkId>[] = [];
|
||||
if (results.e31 && sourceSessions.has(results.e31.sourceSessionId)) {
|
||||
options.push({
|
||||
id: "e31-source-binding",
|
||||
label: "LAB E31 · source binding",
|
||||
});
|
||||
}
|
||||
if (results.e32 && sourceSessions.has(results.e32.sourceSessionId)) {
|
||||
options.push({
|
||||
id: "e32-track-geometry",
|
||||
label: "LAB E32 · TrackGeometry v1",
|
||||
});
|
||||
}
|
||||
if (results.e33 && sourceSessions.has(results.e33.sourceSessionId)) {
|
||||
options.push({
|
||||
id: "e33-worker-shadow",
|
||||
label: "LAB E33 · worker shadow 1×",
|
||||
});
|
||||
}
|
||||
if (results.e34) {
|
||||
options.push({
|
||||
id: "e34-temporal-layer",
|
||||
label: "LAB E34 · temporal occupied/unknown",
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function advancedLaboratorySourceSession(
|
||||
workId: AdvancedLaboratoryWorkId,
|
||||
results: AdvancedLaboratoryResults,
|
||||
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
|
||||
): ObservationSessionSummary | null {
|
||||
const sourceSessionId = workId === "e31-source-binding"
|
||||
? results.e31?.sourceSessionId
|
||||
: workId === "e32-track-geometry"
|
||||
? results.e32?.sourceSessionId
|
||||
: workId === "e33-worker-shadow"
|
||||
? results.e33?.sourceSessionId
|
||||
: null;
|
||||
return sourceSessionId ? sourceSessions.get(sourceSessionId) ?? null : null;
|
||||
}
|
||||
|
||||
export function AdvancedLaboratoryResult({
|
||||
props,
|
||||
rigLabel,
|
||||
workId,
|
||||
results,
|
||||
sourceSessions,
|
||||
replayingSessionId,
|
||||
failedSessionId,
|
||||
replayError,
|
||||
}: {
|
||||
props: LaboratoryWorkspaceProps;
|
||||
rigLabel: string;
|
||||
workId: AdvancedLaboratoryWorkId;
|
||||
results: AdvancedLaboratoryResults;
|
||||
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>;
|
||||
replayingSessionId: string | null;
|
||||
failedSessionId: string | null;
|
||||
replayError: string | null;
|
||||
}) {
|
||||
if (workId === "e34-temporal-layer" && results.e34) {
|
||||
return <E34Result rigLabel={rigLabel} result={results.e34} />;
|
||||
}
|
||||
|
||||
const sourceSession = advancedLaboratorySourceSession(
|
||||
workId,
|
||||
results,
|
||||
sourceSessions,
|
||||
);
|
||||
if (!sourceSession) return null;
|
||||
const evidence = (
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={sourceSession}
|
||||
loading={replayingSessionId === sourceSession.id}
|
||||
error={failedSessionId === sourceSession.id ? replayError : null}
|
||||
/>
|
||||
);
|
||||
if (workId === "e31-source-binding" && results.e31) {
|
||||
return (
|
||||
<E31Result
|
||||
rigLabel={rigLabel}
|
||||
result={results.e31}
|
||||
evidence={evidence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (workId === "e32-track-geometry" && results.e32) {
|
||||
return (
|
||||
<E32Result
|
||||
rigLabel={rigLabel}
|
||||
result={results.e32}
|
||||
evidence={evidence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (workId === "e33-worker-shadow" && results.e33) {
|
||||
return (
|
||||
<E33Result
|
||||
rigLabel={rigLabel}
|
||||
result={results.e33}
|
||||
evidence={evidence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Select } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import type {
|
||||
E34TemporalLayerResult,
|
||||
E34TemporalReviewFrame,
|
||||
} from "../../core/laboratory/e34TemporalLayer";
|
||||
import { formatNumber } from "../../presentation";
|
||||
import {
|
||||
E34TemporalLayerScene,
|
||||
type E34TemporalViewMode,
|
||||
} from "./E34TemporalLayerScene";
|
||||
|
||||
function percent(value: number): string {
|
||||
return `${(value * 100).toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})}%`;
|
||||
}
|
||||
|
||||
function defaultFrame(
|
||||
frames: readonly E34TemporalReviewFrame[],
|
||||
): E34TemporalReviewFrame | null {
|
||||
return frames.find((frame) => (
|
||||
frame.counts.current > 0 && frame.counts.held > 0
|
||||
)) ?? frames.find((frame) => frame.counts.expired > 0) ?? frames[0] ?? null;
|
||||
}
|
||||
|
||||
function frameLabel(frame: E34TemporalReviewFrame): string {
|
||||
const state = frame.counts.expired
|
||||
? `истекло ${frame.counts.expired}`
|
||||
: frame.counts.held
|
||||
? `удерживается ${frame.counts.held}`
|
||||
: `наблюдается ${frame.counts.current}`;
|
||||
return `Кадр ${formatNumber(frame.frameIndex, 0)} · ${state}`;
|
||||
}
|
||||
|
||||
function E34Evidence({
|
||||
result,
|
||||
}: {
|
||||
result: E34TemporalLayerResult;
|
||||
}) {
|
||||
const initial = useMemo(
|
||||
() => defaultFrame(result.reviewFrames),
|
||||
[result.reviewFrames],
|
||||
);
|
||||
const [frameIndex, setFrameIndex] = useState(initial?.frameIndex ?? 0);
|
||||
const [mode, setMode] = useState<E34TemporalViewMode>("3d");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const frame = result.reviewFrames.find(
|
||||
(item) => item.frameIndex === frameIndex,
|
||||
) ?? initial;
|
||||
|
||||
if (!frame) {
|
||||
return (
|
||||
<div className="laboratory-result-pending" role="status">
|
||||
Контрольные состояния временного слоя не опубликованы.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="e34-temporal-evidence">
|
||||
<LaboratoryEvidenceViewer
|
||||
label="Временной occupied/unknown слой E34"
|
||||
mode={mode}
|
||||
modes={[
|
||||
{ value: "3d", label: "3D" },
|
||||
{ value: "plan", label: "План" },
|
||||
]}
|
||||
expanded={expanded}
|
||||
onModeChange={setMode}
|
||||
onExpandedChange={setExpanded}
|
||||
actions={(
|
||||
<Select
|
||||
label="Контрольное состояние E34"
|
||||
value={String(frame.frameIndex)}
|
||||
options={result.reviewFrames.map((item) => ({
|
||||
value: String(item.frameIndex),
|
||||
label: frameLabel(item),
|
||||
}))}
|
||||
variant="split"
|
||||
menuWidth="anchor"
|
||||
onChange={(value) => setFrameIndex(Number(value))}
|
||||
/>
|
||||
)}
|
||||
overlay={(
|
||||
<dl className="e34-temporal-evidence__telemetry">
|
||||
<div>
|
||||
<dt>Кадр / время</dt>
|
||||
<dd>
|
||||
{formatNumber(frame.frameIndex, 0)}
|
||||
{" · "}
|
||||
{frame.sessionSeconds.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 3,
|
||||
})}
|
||||
{" с"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Вход</dt>
|
||||
<dd>{frame.sourceAvailable ? "Есть текущие точки" : "Текущих точек нет"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Состояние слоя</dt>
|
||||
<dd>
|
||||
{frame.counts.current} current · {frame.counts.held} held · {frame.counts.expired} expired
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
>
|
||||
<E34TemporalLayerScene frame={frame} mode={mode} />
|
||||
</LaboratoryEvidenceViewer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function E34Result({
|
||||
rigLabel,
|
||||
result,
|
||||
}: {
|
||||
rigLabel: string;
|
||||
result: E34TemporalLayerResult;
|
||||
}) {
|
||||
const metrics = result.metrics;
|
||||
const config = result.configuration;
|
||||
return (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E34 · короткоживущий occupied/unknown слой"
|
||||
description="Проверяли, может ли TrackGeometry поддерживать ограниченную во времени карту занятости: подтверждённые точки публикуются как occupied, исчезнувшее наблюдение кратко удерживается как unknown и затем удаляется строго по TTL."
|
||||
status="Временной слой принят"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Источник", value: `${rigLabel} · TrackGeometry E32` },
|
||||
{
|
||||
label: "Объём проверки",
|
||||
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров`,
|
||||
},
|
||||
{
|
||||
label: "Параметры слоя",
|
||||
value: `${config.voxelSizeM.toLocaleString("ru-RU")} м · TTL ${config.occupiedTtlSeconds.toLocaleString("ru-RU")} с`,
|
||||
},
|
||||
{ label: "Полномочия", value: "Диагностика · команды и safety выключены" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли сохранить краткую пространственную непрерывность объектов между наблюдениями, не объявляя ненаблюдаемое пространство свободным и не загрязняя постоянную карту?",
|
||||
approach: "Полная запись E32 воспроизведена через hit-only voxel layer в map-frame. Camera-track связывались только по точной идентичности, geometry-only компоненты — по ограниченному пространственно-временному сопоставлению. Первый immutable replay был отклонён; после исправления логических часов TTL и детектора глобального сдвига тот же замороженный профиль выполнен повторно.",
|
||||
principalResult: `Да, в диагностическом контуре. Обработаны все ${formatNumber(metrics.processedFrames, 0)} кадров и все ${formatNumber(metrics.e34ConsumedCurrentPointRows, 0)} текущих точек; непрерывность наблюдений составила ${percent(metrics.continuityFraction)}, задержка логического истечения — ${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с.`,
|
||||
limitation: "Слой не вычисляет свободное пространство, не назначает dynamic/static, не меняет persistent reconstruction и не имеет навигационных или safety-полномочий. Непрерывность — диагностическая метрика, не ground truth.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: "TrackGeometry → short-TTL hit-only occupied/unknown layer",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: "Принятый TrackGeometry из LAB E32",
|
||||
version: `${formatNumber(metrics.e32CurrentPointRows, 0)} квалифицированных точек`,
|
||||
role: "неизменяемый map-frame вход с явной принадлежностью точек",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Bounded temporal component association",
|
||||
version: `voxel ${config.voxelSizeM} м · TTL ${config.occupiedTtlSeconds} с`,
|
||||
role: "точное camera-связывание, geometry-only reassociation, hold и независимое expiry",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Fail-closed diagnostic replay",
|
||||
version: `${metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс/frame p95`,
|
||||
role: "полный учёт кадров, bounds, контроль map-frame и неизменность upstream",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ВРЕМЕННОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Контрольные состояния: наблюдение, удержание unknown и точное истечение"
|
||||
kind="diagnostic-model"
|
||||
resizable
|
||||
>
|
||||
<E34Evidence result={result} />
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title="Краткая непрерывность доказана; свободное пространство намеренно не выводится"
|
||||
status="12 / 12 gate"
|
||||
statusTone="success"
|
||||
metrics={[
|
||||
{
|
||||
label: "Полный replay",
|
||||
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)}`,
|
||||
hint: "все входные точки учтены",
|
||||
},
|
||||
{
|
||||
label: "Связность компонентов",
|
||||
value: percent(metrics.continuityFraction),
|
||||
hint: `${formatNumber(metrics.exactCameraAssociations + metrics.geometrySpatialReassociations, 0)} повторных связей`,
|
||||
},
|
||||
{
|
||||
label: "TTL / истечение",
|
||||
value: `${metrics.maximumHeldAgeSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с`,
|
||||
hint: `${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с задержка`,
|
||||
},
|
||||
{
|
||||
label: "Ограниченность",
|
||||
value: `${formatNumber(metrics.peakActiveComponents, 0)} / ${formatNumber(config.maximumActiveComponents, 0)}`,
|
||||
hint: `${formatNumber(metrics.peakCellsPerComponent, 0)} / ${formatNumber(config.maximumCellsPerComponent, 0)} ячеек`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "На данной полной записи hit-backed компоненты можно детерминированно удерживать до 0,75 с и удалять по независимому deadline, сохраняя bounds, полный учёт и неизменность E32/E33.",
|
||||
notProved: "Работа не доказывает свободное пространство, динамический класс, качество планирования, переносимость порогов на другой сенсорный риг или корректность при реальном глобальном скачке map-frame.",
|
||||
decision: "E34 принимается как read-only диагностический слой. Следующий критический gate — E35: детерминированно проверить деградацию и восстановление при потере источника, нарушении сроков и map-frame fault.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Icon } from "@nodedc/ui-react";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
||||
|
||||
import type {
|
||||
E34Point3,
|
||||
E34TemporalComponent,
|
||||
E34TemporalReviewFrame,
|
||||
} from "../../core/laboratory/e34TemporalLayer";
|
||||
|
||||
export type E34TemporalViewMode = "3d" | "plan";
|
||||
|
||||
function tokenColor(
|
||||
host: HTMLElement,
|
||||
token: string,
|
||||
fallback: readonly [number, number, number],
|
||||
): THREE.Color {
|
||||
const value = getComputedStyle(host).getPropertyValue(token).trim();
|
||||
if (value.startsWith("#")) {
|
||||
return new THREE.Color(value);
|
||||
}
|
||||
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
|
||||
const [red, green, blue] = channels?.length === 3
|
||||
? channels
|
||||
: fallback;
|
||||
return new THREE.Color(red / 255, green / 255, blue / 255);
|
||||
}
|
||||
|
||||
function disposeRenderable(object: THREE.Object3D): void {
|
||||
const renderable = object as THREE.Object3D & {
|
||||
geometry?: THREE.BufferGeometry;
|
||||
material?: THREE.Material | THREE.Material[];
|
||||
};
|
||||
renderable.geometry?.dispose();
|
||||
const materials = Array.isArray(renderable.material)
|
||||
? renderable.material
|
||||
: renderable.material
|
||||
? [renderable.material]
|
||||
: [];
|
||||
materials.forEach((material) => material.dispose());
|
||||
}
|
||||
|
||||
function scenePoint(
|
||||
point: E34Point3,
|
||||
origin: E34Point3,
|
||||
): readonly [number, number, number] {
|
||||
return [
|
||||
point[0] - origin[0],
|
||||
point[2] - origin[2],
|
||||
-(point[1] - origin[1]),
|
||||
];
|
||||
}
|
||||
|
||||
function positions(
|
||||
points: readonly E34Point3[],
|
||||
origin: E34Point3,
|
||||
): Float32Array {
|
||||
const result = new Float32Array(points.length * 3);
|
||||
points.forEach((point, index) => {
|
||||
const [x, y, z] = scenePoint(point, origin);
|
||||
const offset = index * 3;
|
||||
result[offset] = x;
|
||||
result[offset + 1] = y;
|
||||
result[offset + 2] = z;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function median(values: readonly number[]): number {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
||||
}
|
||||
|
||||
function frameOrigin(frame: E34TemporalReviewFrame): E34Point3 {
|
||||
const anchors = frame.components.length
|
||||
? frame.components.map((component) => component.centroidMapXyzM)
|
||||
: frame.cellCentersMapXyzM;
|
||||
return [
|
||||
median(anchors.map((point) => point[0])),
|
||||
median(anchors.map((point) => point[1])),
|
||||
median(anchors.map((point) => point[2])),
|
||||
];
|
||||
}
|
||||
|
||||
function componentColor(
|
||||
host: HTMLElement,
|
||||
component: E34TemporalComponent,
|
||||
): THREE.Color {
|
||||
if (component.state === "expired") {
|
||||
return tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112]);
|
||||
}
|
||||
if (component.state === "held") {
|
||||
return tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102]);
|
||||
}
|
||||
return tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]);
|
||||
}
|
||||
|
||||
function fitRadius(frame: E34TemporalReviewFrame, origin: E34Point3): number {
|
||||
const points = [
|
||||
...frame.cellCentersMapXyzM,
|
||||
...frame.components.map((component) => component.centroidMapXyzM),
|
||||
];
|
||||
if (!points.length) return 4;
|
||||
const distances = points
|
||||
.map((point) => {
|
||||
const [x, y, z] = scenePoint(point, origin);
|
||||
return Math.hypot(x, y, z);
|
||||
})
|
||||
.sort((left, right) => left - right);
|
||||
const p95 = distances[Math.floor((distances.length - 1) * 0.95)] ?? 4;
|
||||
return THREE.MathUtils.clamp(p95, 3, 32);
|
||||
}
|
||||
|
||||
export function E34TemporalLayerScene({
|
||||
frame,
|
||||
mode,
|
||||
}: {
|
||||
frame: E34TemporalReviewFrame;
|
||||
mode: E34TemporalViewMode;
|
||||
}) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const controlsRef = useRef<OrbitControls | null>(null);
|
||||
const contentRef = useRef<THREE.Group | null>(null);
|
||||
const viewRadiusRef = useRef(5);
|
||||
const [renderError, setRenderError] = useState<string | null>(null);
|
||||
const origin = useMemo(() => frameOrigin(frame), [frame]);
|
||||
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
if (!host) return;
|
||||
let renderer: THREE.WebGLRenderer;
|
||||
try {
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
} catch {
|
||||
setRenderError("Браузер не смог создать 3D-сцену временного слоя.");
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setClearColor(
|
||||
tokenColor(host, "--nodedc-canvas", [5, 5, 6]),
|
||||
1,
|
||||
);
|
||||
renderer.domElement.setAttribute(
|
||||
"aria-label",
|
||||
"Интерактивная 3D-сцена временного occupied/unknown слоя E34",
|
||||
);
|
||||
renderer.domElement.setAttribute("role", "img");
|
||||
host.prepend(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 500);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.08;
|
||||
controls.enablePan = true;
|
||||
controls.enableZoom = true;
|
||||
controls.screenSpacePanning = true;
|
||||
controls.minDistance = 0.5;
|
||||
controls.maxDistance = 160;
|
||||
controls.target.set(0, 0, 0);
|
||||
|
||||
const content = new THREE.Group();
|
||||
scene.add(content);
|
||||
sceneRef.current = scene;
|
||||
cameraRef.current = camera;
|
||||
controlsRef.current = controls;
|
||||
contentRef.current = content;
|
||||
|
||||
const resize = () => {
|
||||
const width = Math.max(host.clientWidth, 1);
|
||||
const height = Math.max(host.clientHeight, 1);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width, height, false);
|
||||
};
|
||||
const observer = new ResizeObserver(resize);
|
||||
observer.observe(host);
|
||||
resize();
|
||||
|
||||
let animationFrame = 0;
|
||||
const render = () => {
|
||||
animationFrame = window.requestAnimationFrame(render);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
render();
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
observer.disconnect();
|
||||
controls.dispose();
|
||||
scene.traverse(disposeRenderable);
|
||||
renderer.dispose();
|
||||
renderer.domElement.remove();
|
||||
sceneRef.current = null;
|
||||
cameraRef.current = null;
|
||||
controlsRef.current = null;
|
||||
contentRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
const content = contentRef.current;
|
||||
if (!host || !content) return;
|
||||
while (content.children.length) {
|
||||
const child = content.children[0];
|
||||
if (!child) continue;
|
||||
content.remove(child);
|
||||
child.traverse(disposeRenderable);
|
||||
}
|
||||
|
||||
const contextGeometry = new THREE.BufferGeometry();
|
||||
contextGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(
|
||||
positions(frame.cellCentersMapXyzM, origin),
|
||||
3,
|
||||
),
|
||||
);
|
||||
const context = new THREE.Points(
|
||||
contextGeometry,
|
||||
new THREE.PointsMaterial({
|
||||
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
|
||||
size: 2.4,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
opacity: 0.32,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
content.add(context);
|
||||
|
||||
for (const component of frame.components) {
|
||||
const color = componentColor(host, component);
|
||||
const markerGeometry = new THREE.BufferGeometry();
|
||||
markerGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(
|
||||
positions([component.centroidMapXyzM], origin),
|
||||
3,
|
||||
),
|
||||
);
|
||||
const marker = new THREE.Points(
|
||||
markerGeometry,
|
||||
new THREE.PointsMaterial({
|
||||
color,
|
||||
size: component.state === "expired" ? 8 : 6,
|
||||
sizeAttenuation: false,
|
||||
transparent: true,
|
||||
opacity: component.state === "expired" ? 0.72 : 1,
|
||||
depthWrite: false,
|
||||
}),
|
||||
);
|
||||
content.add(marker);
|
||||
|
||||
if (component.history.length > 1) {
|
||||
const trailGeometry = new THREE.BufferGeometry();
|
||||
trailGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.BufferAttribute(
|
||||
positions(
|
||||
component.history.map((item) => item.centroidMapXyzM),
|
||||
origin,
|
||||
),
|
||||
3,
|
||||
),
|
||||
);
|
||||
content.add(new THREE.Line(
|
||||
trailGeometry,
|
||||
new THREE.LineBasicMaterial({
|
||||
color,
|
||||
transparent: true,
|
||||
opacity: component.state === "expired" ? 0.28 : 0.54,
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const radius = fitRadius(frame, origin);
|
||||
viewRadiusRef.current = radius;
|
||||
const grid = new THREE.GridHelper(
|
||||
radius * 2.4,
|
||||
20,
|
||||
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
|
||||
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
|
||||
);
|
||||
const materials = Array.isArray(grid.material)
|
||||
? grid.material
|
||||
: [grid.material];
|
||||
materials.forEach((material) => {
|
||||
material.transparent = true;
|
||||
material.opacity = 0.15;
|
||||
material.depthWrite = false;
|
||||
});
|
||||
content.add(grid);
|
||||
}, [frame, origin]);
|
||||
|
||||
const resetView = () => {
|
||||
const camera = cameraRef.current;
|
||||
const controls = controlsRef.current;
|
||||
if (!camera || !controls) return;
|
||||
const radius = viewRadiusRef.current;
|
||||
controls.target.set(0, 0, 0);
|
||||
if (mode === "plan") {
|
||||
camera.position.set(0, radius * 2.8, 0.001);
|
||||
camera.up.set(0, 0, -1);
|
||||
} else {
|
||||
camera.position.set(radius * 1.35, radius * 0.9, radius * 1.35);
|
||||
camera.up.set(0, 1, 0);
|
||||
}
|
||||
camera.near = Math.max(radius / 2_000, 0.005);
|
||||
camera.far = Math.max(radius * 20, 120);
|
||||
camera.updateProjectionMatrix();
|
||||
controls.maxDistance = Math.max(radius * 8, 40);
|
||||
controls.update();
|
||||
};
|
||||
|
||||
useEffect(resetView, [frame, mode]);
|
||||
|
||||
return (
|
||||
<div className="e34-temporal-scene">
|
||||
<div ref={hostRef} className="e34-temporal-scene__viewport">
|
||||
{renderError ? (
|
||||
<p className="e34-temporal-scene__error">{renderError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="e34-temporal-scene__toolbar">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="compact"
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
onClick={resetView}
|
||||
>
|
||||
Сбросить ракурс
|
||||
</Button>
|
||||
<div className="e34-temporal-scene__gestures">
|
||||
<span>ЛКМ · вращение</span>
|
||||
<span>Колесо · масштаб</span>
|
||||
<span>ПКМ · панорама</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="e34-temporal-scene__legend">
|
||||
<span data-state="cells">
|
||||
Ячейки · {frame.cellCentersMapXyzM.length}
|
||||
</span>
|
||||
<span data-state="current">Наблюдается · {frame.counts.current}</span>
|
||||
<span data-state="held">Удерживается · {frame.counts.held}</span>
|
||||
<span data-state="expired">Истекло · {frame.counts.expired}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,9 +29,7 @@ import {
|
||||
} from "../../core/laboratory/e30Review";
|
||||
import {
|
||||
fetchAdvancedLaboratoryResults,
|
||||
type E31LaboratoryResult,
|
||||
type E32LaboratoryResult,
|
||||
type E33LaboratoryResult,
|
||||
type AdvancedLaboratoryResults,
|
||||
} from "../../core/laboratory/advancedResults";
|
||||
import {
|
||||
fetchLidarLocalSurfaces,
|
||||
@@ -41,14 +39,17 @@ import { formatNumber } from "../../presentation";
|
||||
import { E30ReviewWorkspace } from "../E30ReviewWorkspace";
|
||||
import { LidarQualityWorkspace } from "../LidarQualityWorkspace";
|
||||
import type { WorkspaceRendererProps } from "../contracts";
|
||||
import { E31Result } from "./E31Result";
|
||||
import { E32Result } from "./E32Result";
|
||||
import { E33Result } from "./E33Result";
|
||||
import {
|
||||
AdvancedLaboratoryResult,
|
||||
advancedLaboratorySourceSession,
|
||||
advancedLaboratoryWorkOptions,
|
||||
isAdvancedLaboratoryWorkId,
|
||||
type AdvancedLaboratoryWorkId,
|
||||
} from "./AdvancedLaboratoryResult";
|
||||
import {
|
||||
e28LaboratoryBrief, e29LaboratoryBrief,
|
||||
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
|
||||
} from "./laboratoryArchiveBriefs";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
@@ -58,11 +59,16 @@ type LaboratoryWorkId =
|
||||
| "e28-local-surface"
|
||||
| "e29-camera-geometry"
|
||||
| "e30-evidence-review"
|
||||
| "e31-source-binding"
|
||||
| "e32-track-geometry"
|
||||
| "e33-worker-shadow"
|
||||
| AdvancedLaboratoryWorkId
|
||||
| `session:${string}`;
|
||||
|
||||
const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
|
||||
e31: null,
|
||||
e32: null,
|
||||
e33: null,
|
||||
e34: null,
|
||||
};
|
||||
|
||||
function digestFromContentId(value: string | null | undefined): string | null {
|
||||
const digest = value?.split("-").at(-1) ?? "";
|
||||
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
|
||||
@@ -554,9 +560,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
|
||||
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
|
||||
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
|
||||
const [e31Result, setE31Result] = useState<E31LaboratoryResult | null>(null);
|
||||
const [e32Result, setE32Result] = useState<E32LaboratoryResult | null>(null);
|
||||
const [e33Result, setE33Result] = useState<E33LaboratoryResult | null>(null);
|
||||
const [advancedResults, setAdvancedResults] = useState(
|
||||
EMPTY_ADVANCED_RESULTS,
|
||||
);
|
||||
const [evidenceLoading, setEvidenceLoading] = useState(true);
|
||||
const [evidenceError, setEvidenceError] = useState<string | null>(null);
|
||||
const sessions = useObservationSessions({
|
||||
@@ -598,14 +604,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
setE28Model(nextE28);
|
||||
setE29Result(nextE29);
|
||||
setE30Result(nextE30);
|
||||
setE31Result(nextAdvanced?.e31 ?? null);
|
||||
setE32Result(nextAdvanced?.e32 ?? null);
|
||||
setE33Result(nextAdvanced?.e33 ?? null);
|
||||
setAdvancedResults(nextAdvanced ?? EMPTY_ADVANCED_RESULTS);
|
||||
const failures = [
|
||||
e28.status === "rejected" ? "E28" : null,
|
||||
e29.status === "rejected" ? "E29" : null,
|
||||
e30.status === "rejected" ? "E30" : null,
|
||||
advanced.status === "rejected" ? "E31–E33" : null,
|
||||
advanced.status === "rejected" ? "E31–E34" : null,
|
||||
].filter(Boolean);
|
||||
setEvidenceError(
|
||||
failures.length
|
||||
@@ -651,32 +655,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
label: "LAB E30 · evidence review A2",
|
||||
});
|
||||
}
|
||||
if (e31Result && sourceSessions.has(e31Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e31-source-binding",
|
||||
label: "LAB E31 · source binding",
|
||||
});
|
||||
}
|
||||
if (e32Result && sourceSessions.has(e32Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e32-track-geometry",
|
||||
label: "LAB E32 · TrackGeometry v1",
|
||||
});
|
||||
}
|
||||
if (e33Result && sourceSessions.has(e33Result.sourceSessionId)) {
|
||||
items.push({
|
||||
id: "e33-worker-shadow",
|
||||
label: "LAB E33 · worker shadow 1×",
|
||||
});
|
||||
}
|
||||
items.push(...advancedLaboratoryWorkOptions(
|
||||
advancedResults,
|
||||
sourceSessions,
|
||||
));
|
||||
return items;
|
||||
}, [
|
||||
advancedResults,
|
||||
e28Model,
|
||||
e29Result,
|
||||
e30Result,
|
||||
e31Result,
|
||||
e32Result,
|
||||
e33Result,
|
||||
sourceSessions,
|
||||
]);
|
||||
const profiles = useMemo(() => {
|
||||
@@ -714,15 +702,6 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
const e30SourceSession = e30Result
|
||||
? sourceSessions.get(e30Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e31SourceSession = e31Result
|
||||
? sourceSessions.get(e31Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e32SourceSession = e32Result
|
||||
? sourceSessions.get(e32Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
const e33SourceSession = e33Result
|
||||
? sourceSessions.get(e33Result.sourceSessionId) ?? null
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -786,13 +765,13 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
void sessions.replay(e30SourceSession.id);
|
||||
return;
|
||||
}
|
||||
const advancedSession = next === "e31-source-binding"
|
||||
? e31SourceSession
|
||||
: next === "e32-track-geometry"
|
||||
? e32SourceSession
|
||||
: next === "e33-worker-shadow"
|
||||
? e33SourceSession
|
||||
: null;
|
||||
const advancedSession = isAdvancedLaboratoryWorkId(next)
|
||||
? advancedLaboratorySourceSession(
|
||||
next,
|
||||
advancedResults,
|
||||
sourceSessions,
|
||||
)
|
||||
: null;
|
||||
if (advancedSession) {
|
||||
void sessions.replay(advancedSession.id);
|
||||
}
|
||||
@@ -940,44 +919,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
result={e30Result}
|
||||
sourceSession={e30SourceSession}
|
||||
/>
|
||||
) : workId === "e31-source-binding" && e31Result && e31SourceSession ? (
|
||||
<E31Result
|
||||
) : isAdvancedLaboratoryWorkId(workId) ? (
|
||||
<AdvancedLaboratoryResult
|
||||
props={props}
|
||||
rigLabel={rigLabel}
|
||||
result={e31Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e31SourceSession}
|
||||
loading={sessions.replayingSessionId === e31SourceSession.id}
|
||||
error={sessions.failedSessionId === e31SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : workId === "e32-track-geometry" && e32Result && e32SourceSession ? (
|
||||
<E32Result
|
||||
rigLabel={rigLabel}
|
||||
result={e32Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e32SourceSession}
|
||||
loading={sessions.replayingSessionId === e32SourceSession.id}
|
||||
error={sessions.failedSessionId === e32SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : workId === "e33-worker-shadow" && e33Result && e33SourceSession ? (
|
||||
<E33Result
|
||||
rigLabel={rigLabel}
|
||||
result={e33Result}
|
||||
evidence={(
|
||||
<RecordedReplayEvidence
|
||||
props={props}
|
||||
sourceSession={e33SourceSession}
|
||||
loading={sessions.replayingSessionId === e33SourceSession.id}
|
||||
error={sessions.failedSessionId === e33SourceSession.id ? sessions.error : null}
|
||||
/>
|
||||
)}
|
||||
workId={workId}
|
||||
results={advancedResults}
|
||||
sourceSessions={sourceSessions}
|
||||
replayingSessionId={sessions.replayingSessionId}
|
||||
failedSessionId={sessions.failedSessionId}
|
||||
replayError={sessions.error}
|
||||
/>
|
||||
) : selectedSession ? (
|
||||
<PublishedLaboratoryResult
|
||||
|
||||
@@ -123,6 +123,89 @@ function e33() {
|
||||
};
|
||||
}
|
||||
|
||||
function e34() {
|
||||
return {
|
||||
result_id: `e34-temporal-occupied-${"4".repeat(64)}`,
|
||||
created_at_utc: "2026-07-27T13:28:30Z",
|
||||
source_session_id: "20260720T065719Z_viewer_live",
|
||||
status: "accepted-bounded-occupied-unknown-temporal-layer",
|
||||
e32_result_id: e32().result_id,
|
||||
e33_result_id: e33().result_id,
|
||||
profile_id: "e34-short-ttl-hit-only-occupied-unknown/v1",
|
||||
pipeline_id: "lidar-local-map/short-ttl-hit-only/v1",
|
||||
coordinate_frame: "map",
|
||||
configuration: {
|
||||
voxel_size_m: 0.45,
|
||||
occupied_ttl_seconds: 0.75,
|
||||
maximum_active_components: 256,
|
||||
maximum_cells_per_component: 4096,
|
||||
geometry_maximum_gap_seconds: 0.35,
|
||||
geometry_maximum_centroid_distance_m: 0.9,
|
||||
},
|
||||
metrics: {
|
||||
source_frames: 4489,
|
||||
processed_frames: 4489,
|
||||
frames_with_current_layer: 3928,
|
||||
held_only_frames: 540,
|
||||
unknown_empty_frames: 21,
|
||||
created_components: 4912,
|
||||
camera_created_components: 1027,
|
||||
geometry_created_components: 3885,
|
||||
geometry_reassociated_components: 2366,
|
||||
exact_camera_associations: 5168,
|
||||
geometry_spatial_reassociations: 17433,
|
||||
held_publications: 39664,
|
||||
expired_components: 4895,
|
||||
peak_active_components: 37,
|
||||
maximum_observed_span_seconds: 13.394,
|
||||
continuity_fraction: 0.821466,
|
||||
geometry_only_reassociation_fraction: 0.81776,
|
||||
e32_current_point_rows: 2119302,
|
||||
e34_consumed_current_point_rows: 2119302,
|
||||
current_cell_rows: 834478,
|
||||
held_cell_rows: 531581,
|
||||
stored_cell_rows: 1366059,
|
||||
free_cell_rows: 0,
|
||||
peak_cells_per_component: 324,
|
||||
maximum_held_age_seconds: 0.75,
|
||||
maximum_expiry_delay_seconds: 0,
|
||||
maximum_replay_materialization_delay_seconds: 0.283,
|
||||
map_frame_jump_candidates: 0,
|
||||
frame_processing_p95_ms: 1.483,
|
||||
build_elapsed_ms: 4317,
|
||||
},
|
||||
review: {
|
||||
schema_version: "missioncore.e34-temporal-review-timeline/v1",
|
||||
result_id: `e34-temporal-occupied-${"4".repeat(64)}`,
|
||||
frames: [{
|
||||
frame_index: 238,
|
||||
session_seconds: 59.206,
|
||||
source_available: false,
|
||||
layer_state: "unknown",
|
||||
counts: { current: 0, held: 0, expired: 1 },
|
||||
cell_centers_map_xyz_m: [],
|
||||
components: [{
|
||||
temporal_id: 136,
|
||||
state: "expired",
|
||||
occupancy_state: "unknown",
|
||||
owner_kind: "camera-track",
|
||||
centroid_map_xyz_m: [20.25, -2.83, 0.21],
|
||||
last_observed_age_seconds: 0.75,
|
||||
association_reason: "independent-ttl-deadline",
|
||||
history_tail: [{
|
||||
frame_index: 230,
|
||||
session_seconds: 58.456,
|
||||
centroid_map_xyz_m: [20.25, -2.83, 0.21],
|
||||
}],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
acceptance: { accepted: true },
|
||||
authority,
|
||||
access: "read-only",
|
||||
};
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
@@ -139,9 +222,9 @@ after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
test("decodes E31, E32 and E33 from separate read-only catalogs", async () => {
|
||||
test("decodes E31–E34 from separate read-only catalogs", async () => {
|
||||
const requests = [];
|
||||
const items = [e31(), e32(), e33()];
|
||||
const items = [e31(), e32(), e33(), e34()];
|
||||
const decoded = await fetchAdvancedLaboratoryResults({
|
||||
fetcher: async (input, init) => {
|
||||
requests.push({ input: String(input), method: init?.method });
|
||||
@@ -156,10 +239,13 @@ test("decodes E31, E32 and E33 from separate read-only catalogs", async () => {
|
||||
assert.equal(decoded.e33.metrics.deliveredFrames, 4489);
|
||||
assert.equal(decoded.e33.metrics.sourceAvailableFrames, 3928);
|
||||
assert.equal(decoded.e33.metrics.resultAgeMaxMs, 13.638);
|
||||
assert.equal(decoded.e34.metrics.processedFrames, 4489);
|
||||
assert.equal(decoded.e34.reviewFrames[0].components[0].state, "expired");
|
||||
assert.deepEqual(requests, [
|
||||
{ input: "/api/v1/laboratory/e31/results?limit=1", method: "GET" },
|
||||
{ input: "/api/v1/laboratory/e32/results?limit=1", method: "GET" },
|
||||
{ input: "/api/v1/laboratory/e33/results?limit=1", method: "GET" },
|
||||
{ input: "/api/v1/laboratory/e34/results?limit=1", method: "GET" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -172,7 +258,8 @@ test("rejects authority escalation in an accepted-looking result", async () => {
|
||||
() => fetchAdvancedLaboratoryResults({
|
||||
fetcher: async (input) => new Response(JSON.stringify(catalog(
|
||||
String(input).includes("/e31/") ? forged
|
||||
: String(input).includes("/e32/") ? e32() : e33(),
|
||||
: String(input).includes("/e32/") ? e32()
|
||||
: String(input).includes("/e33/") ? e33() : e34(),
|
||||
)), { status: 200 }),
|
||||
}),
|
||||
AdvancedLaboratoryContractError,
|
||||
|
||||
@@ -22,6 +22,14 @@ const e33ResultUrl = new URL(
|
||||
"../src/workspaces/laboratory/E33Result.tsx",
|
||||
import.meta.url,
|
||||
);
|
||||
const e34ResultUrl = new URL(
|
||||
"../src/workspaces/laboratory/E34Result.tsx",
|
||||
import.meta.url,
|
||||
);
|
||||
const advancedLaboratoryResultUrl = new URL(
|
||||
"../src/workspaces/laboratory/AdvancedLaboratoryResult.tsx",
|
||||
import.meta.url,
|
||||
);
|
||||
const workspacesUrl = new URL(
|
||||
"../src/workspaces/Workspaces.tsx",
|
||||
import.meta.url,
|
||||
@@ -136,6 +144,23 @@ test("E33 explains the experiment, its method and its retained limits", async ()
|
||||
assert.doesNotMatch(e33Source, /name: result\.e32ResultId/);
|
||||
});
|
||||
|
||||
test("E34 keeps temporal evidence inside the canonical LAB and viewer contracts", async () => {
|
||||
const [e34Source, advancedSource] = await Promise.all([
|
||||
readFile(e34ResultUrl, "utf8"),
|
||||
readFile(advancedLaboratoryResultUrl, "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(e34Source, /<LaboratoryWorkTemplate/);
|
||||
assert.match(e34Source, /<LaboratoryEvidenceViewer/);
|
||||
assert.match(e34Source, /\{ value: "3d", label: "3D" \}/);
|
||||
assert.match(e34Source, /\{ value: "plan", label: "План" \}/);
|
||||
assert.match(e34Source, /Первый immutable replay был отклонён/);
|
||||
assert.match(e34Source, /Слой не вычисляет свободное пространство/);
|
||||
assert.match(e34Source, /Следующий критический gate — E35/);
|
||||
assert.match(advancedSource, /id: "e34-temporal-layer"/);
|
||||
assert.match(advancedSource, /<E34Result/);
|
||||
});
|
||||
|
||||
test("the primary point-cloud viewer restores from fullscreen on Escape", async () => {
|
||||
const workspacesSource = await readFile(workspacesUrl, "utf8");
|
||||
|
||||
|
||||
@@ -681,10 +681,23 @@ workloads and are evidence of node visibility, not E33 process consumption.
|
||||
E33 qualifies downstream publication against the immutable E10→E32 chain; it
|
||||
does not rerun or independently requalify upstream inference.
|
||||
|
||||
Execution was strictly sequential through E33: E30 determined what E31 was
|
||||
E34 accepted immutable temporal-layer result
|
||||
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
|
||||
It consumes all `2,119,302` accepted E32 point rows, keeps current occupied
|
||||
cells separate from held/expired unknown state and preserves the persistent
|
||||
reconstruction. Peak state is 37 components and 324 cells per component;
|
||||
component continuity is `82.15%`, geometry-only re-association is `81.78%`,
|
||||
logical TTL emission delay is zero and coherent global map-frame jump count is
|
||||
zero. The earlier rejected immutable run exposed and retained a `0.333 s`
|
||||
source gap plus an incoherent nearest-neighbor jump detector; the fixed
|
||||
implementation uses an independent TTL deadline and coherent translation
|
||||
support without changing the frozen profile.
|
||||
|
||||
Execution was strictly sequential through E34: E30 determined what E31 was
|
||||
allowed to change; E31 determined the E32 profile; E32 determined the E33
|
||||
runtime input. Exact accounting is now closed, so E34 and E35 are the active
|
||||
critical path.
|
||||
runtime input; E32/E33 then bound the E34 layer. Exact accounting and the
|
||||
bounded nominal temporal layer are now closed, so E35 is the active critical
|
||||
path.
|
||||
E36 is the first generalization gate. A separate product decision follows:
|
||||
either keep the result as operator/shadow evidence, or start L5 occupied-space
|
||||
integration. No LAB in this cycle can enable navigation, commands or safety
|
||||
|
||||
@@ -192,7 +192,17 @@ p95 was `2.668 ms`, process RSS peaked at `92.34 MiB`, both queues remained
|
||||
bounded at depth two and the exact physical worker/container identity is
|
||||
retained. This qualifies the final TrackGeometry publication stage against the
|
||||
immutable upstream E10→E32 chain; it is not a new model-inference benchmark.
|
||||
A8/E34+E35 is now the critical path.
|
||||
A8/E34 is complete in immutable result
|
||||
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
|
||||
It consumes all `2,119,302` accepted E32 point rows across all 4,489 frames,
|
||||
keeps hit-only occupied evidence separate from persistent reconstruction,
|
||||
publishes held/expired state as unknown, peaks at 37 active components and 324
|
||||
cells per component, and emits no free cells. Geometry-only re-association is
|
||||
`81.78%`; overall component continuity is `82.15%`. Expiration uses an
|
||||
independent `0.75 s` deadline and records the `0.283 s` worst source-gap
|
||||
materialization delay separately. The first immutable run remains a rejected
|
||||
artifact documenting the frame-clock and incoherent-jump implementation
|
||||
failures. E35 is now the critical path.
|
||||
|
||||
- [x] Reproduce all 4,489 immutable E29 frames with the exact frozen profile
|
||||
before applying E31/E30 changes.
|
||||
@@ -212,7 +222,7 @@ A8/E34+E35 is now the critical path.
|
||||
and publish per-frame health/timing plus resource telemetry.
|
||||
- [x] Bind the accepted result to physical worker `DESKTOP-OPJ8J04` and the
|
||||
pinned container image; independently verify all result artifacts.
|
||||
- [ ] Build E34 as a separate short-TTL occupied/unknown temporal layer over
|
||||
- [x] Build E34 as a separate short-TTL occupied/unknown temporal layer over
|
||||
accepted E32/E33 evidence.
|
||||
- [ ] Run E35 deterministic degradation/recovery variants without changing
|
||||
the immutable source or accepted E32/E33 results.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# ADR 0027 — E34 short-TTL occupied/unknown temporal layer
|
||||
|
||||
Date: 2026-07-27
|
||||
|
||||
Status: accepted for source-scoped diagnostic/shadow use
|
||||
|
||||
## Context
|
||||
|
||||
E32 published exclusive, source-indexed `PointSlab` ownership over all 4,489
|
||||
immutable RAVNOVES00 frames. E33 proved that the exact E32 publication stream
|
||||
can be delivered at recorded `1.0×` pace through bounded worker channels with
|
||||
closed accounting. Neither result is a local temporal model: geometry-only
|
||||
component keys are frame-local, current support disappears immediately when a
|
||||
source is unavailable, and there is no explicit expiration event.
|
||||
|
||||
The K1 stream is already registered mapped evidence. It does not retain the
|
||||
beam origin and firing time required for honest ray-cleared free-space
|
||||
mapping. The accepted persistent reconstruction must also remain a separate,
|
||||
immutable derivative.
|
||||
|
||||
## Predeclared decision
|
||||
|
||||
1. E34 consumes only the accepted content-addressed E32 result under the
|
||||
accepted E33 timing/accounting envelope. It never rewrites E32, E33, raw
|
||||
evidence, the mapped stream or persistent reconstruction.
|
||||
2. The output is a separate `lidar-local-map/v1` diagnostic derivative in the
|
||||
E32 `map` frame. It publishes only hit-backed `occupied` and conservatively
|
||||
aged `unknown` state. It never publishes `free`.
|
||||
3. Current E32 point owners become `current/occupied` temporal components.
|
||||
Camera track identity is retained only as source provenance. Geometry-only
|
||||
components receive new temporal IDs and may be spatially re-associated
|
||||
without inventing a semantic class.
|
||||
4. A component without current accepted points becomes `held/unknown` for at
|
||||
most `0.75 s`. Its last hit-backed cells remain explicitly stale evidence,
|
||||
not current measurement and not free-space proof.
|
||||
5. After the TTL, the component emits one `expired/unknown` tombstone and is
|
||||
removed from the active occupied layer. Expired cells are never published
|
||||
as occupied.
|
||||
6. Geometry-only re-association is deterministic, one-to-one and bounded to a
|
||||
`0.35 s` gap and `0.9 m` centroid distance. It additionally requires either
|
||||
a `0.05` neighbor-expanded voxel overlap or a centroid distance no greater
|
||||
than one `0.45 m` voxel.
|
||||
7. The layer uses hit-only `0.45 m` voxels, at most `4,096` cells per component
|
||||
and at most `256` active components. Crossing a bound fails closed; cells
|
||||
are not silently truncated and components are not silently evicted.
|
||||
8. A map-frame jump candidate is diagnostic only. It requires at least four
|
||||
adjacent matched components within `0.25 s`, median displacement of at
|
||||
least `1.5 m` and 25th-percentile displacement of at least `0.9 m`.
|
||||
9. Acceptance requires complete 4,489-frame accounting, exact upstream
|
||||
artifact identity, bounded state, no free-space publication, no component
|
||||
held past TTL, expiry no later than one `0.25 s` observation interval after
|
||||
the theoretical deadline and zero global map-frame jump candidates.
|
||||
10. Continuity, re-association, geometry-only persistence and ghost lifetime
|
||||
are measured and reported. They are not converted into detector accuracy
|
||||
or ground truth and are not retuned after inspecting this run.
|
||||
11. Dynamic class, traversability, planner input, commands, navigation and
|
||||
safety authority remain unavailable.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The E34 result can prove a bounded temporal contract without claiming
|
||||
free-space quality or changing persistent mapping.
|
||||
- Camera-only, conflict, unknown and source-unavailable inputs can preserve
|
||||
uncertainty but cannot create new occupied cells without prior accepted
|
||||
hit-backed provenance.
|
||||
- A rejected run remains immutable evidence. Any later profile change requires
|
||||
a new ADR decision, profile identity and LAB result rather than overwriting
|
||||
E34.
|
||||
|
||||
## Execution outcome
|
||||
|
||||
The first immutable implementation result
|
||||
`e34-temporal-occupied-1bab293c1867100e172923b189f8273c47eacca3dd179fd54573200906affd7c`
|
||||
was rejected without changing the profile. It exposed two implementation-level
|
||||
architecture errors:
|
||||
|
||||
- scalar nearest-neighbor displacement was not sufficient to call a jump
|
||||
global; the corrected detector requires one coherent translation within the
|
||||
already fixed voxel residual;
|
||||
- frame-driven expiry could not meet the TTL across a recorded `0.333 s`
|
||||
source gap; expiration now uses an independent logical layer deadline and
|
||||
retains the later replay materialization time separately.
|
||||
|
||||
The accepted result is
|
||||
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
|
||||
It processes all 4,489 frames and all 2,119,302 E32 point rows, peaks at 37
|
||||
active components and 324 cells per component, emits zero free cells, meets
|
||||
the `0.75 s` TTL exactly and reports zero coherent global map-frame jump
|
||||
candidates. A8 now continues with E35 deterministic degradation/recovery.
|
||||
@@ -0,0 +1,182 @@
|
||||
# LAB E34 — bounded short-TTL occupied/unknown temporal layer
|
||||
|
||||
Date: 2026-07-27
|
||||
Status: accepted for source-scoped diagnostic/shadow use; dynamic class, free
|
||||
space, planner, navigation, safety and command authority are unavailable
|
||||
Immutable result:
|
||||
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`
|
||||
|
||||
## Objective and architecture stage
|
||||
|
||||
E34 closes the occupied-layer half of A8. It asks whether accepted E32
|
||||
hit-backed geometry can form a bounded local temporal model without changing
|
||||
raw/map evidence or polluting persistent reconstruction.
|
||||
|
||||
The result is a separate `lidar-local-map/short-ttl-hit-only/v1` derivative. It makes
|
||||
`current`, `held` and `expired` states explicit, spatially re-associates
|
||||
frame-local geometry-only components, preserves camera identity only as source
|
||||
provenance and never converts missing mapped points into free space.
|
||||
|
||||
## Immutable inputs and predeclared profile
|
||||
|
||||
- E32:
|
||||
`e32-track-geometry-a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd`.
|
||||
- Accepted E33 timing/accounting envelope:
|
||||
`e33-worker-shadow-05cc0bb264410fd49536df90e94067ac39731aff0322a8873700d40008a8bb3a`.
|
||||
- Source: RAVNOVES00 / `20260720T065719Z_viewer_live`.
|
||||
- Frames: `4,489`.
|
||||
- Timeline: `35.421857292–484.044857292 s`.
|
||||
- Coordinate frame: accepted E32 `map`.
|
||||
- Profile:
|
||||
`missioncore.e34-temporal-occupied-profile/v1`,
|
||||
`e34-short-ttl-hit-only-occupied-unknown/v1`.
|
||||
|
||||
The profile and ADR 0027 were written before the first full replay:
|
||||
|
||||
- hit-only voxel size `0.45 m`;
|
||||
- occupied TTL `0.75 s`;
|
||||
- maximum `256` active components;
|
||||
- maximum `4,096` cells per component;
|
||||
- geometry-only re-association gap `0.35 s`;
|
||||
- centroid gate `0.9 m`;
|
||||
- neighbor-expanded voxel overlap minimum `0.05`;
|
||||
- no free space, dynamic class or persistent-map mutation;
|
||||
- navigation, safety and command authority false.
|
||||
|
||||
No threshold or bound was changed after inspecting either run.
|
||||
|
||||
## Method and algorithms
|
||||
|
||||
For every E32 frame the E34 layer:
|
||||
|
||||
1. reconstructs the exact validated `TrackGeometryFrame` and memory-mapped
|
||||
`PointSlab`;
|
||||
2. consumes only geometries whose metric basis is `current-points`;
|
||||
3. voxelizes their exclusive map-frame point rows at `0.45 m`;
|
||||
4. preserves an exact camera owner across frames without transferring semantic
|
||||
ownership to E34;
|
||||
5. re-associates geometry-only observations one-to-one using age, centroid and
|
||||
neighbor-expanded voxel overlap;
|
||||
6. publishes current hit-backed components as `current/occupied`;
|
||||
7. publishes missing but not expired prior evidence as `held/unknown`;
|
||||
8. emits an `expired/unknown` tombstone on the independent layer deadline and
|
||||
removes the cells from active occupancy;
|
||||
9. checks a global map jump only when at least four components support one
|
||||
coherent translation; scalar nearest-neighbor distance alone is
|
||||
insufficient;
|
||||
10. records compact cell rows, complete per-frame state, track-history tails,
|
||||
expiration events, review samples, metrics, acceptance and artifact
|
||||
digests.
|
||||
|
||||
Bounds fail closed. Components and cells are never silently truncated or
|
||||
evicted. Unknown camera-only/conflict/held observations without current E32
|
||||
points cannot create occupied cells.
|
||||
|
||||
## Failed first run and architecture corrections
|
||||
|
||||
The first immutable result
|
||||
`e34-temporal-occupied-1bab293c1867100e172923b189f8273c47eacca3dd179fd54573200906affd7c`
|
||||
was rejected by two predeclared gates:
|
||||
|
||||
- seven scalar nearest-neighbor map-jump candidates;
|
||||
- maximum frame-driven expiry delay `0.283 s`, above the `0.25 s` gate.
|
||||
|
||||
The result was not overwritten and the profile was not relaxed.
|
||||
|
||||
Inspection showed that the jump implementation did not yet implement the
|
||||
word “global”: the flagged component displacement directions were
|
||||
incoherent, and frames 1762/3190 contained an exact camera anchor moving only
|
||||
`0.18–0.24 m`. The implementation was corrected to require a single coherent
|
||||
translation with residual no larger than the already fixed `0.45 m` voxel.
|
||||
|
||||
The expiry failure exposed a real source gap: frame 1732 at
|
||||
`208.513857292 s` is followed by frame 1733 at `208.846857292 s`. A
|
||||
frame-driven state machine cannot emit a `0.75 s` TTL event during that
|
||||
`0.333 s` gap. Expiration was therefore moved to an independent logical layer
|
||||
deadline. The exact event time is retained separately from the later replay
|
||||
materialization time. This is an architecture correction, not a larger TTL.
|
||||
|
||||
## Accepted result
|
||||
|
||||
Result:
|
||||
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
|
||||
|
||||
| Measure | Result | Gate |
|
||||
| --- | ---: | ---: |
|
||||
| processed frames | 4,489 / 4,489 | complete |
|
||||
| E32 point rows consumed | 2,119,302 / 2,119,302 | exact |
|
||||
| peak active components | 37 | ≤ 256 |
|
||||
| peak cells/component | 324 | ≤ 4,096 |
|
||||
| current / held cell rows | 834,478 / 531,581 | hit-backed / stale-unknown |
|
||||
| free cell rows | 0 | 0 |
|
||||
| component continuity | 82.15% | descriptive, not GT |
|
||||
| geometry-only re-association | 81.78% | descriptive, not GT |
|
||||
| components with geometry re-association | 2,366 | descriptive |
|
||||
| maximum observed component span | 13.394 s | descriptive |
|
||||
| maximum held age | 0.750 s | ≤ 0.750 s |
|
||||
| logical expiry delay | 0.000 s | ≤ 0.250 s |
|
||||
| maximum replay materialization delay | 0.283 s | retained source-gap evidence |
|
||||
| coherent global map-jump candidates | 0 | 0 |
|
||||
| frame processing p95 | 1.483 ms | diagnostic runtime |
|
||||
|
||||
The layer created 4,912 temporal components: 1,027 camera-provenance and 3,885
|
||||
geometry-only. It recorded 5,168 exact camera associations, 17,433 spatial
|
||||
geometry re-associations, 39,664 held publications and 4,895 expiration
|
||||
events. The peak active state remained 37 components.
|
||||
|
||||
All predeclared acceptance requirements pass. Raw/map evidence, E32/E33 and
|
||||
persistent reconstruction retain their exact artifact identities.
|
||||
|
||||
## Product materialization
|
||||
|
||||
The accepted result is exposed through the path-free read-only endpoint
|
||||
`GET /api/v1/laboratory/e34/results?limit=1` and through a separate
|
||||
`LAB E34 · temporal occupied/unknown` entry in the laboratory contour.
|
||||
|
||||
The UI uses the fixed `missioncore.laboratory-report/v1` anatomy:
|
||||
|
||||
- compact human-readable task, method, principal result and limitation;
|
||||
- a domain 3D viewer with the admitted 3D/plan and fullscreen controls;
|
||||
- 27 bounded review checkpoints for current, held and expired states;
|
||||
- explicit current/held/expired counts, source availability and session time;
|
||||
- a fixed result block with proved, not proved and decision statements.
|
||||
|
||||
The central laboratory workspace did not gain another experiment-specific
|
||||
layout branch. E31–E34 discovery and rendering are isolated in the advanced
|
||||
laboratory feature registry; the E34 visualizer is a domain renderer inside
|
||||
the existing laboratory evidence surface. No new generic Design Guideline
|
||||
component or visual language was introduced.
|
||||
|
||||
## Interpretation and limitations
|
||||
|
||||
E34 proves:
|
||||
|
||||
- bounded current/held/expired occupied/unknown state for this source;
|
||||
- lossless consumption of accepted E32 point rows;
|
||||
- deterministic geometry-only temporal identity;
|
||||
- independent TTL expiry;
|
||||
- no hidden free-space claim or persistent-map write;
|
||||
- enough compact evidence to inspect history and expiration.
|
||||
|
||||
E34 does not prove:
|
||||
|
||||
- detector accuracy or human ground truth;
|
||||
- a dynamic/static class;
|
||||
- ray-cleared free space, traversability, TSDF or ESDF;
|
||||
- second-source generalization;
|
||||
- correct behavior under injected source loss, delay, drops or offset;
|
||||
- planner, navigation, safety or command authority.
|
||||
|
||||
The `0.283 s` replay materialization gap is deliberately retained. E35 must
|
||||
exercise source-independent deadlines and recovery under deterministic loss,
|
||||
staleness, delay, bounded drops and timing offsets.
|
||||
|
||||
## Decision and next stage
|
||||
|
||||
E34 is accepted as a source-scoped diagnostic/shadow temporal occupied/unknown
|
||||
layer. A8 remains open only for E35.
|
||||
|
||||
The next critical-path work is E35 deterministic degradation and recovery
|
||||
using immutable derived replay variants. Missing evidence must become
|
||||
camera-only, geometry-only, stale or unknown; it must never become guessed
|
||||
class, false free space or hidden success.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": "missioncore.e34-temporal-occupied-profile/v1",
|
||||
"profile_id": "e34-short-ttl-hit-only-occupied-unknown/v1",
|
||||
"expected_e32_result_id": "e32-track-geometry-a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd",
|
||||
"expected_e33_result_id": "e33-worker-shadow-05cc0bb264410fd49536df90e94067ac39731aff0322a8873700d40008a8bb3a",
|
||||
"layer": {
|
||||
"coordinate_frame": "map",
|
||||
"voxel_size_m": 0.45,
|
||||
"occupied_ttl_seconds": 0.75,
|
||||
"maximum_active_components": 256,
|
||||
"maximum_cells_per_component": 4096
|
||||
},
|
||||
"association": {
|
||||
"geometry_maximum_gap_seconds": 0.35,
|
||||
"geometry_maximum_centroid_distance_m": 0.9,
|
||||
"geometry_minimum_voxel_overlap_fraction": 0.05,
|
||||
"geometry_neighbor_radius_cells": 1
|
||||
},
|
||||
"map_frame_jump": {
|
||||
"maximum_adjacent_gap_seconds": 0.25,
|
||||
"minimum_matched_components": 4,
|
||||
"minimum_median_displacement_m": 1.5,
|
||||
"minimum_p25_displacement_m": 0.9
|
||||
},
|
||||
"acceptance": {
|
||||
"maximum_expiry_delay_seconds": 0.25,
|
||||
"maximum_map_frame_jump_candidates": 0,
|
||||
"require_complete_frame_accounting": true,
|
||||
"require_exact_upstream_artifact_identity": true,
|
||||
"require_no_free_space_publication": true,
|
||||
"require_bounded_state": true
|
||||
},
|
||||
"policy": {
|
||||
"hit_only_occupied": true,
|
||||
"absence_of_points_means_free": false,
|
||||
"expired_components_remain_occupied": false,
|
||||
"dynamic_class_available": false,
|
||||
"free_space_available": false,
|
||||
"persistent_reconstruction_mutation_allowed": false
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": false,
|
||||
"navigation_or_safety_accepted": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the immutable E34 short-TTL occupied/unknown temporal layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e34_temporal_occupied_replay import (
|
||||
build_e34_temporal_occupied_replay,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--e32-result", type=Path, required=True)
|
||||
parser.add_argument("--e33-result", type=Path, required=True)
|
||||
parser.add_argument("--profile", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e34_temporal_occupied_replay(
|
||||
e32_result_root=args.e32_result,
|
||||
e33_result_root=args.e33_result,
|
||||
profile_path=args.profile,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"accepted": result.accepted,
|
||||
"metrics": result.report["metrics"],
|
||||
"rejection_reasons": result.report["acceptance"][
|
||||
"rejection_reasons"
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0 if result.accepted else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,11 @@ from k1link.compute.e33_worker_shadow import (
|
||||
E33WorkerShadowResult,
|
||||
read_e33_worker_shadow_result,
|
||||
)
|
||||
from k1link.compute.e34_temporal_occupied_replay import (
|
||||
E34TemporalOccupiedReplay,
|
||||
E34TemporalOccupiedReplayError,
|
||||
read_e34_temporal_occupied_replay,
|
||||
)
|
||||
|
||||
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
|
||||
"missioncore.laboratory-advanced-catalog/v1"
|
||||
@@ -32,6 +37,7 @@ LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
|
||||
_E31_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$")
|
||||
_E32_RESULT_ID = re.compile(r"^e32-track-geometry-[a-f0-9]{64}$")
|
||||
_E33_RESULT_ID = re.compile(r"^e33-worker-shadow-[a-f0-9]{64}$")
|
||||
_E34_RESULT_ID = re.compile(r"^e34-temporal-occupied-[a-f0-9]{64}$")
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
@@ -73,6 +79,15 @@ def _read_e33_cached(
|
||||
return read_e33_worker_shadow_result(Path(root_text))
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _read_e34_cached(
|
||||
root_text: str,
|
||||
signature: tuple[int, ...],
|
||||
) -> E34TemporalOccupiedReplay:
|
||||
del signature
|
||||
return read_e34_temporal_occupied_replay(Path(root_text))
|
||||
|
||||
|
||||
def _configured_root(provider: RootProvider) -> Path | None:
|
||||
value = provider()
|
||||
if value is None:
|
||||
@@ -294,6 +309,113 @@ def _project_e33(
|
||||
}
|
||||
|
||||
|
||||
def _project_e34(result: E34TemporalOccupiedReplay) -> dict[str, object]:
|
||||
identity = _object(result.manifest.get("identity"), "E34 identity")
|
||||
profile = _object(identity.get("profile"), "E34 profile")
|
||||
layer = _object(profile.get("layer"), "E34 layer")
|
||||
association = _object(profile.get("association"), "E34 association")
|
||||
metrics = _object(result.report.get("metrics"), "E34 metrics")
|
||||
frames = _object(metrics.get("frames"), "E34 frames")
|
||||
components = _object(metrics.get("components"), "E34 components")
|
||||
occupancy = _object(metrics.get("occupancy"), "E34 occupancy")
|
||||
aging = _object(metrics.get("aging"), "E34 aging")
|
||||
map_frame = _object(metrics.get("map_frame"), "E34 map frame")
|
||||
runtime = _object(metrics.get("runtime"), "E34 runtime")
|
||||
frame_processing = _object(
|
||||
runtime.get("frame_processing_ms"),
|
||||
"E34 frame processing",
|
||||
)
|
||||
acceptance = _object(result.report.get("acceptance"), "E34 acceptance")
|
||||
if acceptance.get("accepted") is not True:
|
||||
raise ValueError("E34 result is not accepted")
|
||||
return {
|
||||
"result_id": result.result_id,
|
||||
"created_at_utc": result.manifest.get("created_at_utc"),
|
||||
"source_session_id": identity.get("source_session_id"),
|
||||
"status": result.manifest.get("acceptance_state"),
|
||||
"e32_result_id": identity.get("e32_result_id"),
|
||||
"e33_result_id": identity.get("e33_result_id"),
|
||||
"profile_id": profile.get("profile_id"),
|
||||
"pipeline_id": identity.get("pipeline"),
|
||||
"coordinate_frame": identity.get("coordinate_frame"),
|
||||
"configuration": {
|
||||
"voxel_size_m": layer.get("voxel_size_m"),
|
||||
"occupied_ttl_seconds": layer.get("occupied_ttl_seconds"),
|
||||
"maximum_active_components": layer.get(
|
||||
"maximum_active_components"
|
||||
),
|
||||
"maximum_cells_per_component": layer.get(
|
||||
"maximum_cells_per_component"
|
||||
),
|
||||
"geometry_maximum_gap_seconds": association.get(
|
||||
"geometry_maximum_gap_seconds"
|
||||
),
|
||||
"geometry_maximum_centroid_distance_m": association.get(
|
||||
"geometry_maximum_centroid_distance_m"
|
||||
),
|
||||
},
|
||||
"metrics": {
|
||||
"source_frames": frames.get("source"),
|
||||
"processed_frames": frames.get("processed"),
|
||||
"frames_with_current_layer": frames.get("with_current_layer"),
|
||||
"held_only_frames": frames.get("held_only"),
|
||||
"unknown_empty_frames": frames.get("unknown_empty"),
|
||||
"created_components": components.get("created"),
|
||||
"camera_created_components": components.get("camera_created"),
|
||||
"geometry_created_components": components.get("geometry_created"),
|
||||
"geometry_reassociated_components": components.get(
|
||||
"geometry_reassociated_components"
|
||||
),
|
||||
"exact_camera_associations": components.get(
|
||||
"exact_camera_associations"
|
||||
),
|
||||
"geometry_spatial_reassociations": components.get(
|
||||
"geometry_spatial_reassociations"
|
||||
),
|
||||
"held_publications": components.get("held_publications"),
|
||||
"expired_components": components.get("expired"),
|
||||
"peak_active_components": components.get("peak_active"),
|
||||
"maximum_observed_span_seconds": components.get(
|
||||
"maximum_observed_span_seconds"
|
||||
),
|
||||
"continuity_fraction": components.get("continuity_fraction"),
|
||||
"geometry_only_reassociation_fraction": components.get(
|
||||
"geometry_only_reassociation_fraction"
|
||||
),
|
||||
"e32_current_point_rows": occupancy.get(
|
||||
"e32_current_point_rows"
|
||||
),
|
||||
"e34_consumed_current_point_rows": occupancy.get(
|
||||
"e34_consumed_current_point_rows"
|
||||
),
|
||||
"current_cell_rows": occupancy.get("current_cell_rows"),
|
||||
"held_cell_rows": occupancy.get("held_cell_rows"),
|
||||
"stored_cell_rows": occupancy.get("stored_cell_rows"),
|
||||
"free_cell_rows": occupancy.get("free_cell_rows"),
|
||||
"peak_cells_per_component": occupancy.get(
|
||||
"peak_cells_per_component"
|
||||
),
|
||||
"maximum_held_age_seconds": aging.get(
|
||||
"maximum_held_age_seconds"
|
||||
),
|
||||
"maximum_expiry_delay_seconds": aging.get(
|
||||
"maximum_expiry_delay_seconds"
|
||||
),
|
||||
"maximum_replay_materialization_delay_seconds": aging.get(
|
||||
"maximum_replay_materialization_delay_seconds"
|
||||
),
|
||||
"map_frame_jump_candidates": map_frame.get("jump_candidates"),
|
||||
"frame_processing_p95_ms": frame_processing.get("p95"),
|
||||
"build_elapsed_ms": runtime.get("build_elapsed_ms"),
|
||||
},
|
||||
"review": copy.deepcopy(result.review),
|
||||
"acceptance": copy.deepcopy(acceptance),
|
||||
"decision": copy.deepcopy(result.report.get("decision")),
|
||||
"authority": copy.deepcopy(result.report.get("authority")),
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
def _empty_catalog(configured: bool) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": LABORATORY_ADVANCED_CATALOG_SCHEMA,
|
||||
@@ -310,6 +432,7 @@ def build_advanced_laboratory_router(
|
||||
e31_root_provider: RootProvider = lambda: None,
|
||||
e32_root_provider: RootProvider = lambda: None,
|
||||
e33_root_provider: RootProvider = lambda: None,
|
||||
e34_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@@ -434,4 +557,39 @@ def build_advanced_laboratory_router(
|
||||
"invalid_total": invalid_total,
|
||||
}
|
||||
|
||||
@router.get("/e34/results")
|
||||
def list_e34_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
root = _configured_root(e34_root_provider)
|
||||
if root is None:
|
||||
return _empty_catalog(False)
|
||||
candidates = _candidates(root, _E34_RESULT_ID)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in candidates:
|
||||
try:
|
||||
result = _read_e34_cached(
|
||||
str(candidate.resolve()),
|
||||
_result_signature(candidate),
|
||||
)
|
||||
if not result.accepted:
|
||||
raise ValueError("E34 result is not accepted")
|
||||
if len(items) < limit:
|
||||
items.append(_project_e34(result))
|
||||
except (
|
||||
E34TemporalOccupiedReplayError,
|
||||
KeyError,
|
||||
OSError,
|
||||
TypeError,
|
||||
ValueError,
|
||||
):
|
||||
invalid_total += 1
|
||||
return {
|
||||
**_empty_catalog(True),
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
@@ -520,6 +520,13 @@ app.include_router(
|
||||
/ "e33"
|
||||
/ "results"
|
||||
),
|
||||
e34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e34"
|
||||
/ "results"
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.routing import APIRoute
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
import k1link.web.advanced_laboratory_api as advanced_api
|
||||
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
||||
|
||||
|
||||
@@ -23,7 +26,7 @@ def _endpoint(router: APIRouter, path: str) -> object:
|
||||
def test_advanced_catalogs_are_empty_when_not_configured() -> None:
|
||||
router = build_advanced_laboratory_router()
|
||||
|
||||
for name in ("e31", "e32", "e33"):
|
||||
for name in ("e31", "e32", "e33", "e34"):
|
||||
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
|
||||
catalog = route(limit=1) # type: ignore[operator]
|
||||
assert catalog == {
|
||||
@@ -42,18 +45,21 @@ def test_advanced_catalogs_fail_closed_on_incomplete_results(
|
||||
e31 = tmp_path / "e31"
|
||||
e32 = tmp_path / "e32"
|
||||
e33 = tmp_path / "e33"
|
||||
for root in (e31, e32, e33):
|
||||
e34 = tmp_path / "e34"
|
||||
for root in (e31, e32, e33, e34):
|
||||
root.mkdir()
|
||||
(e31 / f"e31-source-qualification-{'1' * 64}").mkdir()
|
||||
(e32 / f"e32-track-geometry-{'2' * 64}").mkdir()
|
||||
(e33 / f"e33-worker-shadow-{'3' * 64}").mkdir()
|
||||
(e34 / f"e34-temporal-occupied-{'4' * 64}").mkdir()
|
||||
router = build_advanced_laboratory_router(
|
||||
e31_root_provider=lambda: e31,
|
||||
e32_root_provider=lambda: e32,
|
||||
e33_root_provider=lambda: e33,
|
||||
e34_root_provider=lambda: e34,
|
||||
)
|
||||
|
||||
for name in ("e31", "e32", "e33"):
|
||||
for name in ("e31", "e32", "e33", "e34"):
|
||||
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
|
||||
catalog = route(limit=1) # type: ignore[operator]
|
||||
assert catalog["configured"] is True
|
||||
@@ -77,3 +83,129 @@ def test_advanced_catalog_does_not_follow_a_configured_symlink(
|
||||
catalog = route(limit=1) # type: ignore[operator]
|
||||
|
||||
assert catalog["configured"] is False
|
||||
|
||||
|
||||
def test_e34_catalog_projects_only_accepted_read_only_evidence(
|
||||
tmp_path: Path,
|
||||
monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
result_id = f"e34-temporal-occupied-{'4' * 64}"
|
||||
root = tmp_path / "e34"
|
||||
candidate = root / result_id
|
||||
candidate.mkdir(parents=True)
|
||||
(candidate / "manifest.json").write_text("{}", encoding="utf-8")
|
||||
authority = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
result = SimpleNamespace(
|
||||
result_id=result_id,
|
||||
accepted=True,
|
||||
manifest={
|
||||
"created_at_utc": "2026-07-27T13:28:30Z",
|
||||
"acceptance_state": (
|
||||
"accepted-bounded-occupied-unknown-temporal-layer"
|
||||
),
|
||||
"identity": {
|
||||
"source_session_id": "source-session",
|
||||
"e32_result_id": f"e32-track-geometry-{'2' * 64}",
|
||||
"e33_result_id": f"e33-worker-shadow-{'3' * 64}",
|
||||
"pipeline": "lidar-local-map/short-ttl-hit-only/v1",
|
||||
"coordinate_frame": "map",
|
||||
"profile": {
|
||||
"profile_id": "e34-profile/v1",
|
||||
"layer": {
|
||||
"voxel_size_m": 0.45,
|
||||
"occupied_ttl_seconds": 0.75,
|
||||
"maximum_active_components": 256,
|
||||
"maximum_cells_per_component": 4096,
|
||||
},
|
||||
"association": {
|
||||
"geometry_maximum_gap_seconds": 0.35,
|
||||
"geometry_maximum_centroid_distance_m": 0.9,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
report={
|
||||
"metrics": {
|
||||
"frames": {
|
||||
"source": 4489,
|
||||
"processed": 4489,
|
||||
"with_current_layer": 3928,
|
||||
"held_only": 540,
|
||||
"unknown_empty": 21,
|
||||
},
|
||||
"components": {
|
||||
"created": 4912,
|
||||
"camera_created": 1027,
|
||||
"geometry_created": 3885,
|
||||
"geometry_reassociated_components": 2366,
|
||||
"exact_camera_associations": 5168,
|
||||
"geometry_spatial_reassociations": 17433,
|
||||
"held_publications": 39664,
|
||||
"expired": 4895,
|
||||
"peak_active": 37,
|
||||
"maximum_observed_span_seconds": 13.394,
|
||||
"continuity_fraction": 0.821466,
|
||||
"geometry_only_reassociation_fraction": 0.81776,
|
||||
},
|
||||
"occupancy": {
|
||||
"e32_current_point_rows": 2119302,
|
||||
"e34_consumed_current_point_rows": 2119302,
|
||||
"current_cell_rows": 834478,
|
||||
"held_cell_rows": 531581,
|
||||
"stored_cell_rows": 1366059,
|
||||
"free_cell_rows": 0,
|
||||
"peak_cells_per_component": 324,
|
||||
},
|
||||
"aging": {
|
||||
"maximum_held_age_seconds": 0.75,
|
||||
"maximum_expiry_delay_seconds": 0.0,
|
||||
"maximum_replay_materialization_delay_seconds": 0.283,
|
||||
},
|
||||
"map_frame": {"jump_candidates": 0},
|
||||
"runtime": {
|
||||
"build_elapsed_ms": 4317.0,
|
||||
"frame_processing_ms": {"p95": 1.483},
|
||||
},
|
||||
},
|
||||
"acceptance": {"accepted": True, "requirements": {}},
|
||||
"decision": {"next_gate": "A8/E35"},
|
||||
"authority": authority,
|
||||
},
|
||||
review={
|
||||
"schema_version": (
|
||||
"missioncore.e34-temporal-review-timeline/v1"
|
||||
),
|
||||
"result_id": result_id,
|
||||
"frames": [],
|
||||
},
|
||||
)
|
||||
|
||||
def fake_read(
|
||||
root_text: str,
|
||||
signature: tuple[int, ...],
|
||||
) -> SimpleNamespace:
|
||||
assert root_text == str(candidate.resolve())
|
||||
assert signature
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(advanced_api, "_read_e34_cached", fake_read)
|
||||
router = build_advanced_laboratory_router(
|
||||
e34_root_provider=lambda: root,
|
||||
)
|
||||
|
||||
route = _endpoint(router, "/api/v1/laboratory/e34/results")
|
||||
catalog = route(limit=1) # type: ignore[operator]
|
||||
|
||||
assert catalog["candidate_total"] == 1
|
||||
assert catalog["invalid_total"] == 0
|
||||
item = catalog["items"][0]
|
||||
assert item["status"] == (
|
||||
"accepted-bounded-occupied-unknown-temporal-layer"
|
||||
)
|
||||
assert item["metrics"]["processed_frames"] == 4489
|
||||
assert item["metrics"]["free_cell_rows"] == 0
|
||||
assert item["authority"] == authority
|
||||
assert item["access"] == "read-only"
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.temporal_occupied_layer import (
|
||||
TemporalLayerConfig,
|
||||
TemporalOccupiedLayer,
|
||||
TemporalOccupiedLayerError,
|
||||
)
|
||||
from k1link.compute.track_geometry import (
|
||||
PointSlab,
|
||||
TrackGeometry,
|
||||
TrackGeometryCurrentness,
|
||||
TrackGeometryEvidenceState,
|
||||
TrackGeometryFrame,
|
||||
TrackGeometryMetricBasis,
|
||||
TrackGeometryOwnerKind,
|
||||
TrackGeometrySourceBinding,
|
||||
)
|
||||
|
||||
|
||||
def _config(**changes: object) -> TemporalLayerConfig:
|
||||
values: dict[str, object] = {
|
||||
"voxel_size_m": 0.45,
|
||||
"occupied_ttl_seconds": 0.75,
|
||||
"maximum_active_components": 256,
|
||||
"maximum_cells_per_component": 4096,
|
||||
"geometry_maximum_gap_seconds": 0.35,
|
||||
"geometry_maximum_centroid_distance_m": 0.9,
|
||||
"geometry_minimum_voxel_overlap_fraction": 0.05,
|
||||
"geometry_neighbor_radius_cells": 1,
|
||||
"jump_maximum_adjacent_gap_seconds": 0.25,
|
||||
"jump_minimum_matched_components": 4,
|
||||
"jump_minimum_median_displacement_m": 1.5,
|
||||
"jump_minimum_p25_displacement_m": 0.9,
|
||||
}
|
||||
values.update(changes)
|
||||
return TemporalLayerConfig(**values) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _binding() -> TrackGeometrySourceBinding:
|
||||
return TrackGeometrySourceBinding(
|
||||
source_pack_id="e10-lidar-pack-" + "a" * 64,
|
||||
source_session_id="source-session",
|
||||
representation_profile_id="representation-profile/v1",
|
||||
e31_qualification_id="e31-source-qualification-" + "b" * 64,
|
||||
calibration_sha256="c" * 64,
|
||||
coordinate_frame="map",
|
||||
time_basis="source-time",
|
||||
selected_offset_ms=0,
|
||||
)
|
||||
|
||||
|
||||
def _frame(
|
||||
*,
|
||||
frame_index: int,
|
||||
seconds: float,
|
||||
owners: list[
|
||||
tuple[
|
||||
str,
|
||||
TrackGeometryOwnerKind,
|
||||
list[list[float]],
|
||||
int | None,
|
||||
]
|
||||
],
|
||||
source_available: bool = True,
|
||||
unknown_camera_owner: str | None = None,
|
||||
) -> TrackGeometryFrame:
|
||||
owner_keys = tuple(item[0] for item in owners)
|
||||
source_indices: list[int] = []
|
||||
points: list[list[float]] = []
|
||||
owner_indices: list[int] = []
|
||||
geometries: list[TrackGeometry] = []
|
||||
for owner_index, (owner_key, owner_kind, owner_points, track_id) in enumerate(
|
||||
owners
|
||||
):
|
||||
row_start = len(points)
|
||||
points.extend(owner_points)
|
||||
source_indices.extend(range(row_start, row_start + len(owner_points)))
|
||||
owner_indices.extend([owner_index] * len(owner_points))
|
||||
geometries.append(
|
||||
TrackGeometry(
|
||||
owner_key=owner_key,
|
||||
owner_kind=owner_kind,
|
||||
evidence_state=(
|
||||
TrackGeometryEvidenceState.AGREE
|
||||
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
|
||||
else TrackGeometryEvidenceState.GEOMETRY_ONLY
|
||||
),
|
||||
currentness=TrackGeometryCurrentness.CURRENT,
|
||||
metric_basis=TrackGeometryMetricBasis.CURRENT_POINTS,
|
||||
reason_codes=("accepted-hit-backed-support",),
|
||||
semantic_track_id=track_id,
|
||||
semantic_label=(
|
||||
"object"
|
||||
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
|
||||
else None
|
||||
),
|
||||
bbox_xyxy=(
|
||||
(1.0, 1.0, 2.0, 2.0)
|
||||
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
|
||||
else None
|
||||
),
|
||||
range_m=1.0,
|
||||
)
|
||||
)
|
||||
if unknown_camera_owner is not None:
|
||||
track_id = int(unknown_camera_owner.split(":")[1])
|
||||
geometries.append(
|
||||
TrackGeometry(
|
||||
owner_key=unknown_camera_owner,
|
||||
owner_kind=TrackGeometryOwnerKind.CAMERA_TRACK,
|
||||
evidence_state=TrackGeometryEvidenceState.CAMERA_ONLY,
|
||||
currentness=TrackGeometryCurrentness.CURRENT,
|
||||
metric_basis=TrackGeometryMetricBasis.UNAVAILABLE,
|
||||
reason_codes=("camera-without-current-points",),
|
||||
semantic_track_id=track_id,
|
||||
semantic_label="object",
|
||||
bbox_xyxy=(1.0, 1.0, 2.0, 2.0),
|
||||
)
|
||||
)
|
||||
point_array = (
|
||||
np.asarray(points, dtype="<f4")
|
||||
if points
|
||||
else np.empty((0, 3), dtype="<f4")
|
||||
)
|
||||
return TrackGeometryFrame(
|
||||
binding=_binding(),
|
||||
frame_index=frame_index,
|
||||
source_frame_index=frame_index,
|
||||
session_seconds=seconds,
|
||||
source_available=source_available,
|
||||
point_slab=PointSlab(
|
||||
frame_index=frame_index,
|
||||
source_frame_index=frame_index,
|
||||
source_point_count=len(points),
|
||||
coordinate_frame="map",
|
||||
owner_keys=owner_keys,
|
||||
source_indices=np.asarray(source_indices, dtype="<i8"),
|
||||
points_xyz_m=point_array,
|
||||
owner_indices=np.asarray(owner_indices, dtype="<u4"),
|
||||
),
|
||||
geometries=tuple(geometries),
|
||||
)
|
||||
|
||||
|
||||
def test_temporal_layer_holds_unknown_then_expires_without_publishing_cells() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
current = layer.update(
|
||||
_frame(
|
||||
frame_index=0,
|
||||
seconds=0.0,
|
||||
owners=[
|
||||
(
|
||||
"track:7",
|
||||
TrackGeometryOwnerKind.CAMERA_TRACK,
|
||||
[[1.0, 2.0, 0.5], [1.1, 2.0, 0.5]],
|
||||
7,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
held = layer.update(
|
||||
_frame(
|
||||
frame_index=1,
|
||||
seconds=0.5,
|
||||
owners=[],
|
||||
source_available=False,
|
||||
)
|
||||
)
|
||||
expired = layer.update(
|
||||
_frame(
|
||||
frame_index=2,
|
||||
seconds=0.9,
|
||||
owners=[],
|
||||
source_available=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert current.document["current"][0]["occupancy_state"] == "occupied"
|
||||
assert held.document["held"][0]["occupancy_state"] == "unknown"
|
||||
assert held.document["held"][0]["cell_row_count"] > 0
|
||||
assert expired.document["expired"][0]["occupancy_state"] == "unknown"
|
||||
assert expired.document["expired"][0]["cell_row_count"] == 0
|
||||
assert expired.document["expired"][0]["expiration_event"] == {
|
||||
"deadline_seconds": 0.75,
|
||||
"emitted_at_seconds": 0.75,
|
||||
"emission_delay_seconds": 0.0,
|
||||
"replay_materialized_at_seconds": 0.9,
|
||||
"replay_materialization_delay_seconds": pytest.approx(0.15),
|
||||
"clock": "independent-layer-deadline",
|
||||
}
|
||||
assert expired.cell_rows.shape == (0, 3)
|
||||
assert layer.active_component_count == 0
|
||||
|
||||
|
||||
def test_geometry_only_components_reassociate_without_inventing_semantics() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
first = layer.update(
|
||||
_frame(
|
||||
frame_index=0,
|
||||
seconds=0.0,
|
||||
owners=[
|
||||
(
|
||||
"geometry:0",
|
||||
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
|
||||
[[1.0, 1.0, 0.5], [1.2, 1.0, 0.5]],
|
||||
None,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
second = layer.update(
|
||||
_frame(
|
||||
frame_index=1,
|
||||
seconds=0.1,
|
||||
owners=[
|
||||
(
|
||||
"geometry:9",
|
||||
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
|
||||
[[1.1, 1.0, 0.5], [1.3, 1.0, 0.5]],
|
||||
None,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert first.document["current"][0]["temporal_id"] == second.document[
|
||||
"current"
|
||||
][0]["temporal_id"]
|
||||
assert second.document["current"][0]["association_reason"] == (
|
||||
"geometry-spatial-reassociation"
|
||||
)
|
||||
assert second.document["current"][0]["semantic_provenance"] == {
|
||||
"owner": None,
|
||||
"track_ids": [],
|
||||
"labels": [],
|
||||
"class_inferred_by_e34": False,
|
||||
}
|
||||
|
||||
|
||||
def test_unknown_without_current_points_cannot_create_occupied_cells() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
projection = layer.update(
|
||||
_frame(
|
||||
frame_index=0,
|
||||
seconds=0.0,
|
||||
owners=[],
|
||||
unknown_camera_owner="track:11",
|
||||
)
|
||||
)
|
||||
|
||||
assert projection.document["layer_state"] == "unknown"
|
||||
assert projection.document["current"] == []
|
||||
assert projection.document["input"]["unknown_without_current_points"] == 1
|
||||
assert projection.cell_rows.shape == (0, 3)
|
||||
assert layer.active_component_count == 0
|
||||
|
||||
|
||||
def test_temporal_layer_detects_supported_global_map_frame_jump() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
first = [
|
||||
(
|
||||
f"track:{track_id}",
|
||||
TrackGeometryOwnerKind.CAMERA_TRACK,
|
||||
[[float(track_id), 0.0, 0.5]],
|
||||
track_id,
|
||||
)
|
||||
for track_id in range(1, 5)
|
||||
]
|
||||
shifted = [
|
||||
(
|
||||
f"track:{track_id}",
|
||||
TrackGeometryOwnerKind.CAMERA_TRACK,
|
||||
[[float(track_id) + 2.0, 0.0, 0.5]],
|
||||
track_id,
|
||||
)
|
||||
for track_id in range(1, 5)
|
||||
]
|
||||
layer.update(_frame(frame_index=0, seconds=0.0, owners=first))
|
||||
projection = layer.update(
|
||||
_frame(frame_index=1, seconds=0.1, owners=shifted)
|
||||
)
|
||||
|
||||
assert projection.document["map_frame_jump"]["candidate"] is True
|
||||
assert projection.document["map_frame_jump"][
|
||||
"matched_component_count"
|
||||
] == 4
|
||||
|
||||
|
||||
def test_temporal_layer_rejects_incoherent_nearest_neighbor_jump() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
first = [
|
||||
(
|
||||
f"geometry:{index}",
|
||||
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
|
||||
[[float(index) * 5.0, 0.0, 0.5]],
|
||||
None,
|
||||
)
|
||||
for index in range(4)
|
||||
]
|
||||
incoherent = [
|
||||
(
|
||||
f"geometry:{index}",
|
||||
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
|
||||
[[x, y, 0.5]],
|
||||
None,
|
||||
)
|
||||
for index, (x, y) in enumerate(
|
||||
((2.0, 0.0), (8.0, 2.0), (13.0, -3.0), (25.0, 4.0))
|
||||
)
|
||||
]
|
||||
layer.update(_frame(frame_index=0, seconds=0.0, owners=first))
|
||||
projection = layer.update(
|
||||
_frame(frame_index=1, seconds=0.1, owners=incoherent)
|
||||
)
|
||||
|
||||
assert projection.document["map_frame_jump"]["candidate"] is False
|
||||
assert projection.document["map_frame_jump"]["reason"] == (
|
||||
"no-coherent-global-translation"
|
||||
)
|
||||
|
||||
|
||||
def test_temporal_layer_fails_closed_instead_of_truncating_cells() -> None:
|
||||
layer = TemporalOccupiedLayer(_config(maximum_cells_per_component=1))
|
||||
|
||||
with pytest.raises(
|
||||
TemporalOccupiedLayerError,
|
||||
match="cell bound exceeded",
|
||||
):
|
||||
layer.update(
|
||||
_frame(
|
||||
frame_index=0,
|
||||
seconds=0.0,
|
||||
owners=[
|
||||
(
|
||||
"geometry:0",
|
||||
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
|
||||
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
|
||||
None,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_temporal_layer_rejects_non_monotonic_replay() -> None:
|
||||
layer = TemporalOccupiedLayer(_config())
|
||||
layer.update(_frame(frame_index=0, seconds=0.0, owners=[]))
|
||||
|
||||
with pytest.raises(
|
||||
TemporalOccupiedLayerError,
|
||||
match="frame order changed",
|
||||
):
|
||||
layer.update(_frame(frame_index=2, seconds=0.1, owners=[]))
|
||||
Reference in New Issue
Block a user