|
|
|
@@ -0,0 +1,327 @@
|
|
|
|
|
import type { LaboratoryFetch } from "./advancedResults";
|
|
|
|
|
|
|
|
|
|
const RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
|
|
|
|
|
|
|
|
|
|
export type M49TgsFullShadowStateCode = 0 | 1 | 2 | 3;
|
|
|
|
|
|
|
|
|
|
export interface M49TgsFullShadowResult {
|
|
|
|
|
resultId: string;
|
|
|
|
|
createdAtUtc: string;
|
|
|
|
|
source: {
|
|
|
|
|
sourcePackSha256: string;
|
|
|
|
|
linkedVisualResultId: string;
|
|
|
|
|
};
|
|
|
|
|
configuration: {
|
|
|
|
|
configSha256: string;
|
|
|
|
|
cellSizeM: number;
|
|
|
|
|
radiusM: number;
|
|
|
|
|
historySeconds: number;
|
|
|
|
|
};
|
|
|
|
|
execution: {
|
|
|
|
|
wrapperElapsedSeconds: number;
|
|
|
|
|
};
|
|
|
|
|
timeline: {
|
|
|
|
|
frameCount: 4489;
|
|
|
|
|
availableLidarFrameCount: 3928;
|
|
|
|
|
missingLidarFrameCount: 561;
|
|
|
|
|
durationSeconds: number;
|
|
|
|
|
effectiveFps: number;
|
|
|
|
|
};
|
|
|
|
|
pointAccounting: {
|
|
|
|
|
eligible: number;
|
|
|
|
|
ground: number;
|
|
|
|
|
nonground: number;
|
|
|
|
|
rejected: number;
|
|
|
|
|
unaccounted: 0;
|
|
|
|
|
};
|
|
|
|
|
performance: {
|
|
|
|
|
candidateTgsMs: { p50: number; p95: number; p99: number; max: number };
|
|
|
|
|
completionAgeMs: { p50: number; p95: number; p99: number; max: number };
|
|
|
|
|
capacityDropCount: number;
|
|
|
|
|
};
|
|
|
|
|
acceptance: {
|
|
|
|
|
representationComplete: true;
|
|
|
|
|
visualQualityAccepted: false;
|
|
|
|
|
integratedGraphPerformanceAccepted: false;
|
|
|
|
|
allFramesAccounted: boolean;
|
|
|
|
|
allEligiblePointsAccounted: boolean;
|
|
|
|
|
};
|
|
|
|
|
decision: {
|
|
|
|
|
state: string;
|
|
|
|
|
candidateRetained: boolean;
|
|
|
|
|
nextAction: string;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface M49TgsFullShadowSpatial {
|
|
|
|
|
resultId: string;
|
|
|
|
|
sourceSequence: number;
|
|
|
|
|
sourceFrameIndex: number;
|
|
|
|
|
sessionSeconds: number;
|
|
|
|
|
sampleAvailable: boolean;
|
|
|
|
|
costmap: {
|
|
|
|
|
cellSizeM: number;
|
|
|
|
|
radiusM: number;
|
|
|
|
|
centersXyM: readonly (readonly [number, number])[];
|
|
|
|
|
states: readonly M49TgsFullShadowStateCode[];
|
|
|
|
|
zBoundsM: readonly (readonly [number | null, number | null])[];
|
|
|
|
|
};
|
|
|
|
|
metrics: {
|
|
|
|
|
eligiblePointCount: number;
|
|
|
|
|
groundPointCount: number;
|
|
|
|
|
nongroundPointCount: number;
|
|
|
|
|
rejectedPointCount: number;
|
|
|
|
|
occupiedCellCount: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface M49TgsFullShadowSpatialChunk {
|
|
|
|
|
resultId: string;
|
|
|
|
|
start: number;
|
|
|
|
|
count: number;
|
|
|
|
|
frames: readonly M49TgsFullShadowSpatial[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class M49TgsFullShadowContractError extends Error {}
|
|
|
|
|
|
|
|
|
|
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
|
|
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: ожидался объект.`);
|
|
|
|
|
}
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function text(value: unknown, label: string): string {
|
|
|
|
|
if (typeof value !== "string" || !value.trim()) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: ожидалась строка.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function numberValue(value: unknown, label: string): number {
|
|
|
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: ожидалось число.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function integer(value: unknown, label: string): number {
|
|
|
|
|
const parsed = numberValue(value, label);
|
|
|
|
|
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: ожидалось целое число.`);
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function booleanValue(value: unknown, label: string): boolean {
|
|
|
|
|
if (typeof value !== "boolean") {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: ожидался boolean.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exact<T extends string | boolean | number>(value: unknown, expected: T, label: string): T {
|
|
|
|
|
if (value !== expected) throw new M49TgsFullShadowContractError(`${label}: нарушен контракт.`);
|
|
|
|
|
return expected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function timing(value: unknown, label: string) {
|
|
|
|
|
const row = objectValue(value, label);
|
|
|
|
|
return {
|
|
|
|
|
p50: numberValue(row.p50, `${label}.p50`),
|
|
|
|
|
p95: numberValue(row.p95, `${label}.p95`),
|
|
|
|
|
p99: numberValue(row.p99, `${label}.p99`),
|
|
|
|
|
max: numberValue(row.max, `${label}.max`),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchM49TgsFullShadowResult(
|
|
|
|
|
id: string,
|
|
|
|
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
|
|
|
|
): Promise<M49TgsFullShadowResult> {
|
|
|
|
|
if (!RESULT_ID.test(id)) throw new M49TgsFullShadowContractError("M49 full-shadow identity недопустима.");
|
|
|
|
|
const response = await fetcher(
|
|
|
|
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}`,
|
|
|
|
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
|
|
|
|
);
|
|
|
|
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full shadow недоступен: HTTP ${response.status}.`);
|
|
|
|
|
const payload = objectValue(await response.json(), "M49 full shadow");
|
|
|
|
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-view/v1", "M49 full schema");
|
|
|
|
|
exact(payload.result_id, id, "M49 full result");
|
|
|
|
|
exact(payload.ground_truth, false, "M49 full ground truth");
|
|
|
|
|
exact(payload.access, "read-only", "M49 full access");
|
|
|
|
|
const source = objectValue(payload.source, "M49 full source");
|
|
|
|
|
const configuration = objectValue(payload.configuration, "M49 full configuration");
|
|
|
|
|
const execution = objectValue(payload.execution, "M49 full execution");
|
|
|
|
|
const timeline = objectValue(payload.timeline, "M49 full timeline");
|
|
|
|
|
const accounting = objectValue(payload.point_accounting, "M49 full accounting");
|
|
|
|
|
const performance = objectValue(payload.performance, "M49 full performance");
|
|
|
|
|
const acceptance = objectValue(payload.acceptance, "M49 full acceptance");
|
|
|
|
|
const decision = objectValue(payload.decision, "M49 full decision");
|
|
|
|
|
return {
|
|
|
|
|
resultId: text(payload.result_id, "M49 full result"),
|
|
|
|
|
createdAtUtc: text(payload.created_at_utc, "M49 full created"),
|
|
|
|
|
source: {
|
|
|
|
|
sourcePackSha256: text(source.source_pack_sha256, "M49 full source pack"),
|
|
|
|
|
linkedVisualResultId: text(source.linked_visual_result_id, "M49 full visual result"),
|
|
|
|
|
},
|
|
|
|
|
configuration: {
|
|
|
|
|
configSha256: text(configuration.config_sha256, "M49 full config"),
|
|
|
|
|
cellSizeM: numberValue(configuration.cell_size_m, "M49 full cell size"),
|
|
|
|
|
radiusM: numberValue(configuration.radius_m, "M49 full radius"),
|
|
|
|
|
historySeconds: numberValue(configuration.history_seconds, "M49 full history"),
|
|
|
|
|
},
|
|
|
|
|
execution: { wrapperElapsedSeconds: numberValue(execution.wrapper_elapsed_seconds, "M49 full wall") },
|
|
|
|
|
timeline: {
|
|
|
|
|
frameCount: exact(integer(timeline.frame_count, "M49 full frames"), 4489, "M49 full frames"),
|
|
|
|
|
availableLidarFrameCount: exact(integer(timeline.available_lidar_frame_count, "M49 full available"), 3928, "M49 full available"),
|
|
|
|
|
missingLidarFrameCount: exact(integer(timeline.missing_lidar_frame_count, "M49 full missing"), 561, "M49 full missing"),
|
|
|
|
|
durationSeconds: numberValue(timeline.duration_seconds, "M49 full duration"),
|
|
|
|
|
effectiveFps: numberValue(timeline.effective_fps, "M49 full fps"),
|
|
|
|
|
},
|
|
|
|
|
pointAccounting: {
|
|
|
|
|
eligible: integer(accounting.eligible, "M49 full eligible"),
|
|
|
|
|
ground: integer(accounting.ground, "M49 full ground"),
|
|
|
|
|
nonground: integer(accounting.nonground, "M49 full nonground"),
|
|
|
|
|
rejected: integer(accounting.rejected, "M49 full rejected"),
|
|
|
|
|
unaccounted: exact(integer(accounting.unaccounted, "M49 full unaccounted"), 0, "M49 full unaccounted"),
|
|
|
|
|
},
|
|
|
|
|
performance: {
|
|
|
|
|
candidateTgsMs: timing(performance.candidate_tgs_ms, "M49 full TGS"),
|
|
|
|
|
completionAgeMs: timing(performance.completion_age_ms, "M49 full completion"),
|
|
|
|
|
capacityDropCount: integer(performance.capacity_drop_count, "M49 full drops"),
|
|
|
|
|
},
|
|
|
|
|
acceptance: {
|
|
|
|
|
representationComplete: exact(acceptance.representation_complete, true, "M49 full representation"),
|
|
|
|
|
visualQualityAccepted: exact(acceptance.visual_quality_accepted, false, "M49 full visual"),
|
|
|
|
|
integratedGraphPerformanceAccepted: exact(acceptance.integrated_graph_performance_accepted, false, "M49 full integrated"),
|
|
|
|
|
allFramesAccounted: booleanValue(acceptance.all_frames_accounted, "M49 full frame accounting"),
|
|
|
|
|
allEligiblePointsAccounted: booleanValue(acceptance.all_eligible_points_accounted, "M49 full point accounting"),
|
|
|
|
|
},
|
|
|
|
|
decision: {
|
|
|
|
|
state: text(decision.state, "M49 full decision"),
|
|
|
|
|
candidateRetained: booleanValue(decision.candidate_retained, "M49 full retained"),
|
|
|
|
|
nextAction: text(decision.next_action, "M49 full next action"),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pair(value: unknown, label: string): readonly [number, number] {
|
|
|
|
|
if (!Array.isArray(value) || value.length !== 2) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`${label}: неверная размерность.`);
|
|
|
|
|
}
|
|
|
|
|
return [numberValue(value[0], label), numberValue(value[1], label)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseSpatial(
|
|
|
|
|
id: string,
|
|
|
|
|
sourceSequence: number,
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
sharedCostmap?: Record<string, unknown>,
|
|
|
|
|
): M49TgsFullShadowSpatial {
|
|
|
|
|
exact(payload.source_sequence, sourceSequence, "M49 full spatial sequence");
|
|
|
|
|
const costmap = sharedCostmap ?? objectValue(payload.costmap, "M49 full costmap");
|
|
|
|
|
const metrics = objectValue(payload.metrics, "M49 full frame metrics");
|
|
|
|
|
const centers = costmap.centers_xy_m;
|
|
|
|
|
const statesRaw = sharedCostmap ? payload.states : costmap.states;
|
|
|
|
|
const zBoundsRaw = sharedCostmap ? payload.z_bounds_m : costmap.z_bounds_m;
|
|
|
|
|
if (!Array.isArray(centers) || !Array.isArray(statesRaw) || !Array.isArray(zBoundsRaw)
|
|
|
|
|
|| centers.length !== 2244 || statesRaw.length !== 2244 || zBoundsRaw.length !== 2244) {
|
|
|
|
|
throw new M49TgsFullShadowContractError("M49 full costmap: неверная размерность.");
|
|
|
|
|
}
|
|
|
|
|
const states = statesRaw.map((value, index) => {
|
|
|
|
|
const parsed = integer(value, `M49 full state ${index}`);
|
|
|
|
|
if (parsed > 3) throw new M49TgsFullShadowContractError("M49 full state недопустим.");
|
|
|
|
|
return parsed as M49TgsFullShadowStateCode;
|
|
|
|
|
});
|
|
|
|
|
const zBounds = zBoundsRaw.map((value, index) => {
|
|
|
|
|
if (!Array.isArray(value) || value.length !== 2) {
|
|
|
|
|
throw new M49TgsFullShadowContractError(`M49 full z ${index}: неверная размерность.`);
|
|
|
|
|
}
|
|
|
|
|
return value.map((item) => item === null
|
|
|
|
|
? null
|
|
|
|
|
: numberValue(item, `M49 full z ${index}`)) as [number | null, number | null];
|
|
|
|
|
});
|
|
|
|
|
const sampleAvailable = booleanValue(payload.sample_available, "M49 full sample available");
|
|
|
|
|
if (!sampleAvailable && states.some((state) => state !== 0)) {
|
|
|
|
|
throw new M49TgsFullShadowContractError("M49 missing LiDAR frame не остался UNOBSERVED.");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
resultId: id,
|
|
|
|
|
sourceSequence,
|
|
|
|
|
sourceFrameIndex: integer(payload.source_frame_index, "M49 full source frame"),
|
|
|
|
|
sessionSeconds: numberValue(payload.session_seconds, "M49 full session seconds"),
|
|
|
|
|
sampleAvailable,
|
|
|
|
|
costmap: {
|
|
|
|
|
cellSizeM: numberValue(costmap.cell_size_m, "M49 full cell size"),
|
|
|
|
|
radiusM: numberValue(costmap.radius_m, "M49 full radius"),
|
|
|
|
|
centersXyM: centers.map((value, index) => pair(value, `M49 full center ${index}`)),
|
|
|
|
|
states,
|
|
|
|
|
zBoundsM: zBounds,
|
|
|
|
|
},
|
|
|
|
|
metrics: {
|
|
|
|
|
eligiblePointCount: integer(metrics.eligible_point_count, "M49 full eligible"),
|
|
|
|
|
groundPointCount: integer(metrics.ground_point_count, "M49 full ground"),
|
|
|
|
|
nongroundPointCount: integer(metrics.nonground_point_count, "M49 full nonground"),
|
|
|
|
|
rejectedPointCount: integer(metrics.rejected_point_count, "M49 full rejected"),
|
|
|
|
|
occupiedCellCount: integer(metrics.occupied_cell_count, "M49 full occupied cells"),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchM49TgsFullShadowSpatial(
|
|
|
|
|
id: string,
|
|
|
|
|
sourceSequence: number,
|
|
|
|
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
|
|
|
|
): Promise<M49TgsFullShadowSpatial> {
|
|
|
|
|
if (!RESULT_ID.test(id) || !Number.isSafeInteger(sourceSequence) || sourceSequence < 0 || sourceSequence >= 4489) {
|
|
|
|
|
throw new M49TgsFullShadowContractError("M49 full-shadow frame identity недопустима.");
|
|
|
|
|
}
|
|
|
|
|
const response = await fetcher(
|
|
|
|
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}/frames/${sourceSequence}/spatial`,
|
|
|
|
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
|
|
|
|
);
|
|
|
|
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full-shadow frame недоступен: HTTP ${response.status}.`);
|
|
|
|
|
const payload = objectValue(await response.json(), "M49 full spatial");
|
|
|
|
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-spatial/v1", "M49 full spatial schema");
|
|
|
|
|
exact(payload.result_id, id, "M49 full spatial result");
|
|
|
|
|
return parseSpatial(id, sourceSequence, payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchM49TgsFullShadowSpatialChunk(
|
|
|
|
|
id: string,
|
|
|
|
|
start: number,
|
|
|
|
|
count = 24,
|
|
|
|
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
|
|
|
|
): Promise<M49TgsFullShadowSpatialChunk> {
|
|
|
|
|
if (!RESULT_ID.test(id) || !Number.isSafeInteger(start) || start < 0 || start >= 4489
|
|
|
|
|
|| !Number.isSafeInteger(count) || count < 1 || count > 24) {
|
|
|
|
|
throw new M49TgsFullShadowContractError("M49 full-shadow chunk identity недопустима.");
|
|
|
|
|
}
|
|
|
|
|
const response = await fetcher(
|
|
|
|
|
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}/spatial/chunk?start=${start}&count=${count}`,
|
|
|
|
|
{ method: "GET", headers: { Accept: "application/json" }, signal },
|
|
|
|
|
);
|
|
|
|
|
if (!response.ok) throw new M49TgsFullShadowContractError(`M49 full-shadow chunk недоступен: HTTP ${response.status}.`);
|
|
|
|
|
const payload = objectValue(await response.json(), "M49 full spatial chunk");
|
|
|
|
|
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-spatial-chunk/v1", "M49 full chunk schema");
|
|
|
|
|
exact(payload.result_id, id, "M49 full chunk result");
|
|
|
|
|
exact(payload.start, start, "M49 full chunk start");
|
|
|
|
|
const returnedCount = integer(payload.count, "M49 full chunk count");
|
|
|
|
|
if (!Array.isArray(payload.frames) || payload.frames.length !== returnedCount
|
|
|
|
|
|| returnedCount !== Math.min(count, 4489 - start)) {
|
|
|
|
|
throw new M49TgsFullShadowContractError("M49 full chunk: неверная размерность.");
|
|
|
|
|
}
|
|
|
|
|
const costmap = objectValue(payload.costmap, "M49 full chunk costmap");
|
|
|
|
|
return {
|
|
|
|
|
resultId: id,
|
|
|
|
|
start,
|
|
|
|
|
count: returnedCount,
|
|
|
|
|
frames: payload.frames.map((value, index) => parseSpatial(
|
|
|
|
|
id,
|
|
|
|
|
start + index,
|
|
|
|
|
objectValue(value, `M49 full chunk frame ${index}`),
|
|
|
|
|
costmap,
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
}
|