feat(lab): publish full TGS shadow evidence
This commit is contained in:
@@ -47,6 +47,7 @@ import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
|
|||||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||||
|
import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||||
|
|
||||||
export type AdvancedLaboratoryWorkId =
|
export type AdvancedLaboratoryWorkId =
|
||||||
| "m48-object-centric-quality"
|
| "m48-object-centric-quality"
|
||||||
@@ -56,6 +57,7 @@ export type AdvancedLaboratoryWorkId =
|
|||||||
| "m48s-fixed-class-detector"
|
| "m48s-fixed-class-detector"
|
||||||
| "m48t-risk-quality-temporal"
|
| "m48t-risk-quality-temporal"
|
||||||
| "m49-tgs-fail-closed-evidence"
|
| "m49-tgs-fail-closed-evidence"
|
||||||
|
| "m49-tgs-full-shadow"
|
||||||
| "m47-reference-graph-shadow"
|
| "m47-reference-graph-shadow"
|
||||||
| "m4-replay-threat"
|
| "m4-replay-threat"
|
||||||
| "l3-pointpillars-visual-audit"
|
| "l3-pointpillars-visual-audit"
|
||||||
@@ -105,6 +107,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
|||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
"m4-replay-threat",
|
"m4-replay-threat",
|
||||||
"l3-pointpillars-visual-audit",
|
"l3-pointpillars-visual-audit",
|
||||||
@@ -149,6 +152,7 @@ const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
|||||||
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
||||||
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
||||||
"m49-tgs-fail-closed-evidence": "m49-tgs-fail-closed",
|
"m49-tgs-fail-closed-evidence": "m49-tgs-fail-closed",
|
||||||
|
"m49-tgs-full-shadow": "m49-tgs-full-shadow",
|
||||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||||
"m4-replay-threat": "m4-threat-replay",
|
"m4-replay-threat": "m4-threat-replay",
|
||||||
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
||||||
@@ -201,6 +205,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
|||||||
m48s: null,
|
m48s: null,
|
||||||
m48t: null,
|
m48t: null,
|
||||||
m49Tgs: null,
|
m49Tgs: null,
|
||||||
|
m49TgsFull: null,
|
||||||
m4Threat: null,
|
m4Threat: null,
|
||||||
l3: null,
|
l3: null,
|
||||||
l31: null,
|
l31: null,
|
||||||
@@ -332,6 +337,7 @@ export function advancedLaboratoryResultAvailable(
|
|||||||
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
||||||
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
||||||
: workId === "m49-tgs-fail-closed-evidence" ? results.m49Tgs !== null
|
: workId === "m49-tgs-fail-closed-evidence" ? results.m49Tgs !== null
|
||||||
|
: workId === "m49-tgs-full-shadow" ? results.m49TgsFull !== null
|
||||||
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
||||||
: workId === "m4-replay-threat" ? results.m4Threat !== null
|
: workId === "m4-replay-threat" ? results.m4Threat !== null
|
||||||
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
|
||||||
@@ -402,6 +408,9 @@ export async function fetchAdvancedLaboratoryResult(
|
|||||||
} else if (workId === "m49-tgs-fail-closed-evidence") {
|
} else if (workId === "m49-tgs-fail-closed-evidence") {
|
||||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 TGS LAB identity не выбрана.");
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 TGS LAB identity не выбрана.");
|
||||||
results.m49Tgs = await fetchM49TgsFailClosedResult(resultId, { fetcher, signal });
|
results.m49Tgs = await fetchM49TgsFailClosedResult(resultId, { fetcher, signal });
|
||||||
|
} else if (workId === "m49-tgs-full-shadow") {
|
||||||
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 full TGS shadow identity не выбрана.");
|
||||||
|
results.m49TgsFull = await fetchM49TgsFullShadowResult(resultId, { fetcher, signal });
|
||||||
} else if (workId === "m47-reference-graph-shadow") {
|
} else if (workId === "m47-reference-graph-shadow") {
|
||||||
if (!resultId) {
|
if (!resultId) {
|
||||||
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
|
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancySha
|
|||||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
import type { M49TgsFailClosedResult } from "./m49TgsFailClosed";
|
import type { M49TgsFailClosedResult } from "./m49TgsFailClosed";
|
||||||
|
import type { M49TgsFullShadowResult } from "./m49TgsFullShadow";
|
||||||
|
|
||||||
export interface AdvancedLaboratoryResults {
|
export interface AdvancedLaboratoryResults {
|
||||||
m47Graph: M47ReferenceGraphLabResult | null;
|
m47Graph: M47ReferenceGraphLabResult | null;
|
||||||
@@ -51,6 +52,7 @@ export interface AdvancedLaboratoryResults {
|
|||||||
m48s: M48SFixedClassDetectorResult | null;
|
m48s: M48SFixedClassDetectorResult | null;
|
||||||
m48t: M48TRiskQualityResult | null;
|
m48t: M48TRiskQualityResult | null;
|
||||||
m49Tgs: M49TgsFailClosedResult | null;
|
m49Tgs: M49TgsFailClosedResult | null;
|
||||||
|
m49TgsFull: M49TgsFullShadowResult | null;
|
||||||
m4Threat: M4ThreatReplayResult | null;
|
m4Threat: M4ThreatReplayResult | null;
|
||||||
l3: L3PointPillarsVisualAuditResult | null;
|
l3: L3PointPillarsVisualAuditResult | null;
|
||||||
l31: L31PointPillarsRavnovesResult | null;
|
l31: L31PointPillarsRavnovesResult | null;
|
||||||
|
|||||||
@@ -969,7 +969,7 @@ export async function fetchAdvancedLaboratoryResults({
|
|||||||
return {
|
return {
|
||||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||||
m48r3StaticOccupancy: null,
|
m48r3StaticOccupancy: null,
|
||||||
m48s: null, m48t: null, m49Tgs: null, m4Threat: null,
|
m48s: null, m48t: null, m49Tgs: null, m49TgsFull: null, m4Threat: null,
|
||||||
l3: null, l31: null, l32: null, l33: null,
|
l3: null, l31: null, l32: null, l33: null,
|
||||||
e31,
|
e31,
|
||||||
e32,
|
e32,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShad
|
|||||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||||
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
|
||||||
|
import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult";
|
||||||
|
|
||||||
export { isAdvancedLaboratoryWorkId };
|
export { isAdvancedLaboratoryWorkId };
|
||||||
export type { AdvancedLaboratoryWorkId };
|
export type { AdvancedLaboratoryWorkId };
|
||||||
@@ -112,6 +113,9 @@ export function AdvancedLaboratoryResult({
|
|||||||
if (workId === "m49-tgs-fail-closed-evidence" && results.m49Tgs) {
|
if (workId === "m49-tgs-fail-closed-evidence" && results.m49Tgs) {
|
||||||
return <M49TgsFailClosedResultView rigLabel={rigLabel} result={results.m49Tgs} />;
|
return <M49TgsFailClosedResultView rigLabel={rigLabel} result={results.m49Tgs} />;
|
||||||
}
|
}
|
||||||
|
if (workId === "m49-tgs-full-shadow" && results.m49TgsFull) {
|
||||||
|
return <M49TgsFullShadowResultView rigLabel={rigLabel} result={results.m49TgsFull} />;
|
||||||
|
}
|
||||||
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
|
||||||
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RecordedEvidenceSemanticClass,
|
||||||
|
RecordedEvidenceSemanticPaletteEntry,
|
||||||
|
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
|
||||||
|
import {
|
||||||
|
fetchM49TgsFullShadowSpatialChunk,
|
||||||
|
type M49TgsFullShadowResult,
|
||||||
|
type M49TgsFullShadowSpatial,
|
||||||
|
type M49TgsFullShadowSpatialChunk,
|
||||||
|
type M49TgsFullShadowStateCode,
|
||||||
|
} from "../../core/laboratory/m49TgsFullShadow";
|
||||||
|
import {
|
||||||
|
M4ReplayThreatVisual,
|
||||||
|
type M4ReplayClassifiedSpatialFrame,
|
||||||
|
} from "./M4ReplayThreatVisual";
|
||||||
|
|
||||||
|
const CLASSES: readonly RecordedEvidenceSemanticClass[] = [
|
||||||
|
{ id: 1, label: "Ground support" },
|
||||||
|
{ id: 2, label: "Non-ground occupied" },
|
||||||
|
{ id: 3, label: "Unknown / rejected" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
|
||||||
|
{ classId: 1, color: { kind: "token", token: "--nodedc-success-rgb" } },
|
||||||
|
{ classId: 2, color: { kind: "token", token: "--nodedc-danger-rgb" } },
|
||||||
|
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHUNK_FRAMES = 24;
|
||||||
|
const RETAINED_CHUNKS = 4;
|
||||||
|
|
||||||
|
function cellState(code: M49TgsFullShadowStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
|
||||||
|
if (code === 1) return "ground-support";
|
||||||
|
if (code === 2) return "nonground-occupied";
|
||||||
|
if (code === 3) return "unknown-rejected";
|
||||||
|
return "unobserved";
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(error: unknown): string {
|
||||||
|
return error instanceof Error && error.message.trim()
|
||||||
|
? error.message
|
||||||
|
: "Полный TGS spatial frame недоступен.";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowResult }) {
|
||||||
|
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||||
|
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
||||||
|
() => new Map(),
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const inFlightRef = useRef(new Map<number, AbortController>());
|
||||||
|
const activeChunkStart = activeSequence === null
|
||||||
|
? null
|
||||||
|
: Math.floor(activeSequence / CHUNK_FRAMES) * CHUNK_FRAMES;
|
||||||
|
const activeChunkStartRef = useRef(activeChunkStart);
|
||||||
|
activeChunkStartRef.current = activeChunkStart;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||||
|
inFlightRef.current.clear();
|
||||||
|
setChunks(new Map());
|
||||||
|
setError(null);
|
||||||
|
return () => {
|
||||||
|
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||||
|
inFlightRef.current.clear();
|
||||||
|
};
|
||||||
|
}, [result.resultId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeChunkStart === null) return;
|
||||||
|
const desiredStarts = [activeChunkStart, activeChunkStart + CHUNK_FRAMES]
|
||||||
|
.filter((start) => start < result.timeline.frameCount);
|
||||||
|
const desired = new Set(desiredStarts);
|
||||||
|
for (const [start, controller] of inFlightRef.current) {
|
||||||
|
if (desired.has(start)) continue;
|
||||||
|
controller.abort();
|
||||||
|
inFlightRef.current.delete(start);
|
||||||
|
}
|
||||||
|
for (const start of desiredStarts) {
|
||||||
|
if (chunks.has(start) || inFlightRef.current.has(start)) continue;
|
||||||
|
const controller = new AbortController();
|
||||||
|
inFlightRef.current.set(start, controller);
|
||||||
|
void fetchM49TgsFullShadowSpatialChunk(result.resultId, start, CHUNK_FRAMES, {
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
.then((chunk) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
setChunks((current) => {
|
||||||
|
const next = new Map(current);
|
||||||
|
next.set(start, chunk);
|
||||||
|
const center = activeChunkStartRef.current ?? start;
|
||||||
|
const retained = [...next.keys()]
|
||||||
|
.sort((left, right) => Math.abs(left - center) - Math.abs(right - center))
|
||||||
|
.slice(0, RETAINED_CHUNKS);
|
||||||
|
return new Map(retained.map((key) => [key, next.get(key)!]));
|
||||||
|
});
|
||||||
|
if (start === activeChunkStartRef.current) setError(null);
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted && start === activeChunkStartRef.current) {
|
||||||
|
setError(message(caught));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (inFlightRef.current.get(start) === controller) inFlightRef.current.delete(start);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [activeChunkStart, chunks, result.resultId, result.timeline.frameCount]);
|
||||||
|
|
||||||
|
const spatial = useMemo<M49TgsFullShadowSpatial | null>(() => {
|
||||||
|
if (activeSequence === null || activeChunkStart === null) return null;
|
||||||
|
return chunks.get(activeChunkStart)?.frames.find(
|
||||||
|
(frame) => frame.sourceSequence === activeSequence,
|
||||||
|
) ?? null;
|
||||||
|
}, [activeChunkStart, activeSequence, chunks]);
|
||||||
|
const loading = activeSequence !== null && !spatial && !error;
|
||||||
|
|
||||||
|
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||||
|
if (!spatial) return null;
|
||||||
|
return {
|
||||||
|
sourceSequence: spatial.sourceSequence,
|
||||||
|
sampleAvailable: spatial.sampleAvailable,
|
||||||
|
sourcePointCount: spatial.metrics.eligiblePointCount,
|
||||||
|
pointsMapGravityLocalXyzM: [],
|
||||||
|
pointClassIds: [],
|
||||||
|
cellsMapGravityLocal: spatial.costmap.centersXyM.map((center, index) => ({
|
||||||
|
centerXyM: center,
|
||||||
|
zBoundsM: spatial.costmap.zBoundsM[index]!,
|
||||||
|
state: cellState(spatial.costmap.states[index]!),
|
||||||
|
})),
|
||||||
|
cellSizeM: spatial.costmap.cellSizeM,
|
||||||
|
classes: CLASSES,
|
||||||
|
palette: PALETTE,
|
||||||
|
};
|
||||||
|
}, [spatial]);
|
||||||
|
|
||||||
|
const handleSequenceChange = useCallback((sequence: number | null) => {
|
||||||
|
setActiveSequence(sequence);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<M4ReplayThreatVisual
|
||||||
|
resultId={result.source.linkedVisualResultId}
|
||||||
|
showReviewAnchorBoxes={false}
|
||||||
|
reviewLabel="4 489 source-paced TGS frames"
|
||||||
|
evidenceLabel="M49 · full TGS shadow"
|
||||||
|
initialSpatialMode="3d"
|
||||||
|
onActiveSequenceChange={handleSequenceChange}
|
||||||
|
classifiedSpatialLayer={{
|
||||||
|
label: "TGS full shadow · causal rolling 1 s",
|
||||||
|
pointLayerLabel: "SOURCE POINTS",
|
||||||
|
cellLayerLabel: "TGS COSTMAP",
|
||||||
|
expectedAtSequence: true,
|
||||||
|
frame: classifiedFrame,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
replacePointCloud: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
LaboratoryEvidence,
|
||||||
|
LaboratoryResultSummary,
|
||||||
|
LaboratorySummary,
|
||||||
|
LaboratoryWorkTemplate,
|
||||||
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
|
import type { M49TgsFullShadowResult } from "../../core/laboratory/m49TgsFullShadow";
|
||||||
|
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||||
|
|
||||||
|
function number(value: number, digits = 1): string {
|
||||||
|
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M49TgsFullShadowResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M49TgsFullShadowResult;
|
||||||
|
}) {
|
||||||
|
const accepted = result.decision.candidateRetained;
|
||||||
|
const status = accepted
|
||||||
|
? "Source-paced CPU shadow принят; визуальное и integrated-graph качество ещё проверяются"
|
||||||
|
: "Source-paced CPU shadow не прошёл performance gate";
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.9T5 · полный source-paced TRAVEL TGS shadow"
|
||||||
|
description="Один CPU-only процесс прошёл весь recorded timeline RAVNOVES00 в исходном темпе. Камера остаётся владельцем времени; dense source cloud сохраняется, поверх него показывается четырёхсостояний TGS costmap."
|
||||||
|
status={status}
|
||||||
|
statusTone={accepted ? "success" : "danger"}
|
||||||
|
facts={[
|
||||||
|
{ label: "Конфигурация", value: `${rigLabel} RIGHT · gravity-aligned LiDAR · causal ${number(result.configuration.historySeconds)} с` },
|
||||||
|
{ label: "Timeline", value: `${result.timeline.frameCount.toLocaleString("ru-RU")} кадров · ${result.timeline.availableLidarFrameCount.toLocaleString("ru-RU")} LiDAR · ${result.timeline.missingLidarFrameCount} UNOBSERVED` },
|
||||||
|
{ label: "Нагрузка", value: `Worker 006 CPU-only · GPU 0 · ${number(result.timeline.effectiveFps, 3)} source FPS` },
|
||||||
|
{ label: "Authority", value: "REPLAY-SIMULATED · navigation/actuation OFF · integrated graph отдельно" },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Удерживает ли готовый TRAVEL TGS полный десятигерцовый replay без очереди и потери кадров?",
|
||||||
|
approach: "Все 4 489 camera frames планируются по исходным timestamps. Для 3 928 доступных LiDAR frames выполняется causal rolling 1 s; 561 пропуск остаётся полностью UNOBSERVED.",
|
||||||
|
principalResult: `${result.timeline.frameCount}/4 489 frames и ${result.pointAccounting.eligible.toLocaleString("ru-RU")} eligible points учтены; TGS p95/p99 ${number(result.performance.candidateTgsMs.p95, 2)}/${number(result.performance.candidateTgsMs.p99, 2)} мс, completion age p99 ${number(result.performance.completionAgeMs.p99, 2)} мс, capacity drops ${result.performance.capacityDropCount}.`,
|
||||||
|
limitation: "Это isolated CPU shadow. Он ещё не доказывает качество красных occupied-ячеек, проходимость для конкретного корпуса или регрессию FPS полного world-state graph.",
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "deterministic",
|
||||||
|
pipelineId: "travel-tgs-full-source-paced-shadow/v1",
|
||||||
|
components: [
|
||||||
|
{ kind: "source", name: "RAVNOVES00", version: "4 489-frame recorded timeline", role: "camera-owned source clock + registered LiDAR", identitySha256: result.source.sourcePackSha256 },
|
||||||
|
{ kind: "algorithm", name: "TRAVEL GroundSeg", version: "95dc2fbd66a343efd9060c45a5711b6307a950a4", role: "gravity-aligned ground/non-ground separation; AOS OFF", identitySha256: result.configuration.configSha256 },
|
||||||
|
{ kind: "algorithm", name: "fail-closed costmap adapter", version: "v1", role: "occupied > rejected > ground > unobserved; no free inference", identitySha256: result.resultId.split("-").at(-1) ?? null },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.9T5 VISUAL EVIDENCE · FULL CAMERA TIMELINE + SOURCE CLOUD + TGS COSTMAP"
|
||||||
|
title="Полный timeline: dense исходные точки сохранены, TGS-ячейки синхронны каждому кадру"
|
||||||
|
kind="recorded-replay"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M49TgsFullShadowEvidence result={result} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Что доказал полный прогон"
|
||||||
|
status={status}
|
||||||
|
statusTone={accepted ? "success" : "danger"}
|
||||||
|
metrics={[
|
||||||
|
{ label: "Timeline", value: `${result.timeline.frameCount}/4 489`, hint: `${result.timeline.availableLidarFrameCount} LiDAR + ${result.timeline.missingLidarFrameCount} explicit UNOBSERVED` },
|
||||||
|
{ label: "TGS p95 / p99", value: `${number(result.performance.candidateTgsMs.p95, 2)} / ${number(result.performance.candidateTgsMs.p99, 2)} мс`, hint: "чистый candidate stage на CPU" },
|
||||||
|
{ label: "Completion age p99", value: `${number(result.performance.completionAgeMs.p99, 2)} мс`, hint: "от source timestamp до готового frame result" },
|
||||||
|
{ label: "Capacity drops", value: String(result.performance.capacityDropCount), hint: "кадры не отбрасывались ради темпа" },
|
||||||
|
{ label: "Point accounting", value: "100%", hint: `${result.pointAccounting.eligible.toLocaleString("ru-RU")} eligible points` },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: "Полный CPU-only TGS shadow воспроизводимо проходит recorded source clock, сохраняет fail-closed представление и не использует AOS/GPU.",
|
||||||
|
notProved: "Не приняты visual traversability, модель корпуса, камера-проекция TGS и нагрузка после встраивания в полный realtime world-state graph.",
|
||||||
|
decision: accepted
|
||||||
|
? "Кандидат остаётся. Просмотреть полный timeline, затем подключить shadow к realtime graph и измерить общий FPS/latency regression."
|
||||||
|
: "Кандидат не встраивать; сначала локализовать performance gate, который не прошёл полный replay.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -109,6 +109,8 @@ export interface M4ReplayThreatReviewAnchor {
|
|||||||
|
|
||||||
export interface M4ReplayClassifiedSpatialFrame {
|
export interface M4ReplayClassifiedSpatialFrame {
|
||||||
sourceSequence: number;
|
sourceSequence: number;
|
||||||
|
sampleAvailable?: boolean;
|
||||||
|
sourcePointCount?: number;
|
||||||
pointsMapGravityLocalXyzM: readonly (readonly [number, number, number])[];
|
pointsMapGravityLocalXyzM: readonly (readonly [number, number, number])[];
|
||||||
pointClassIds: readonly (number | null)[];
|
pointClassIds: readonly (number | null)[];
|
||||||
cellsMapGravityLocal: readonly {
|
cellsMapGravityLocal: readonly {
|
||||||
@@ -129,6 +131,7 @@ export interface M4ReplayClassifiedSpatialLayer {
|
|||||||
frame: M4ReplayClassifiedSpatialFrame | null;
|
frame: M4ReplayClassifiedSpatialFrame | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
replacePointCloud?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
|
||||||
@@ -255,8 +258,8 @@ export function M4ReplayThreatVisual({
|
|||||||
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
|
||||||
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onActiveSequenceChange?.(frame?.sequence ?? null);
|
onActiveSequenceChange?.(timelineFrame.activeSequence);
|
||||||
}, [frame?.sequence, onActiveSequenceChange]);
|
}, [onActiveSequenceChange, timelineFrame.activeSequence]);
|
||||||
const lastSpatialFrameRef = useRef<{
|
const lastSpatialFrameRef = useRef<{
|
||||||
resultId: string;
|
resultId: string;
|
||||||
frame: M4ThreatTimelineFrame;
|
frame: M4ThreatTimelineFrame;
|
||||||
@@ -400,16 +403,18 @@ export function M4ReplayThreatVisual({
|
|||||||
return status === 2 || status === 3 ? classId : null;
|
return status === 2 || status === 3 ? classId : null;
|
||||||
});
|
});
|
||||||
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
|
||||||
const classifiedSpatialFrame = !displayingBufferedFrame
|
const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence
|
||||||
&& classifiedSpatialLayer?.frame?.sourceSequence === frame?.sequence
|
? spatialFrame
|
||||||
&& spatialFrame?.sequence === frame?.sequence
|
: null;
|
||||||
|
const classifiedSpatialFrame = classifiedSpatialLayer?.frame?.sourceSequence === timelineFrame.activeSequence
|
||||||
? classifiedSpatialLayer?.frame ?? null
|
? classifiedSpatialLayer?.frame ?? null
|
||||||
: null;
|
: null;
|
||||||
|
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
|
||||||
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
|
||||||
const mapGravityLocalSensorToBodyGround = useCallback((
|
const mapGravityLocalSensorToBodyGround = useCallback((
|
||||||
point: readonly [number, number, number],
|
point: readonly [number, number, number],
|
||||||
): readonly [number, number, number] => {
|
): readonly [number, number, number] => {
|
||||||
const basis = spatialFrame?.bodyFrame?.basisMapFromBody;
|
const basis = activeSpatialFrame?.bodyFrame?.basisMapFromBody;
|
||||||
const rotated: readonly [number, number, number] = basis ? [
|
const rotated: readonly [number, number, number] = basis ? [
|
||||||
basis[0][0] * point[0] + basis[1][0] * point[1] + basis[2][0] * point[2],
|
basis[0][0] * point[0] + basis[1][0] * point[1] + basis[2][0] * point[2],
|
||||||
basis[0][1] * point[0] + basis[1][1] * point[1] + basis[2][1] * point[2],
|
basis[0][1] * point[0] + basis[1][1] * point[1] + basis[2][1] * point[2],
|
||||||
@@ -418,7 +423,7 @@ export function M4ReplayThreatVisual({
|
|||||||
// TGS evidence is translation-only map-gravity-local with the current LiDAR
|
// TGS evidence is translation-only map-gravity-local with the current LiDAR
|
||||||
// as its origin. The metric scene uses the body ground projection as z=0.
|
// as its origin. The metric scene uses the body ground projection as z=0.
|
||||||
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
|
||||||
}, [nominalSensorHeightM, spatialFrame?.bodyFrame?.basisMapFromBody]);
|
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
|
||||||
const classifiedPointsBody = useMemo(
|
const classifiedPointsBody = useMemo(
|
||||||
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
|
||||||
mapGravityLocalSensorToBodyGround,
|
mapGravityLocalSensorToBodyGround,
|
||||||
@@ -776,7 +781,11 @@ export function M4ReplayThreatVisual({
|
|||||||
? splitPrimarySize
|
? splitPrimarySize
|
||||||
: 100;
|
: 100;
|
||||||
|
|
||||||
const overlay = metadata.timeline && frame ? (
|
const overlaySequence = timelineFrame.activeSequence ?? frame?.sequence ?? null;
|
||||||
|
const overlaySessionSeconds = overlaySequence === null
|
||||||
|
? null
|
||||||
|
: (metadata.timeline?.frameTimesNs[overlaySequence] ?? 0) / 1_000_000_000;
|
||||||
|
const overlay = metadata.timeline && overlaySequence !== null ? (
|
||||||
<div
|
<div
|
||||||
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
|
className="l3-visual-audit__overlay m4-replay-threat-visual__overlay"
|
||||||
style={{
|
style={{
|
||||||
@@ -785,9 +794,9 @@ export function M4ReplayThreatVisual({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span>RAVNOVES00 · recorded realtime</span>
|
<span>RAVNOVES00 · recorded realtime</span>
|
||||||
<strong>frame {frame.sequence + 1}/{metadata.timeline.frameCount}</strong>
|
<strong>frame {overlaySequence + 1}/{metadata.timeline.frameCount}</strong>
|
||||||
<small>
|
<small>
|
||||||
+{(frame.sessionSeconds - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
+{((overlaySessionSeconds ?? metadata.timeline.timelineStartSeconds) - metadata.timeline.timelineStartSeconds).toFixed(3)} с
|
||||||
· {displayingBufferedFrame
|
· {displayingBufferedFrame
|
||||||
? "держим последний кадр, следующий в буфере"
|
? "держим последний кадр, следующий в буфере"
|
||||||
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
: playbackController.playback.playing ? "воспроизведение" : "пауза / seek"}
|
||||||
@@ -797,24 +806,32 @@ export function M4ReplayThreatVisual({
|
|||||||
<span>Spatial evidence</span>
|
<span>Spatial evidence</span>
|
||||||
<strong>{classifiedSpatialLayer
|
<strong>{classifiedSpatialLayer
|
||||||
? classifiedSpatialFrame
|
? classifiedSpatialFrame
|
||||||
|
? replaceClassifiedPointCloud
|
||||||
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
|
||||||
|
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} TGS cells`
|
||||||
: "TGS spatial buffer"
|
: "TGS spatial buffer"
|
||||||
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
|
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
|
||||||
<small>{classifiedSpatialLayer
|
<small>{classifiedSpatialLayer
|
||||||
? classifiedSpatialFrame
|
? classifiedSpatialFrame
|
||||||
|
? classifiedSpatialFrame.sampleAvailable === false
|
||||||
|
? "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
|
||||||
|
: activeSpatialFrame
|
||||||
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
||||||
|
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
|
||||||
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
|
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
{spatialFrame
|
{spatialFrame
|
||||||
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
|
||||||
: "квалифицированный spatial frame ещё не получен"}
|
: "квалифицированный spatial frame ещё не получен"}
|
||||||
{frame.worldStateAvailable
|
{frame
|
||||||
|
? frame.worldStateAvailable
|
||||||
? " · world-state delivered"
|
? " · world-state delivered"
|
||||||
: ` · world-state gap (${frame.terminalOutcome})`}
|
: ` · world-state gap (${frame.terminalOutcome})`
|
||||||
|
: " · world-state frame unavailable"}
|
||||||
{accumulatedCameraPoints
|
{accumulatedCameraPoints
|
||||||
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
|
||||||
: pointCloudOverlay
|
: pointCloudOverlay && frame
|
||||||
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
|
||||||
: showMediaPoints && cameraPointOverlay.error
|
: showMediaPoints && cameraPointOverlay.error
|
||||||
? " · накопленное camera cloud недоступно"
|
? " · накопленное camera cloud недоступно"
|
||||||
@@ -935,12 +952,12 @@ export function M4ReplayThreatVisual({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{spatialFrame && (!classifiedSpatialLayer || classifiedSpatialFrame) ? (
|
{(!classifiedSpatialLayer ? spatialFrame : classifiedSpatialFrame) ? (
|
||||||
<LaboratoryMetricEvidenceScene
|
<LaboratoryMetricEvidenceScene
|
||||||
ref={metricSceneRef}
|
ref={metricSceneRef}
|
||||||
pointCloudBodyXyzM={classifiedSpatialFrame
|
pointCloudBodyXyzM={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedPointsBody
|
? classifiedPointsBody
|
||||||
: spatialFrame.pointCloudBodyXyzM}
|
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
|
||||||
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
|
||||||
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
|
||||||
rig={timeline.rig}
|
rig={timeline.rig}
|
||||||
@@ -952,13 +969,13 @@ export function M4ReplayThreatVisual({
|
|||||||
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
|
||||||
showRollingMap={showRollingMap}
|
showRollingMap={showRollingMap}
|
||||||
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
showLowStep={classifiedSpatialFrame ? false : showLowStep}
|
||||||
pointSemanticClassIds={classifiedSpatialFrame
|
pointSemanticClassIds={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.pointClassIds
|
? classifiedSpatialFrame.pointClassIds
|
||||||
: alignedSemanticPointIds}
|
: alignedSemanticPointIds}
|
||||||
semanticClasses={classifiedSpatialFrame
|
semanticClasses={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.classes
|
? classifiedSpatialFrame.classes
|
||||||
: semanticClasses}
|
: semanticClasses}
|
||||||
semanticPalette={classifiedSpatialFrame
|
semanticPalette={classifiedSpatialFrame && replaceClassifiedPointCloud
|
||||||
? classifiedSpatialFrame.palette
|
? classifiedSpatialFrame.palette
|
||||||
: semanticPalette}
|
: semanticPalette}
|
||||||
classifiedCells={classifiedCellsBody}
|
classifiedCells={classifiedCellsBody}
|
||||||
@@ -979,7 +996,15 @@ export function M4ReplayThreatVisual({
|
|||||||
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
|
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{frame && !frame.spatialAvailable ? (
|
{classifiedSpatialFrame?.sampleAvailable === false ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; все 2 244 TGS-ячейки явно UNOBSERVED.
|
||||||
|
</div>
|
||||||
|
) : classifiedSpatialFrame && !activeSpatialFrame ? (
|
||||||
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
|
Кадр {classifiedSpatialFrame.sourceSequence + 1}: TGS costmap показан cell-only; linked source cloud для отрисовки отсутствует.
|
||||||
|
</div>
|
||||||
|
) : frame && !frame.spatialAvailable ? (
|
||||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||||
{spatialFrame
|
{spatialFrame
|
||||||
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
|
? `На кадре ${frame.sequence + 1} нет body frame; держим spatial evidence кадра ${spatialFrame.sequence + 1}.`
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
|||||||
experimentName: "TRAVEL TGS fail-closed traversability evidence",
|
experimentName: "TRAVEL TGS fail-closed traversability evidence",
|
||||||
variantName: "M4.9T4 · 10 anchors · causal rolling 1 s · AOS OFF",
|
variantName: "M4.9T4 · 10 anchors · causal rolling 1 s · AOS OFF",
|
||||||
},
|
},
|
||||||
|
"m49-tgs-full-shadow": {
|
||||||
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + gravity-aligned LiDAR`,
|
||||||
|
experimentId: "m49-tgs-full-shadow",
|
||||||
|
experimentName: "TRAVEL TGS complete source-paced shadow",
|
||||||
|
variantName: "M4.9T5 · 4 489 frames · causal rolling 1 s · CPU-only",
|
||||||
|
},
|
||||||
"m47-reference-graph-shadow": {
|
"m47-reference-graph-shadow": {
|
||||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function mergeResults(
|
|||||||
m48s: next.m48s ?? current.m48s,
|
m48s: next.m48s ?? current.m48s,
|
||||||
m48t: next.m48t ?? current.m48t,
|
m48t: next.m48t ?? current.m48t,
|
||||||
m49Tgs: next.m49Tgs ?? current.m49Tgs,
|
m49Tgs: next.m49Tgs ?? current.m49Tgs,
|
||||||
|
m49TgsFull: next.m49TgsFull ?? current.m49TgsFull,
|
||||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||||
l3: next.l3 ?? current.l3,
|
l3: next.l3 ?? current.l3,
|
||||||
l31: next.l31 ?? current.l31,
|
l31: next.l31 ?? current.l31,
|
||||||
@@ -126,6 +127,7 @@ export function useAdvancedLaboratoryCatalog({
|
|||||||
"m48-static-occupancy-qualification",
|
"m48-static-occupancy-qualification",
|
||||||
"m48r3-static-occupancy-shadow",
|
"m48r3-static-occupancy-shadow",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
].includes(selectedWorkId)
|
].includes(selectedWorkId)
|
||||||
&& !indexedResultId
|
&& !indexedResultId
|
||||||
) return;
|
) return;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { after, before, test } from "node:test";
|
||||||
|
|
||||||
|
import { createServer } from "vite";
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let fetchM49TgsFullShadowSpatialChunk;
|
||||||
|
|
||||||
|
const resultId = `m49-tgs-full-shadow-${"a".repeat(64)}`;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = await createServer({
|
||||||
|
appType: "custom",
|
||||||
|
logLevel: "silent",
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
});
|
||||||
|
({ fetchM49TgsFullShadowSpatialChunk } = await server.ssrLoadModule(
|
||||||
|
"/src/core/laboratory/m49TgsFullShadow.ts",
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.9T5 chunk keeps every missing-LiDAR cell explicitly UNOBSERVED", async () => {
|
||||||
|
let requestedUrl = "";
|
||||||
|
const centers = Array.from({ length: 2244 }, (_, index) => [index * 0.45, 0]);
|
||||||
|
const unobserved = Array.from({ length: 2244 }, () => 0);
|
||||||
|
const zBounds = Array.from({ length: 2244 }, () => [null, null]);
|
||||||
|
const metrics = {
|
||||||
|
eligible_point_count: 0,
|
||||||
|
ground_point_count: 0,
|
||||||
|
nonground_point_count: 0,
|
||||||
|
rejected_point_count: 0,
|
||||||
|
occupied_cell_count: 0,
|
||||||
|
};
|
||||||
|
const chunk = await fetchM49TgsFullShadowSpatialChunk(resultId, 7, 1, {
|
||||||
|
fetcher: async (url) => {
|
||||||
|
requestedUrl = String(url);
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
schema_version: "missioncore.m49-tgs-full-shadow-spatial-chunk/v1",
|
||||||
|
result_id: resultId,
|
||||||
|
start: 7,
|
||||||
|
count: 1,
|
||||||
|
coordinate_frame: "map-gravity-local",
|
||||||
|
costmap: {
|
||||||
|
cell_size_m: 0.45,
|
||||||
|
radius_m: 12,
|
||||||
|
centers_xy_m: centers,
|
||||||
|
},
|
||||||
|
frames: [{
|
||||||
|
source_sequence: 7,
|
||||||
|
source_frame_index: 7,
|
||||||
|
session_seconds: 36.119857292,
|
||||||
|
sample_available: false,
|
||||||
|
states: unobserved,
|
||||||
|
z_bounds_m: zBounds,
|
||||||
|
metrics,
|
||||||
|
}],
|
||||||
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
requestedUrl,
|
||||||
|
`/api/v1/laboratory/m49/tgs-full-shadow/${resultId}/spatial/chunk?start=7&count=1`,
|
||||||
|
);
|
||||||
|
assert.equal(chunk.count, 1);
|
||||||
|
assert.equal(chunk.frames[0].sampleAvailable, false);
|
||||||
|
assert.equal(chunk.frames[0].costmap.states.length, 2244);
|
||||||
|
assert.deepEqual(new Set(chunk.frames[0].costmap.states), new Set([0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.9T5 viewer prefetches 24-frame immutable chunks", async () => {
|
||||||
|
const source = await readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
assert.match(source, /const CHUNK_FRAMES = 24/);
|
||||||
|
assert.match(source, /activeChunkStart \+ CHUNK_FRAMES/);
|
||||||
|
assert.match(source, /fetchM49TgsFullShadowSpatialChunk/);
|
||||||
|
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
|
||||||
|
});
|
||||||
@@ -801,12 +801,13 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
|||||||
assert.match(visual, /showLocalSurface/);
|
assert.match(visual, /showLocalSurface/);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
/pointCloudBodyXyzM=\{classifiedSpatialFrame[\s\S]*\? classifiedPointsBody[\s\S]*: spatialFrame\.pointCloudBodyXyzM\}/,
|
/pointCloudBodyXyzM=\{classifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
/const classifiedSpatialFrame = !displayingBufferedFrame[\s\S]*classifiedSpatialLayer\?\.frame\?\.sourceSequence === frame\?\.sequence[\s\S]*spatialFrame\?\.sequence === frame\?\.sequence/,
|
/const activeSpatialFrame = spatialFrame\?\.sequence === timelineFrame\.activeSequence[\s\S]*const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence/,
|
||||||
);
|
);
|
||||||
|
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
|
||||||
assert.match(
|
assert.match(
|
||||||
visual,
|
visual,
|
||||||
/mapGravityLocalSensorToBodyGround[\s\S]*rotated\[2\] \+ nominalSensorHeightM/,
|
/mapGravityLocalSensorToBodyGround[\s\S]*rotated\[2\] \+ nominalSensorHeightM/,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||||
|
"work_id": "m49-tgs-full-shadow",
|
||||||
|
"evidence": {
|
||||||
|
"runtime_relative_root": "m49/tgs-full-shadow-results",
|
||||||
|
"result_id_prefix": "m49-tgs-full-shadow",
|
||||||
|
"document_name": "manifest.json",
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -176,6 +176,20 @@
|
|||||||
"run": "missioncore.laboratory-run/v1",
|
"run": "missioncore.laboratory-run/v1",
|
||||||
"evidence": "missioncore.m49-tgs-fail-closed-result/v1"
|
"evidence": "missioncore.m49-tgs-fail-closed-result/v1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"work_id": "m49-tgs-full-shadow",
|
||||||
|
"lifecycle": "experimental",
|
||||||
|
"isolation": "bounded-adapter",
|
||||||
|
"adapter_id": "experimental.m49-tgs-full-shadow/v1",
|
||||||
|
"input_roles": ["repository_root"],
|
||||||
|
"contracts": {
|
||||||
|
"source": "missioncore.m49-tgs-full-shadow-result/v1",
|
||||||
|
"provider": "missioncore.travel-tgs-ground-segmentation/v1",
|
||||||
|
"graph": "missioncore.m49-tgs-full-source-paced-shadow/v1",
|
||||||
|
"run": "missioncore.laboratory-run/v1",
|
||||||
|
"evidence": "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"legacy_work_ids": [
|
"legacy_work_ids": [
|
||||||
|
|||||||
@@ -274,6 +274,13 @@
|
|||||||
"signal": "progress",
|
"signal": "progress",
|
||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"catalog_id": "m49-tgs-full-shadow",
|
||||||
|
"evidence_id": "m49-tgs-full-shadow-0faeaaf3aba8dccae974eab51ff9cccf264a13ec285abb1920c1e5a09a7e87bc",
|
||||||
|
"signal": "progress",
|
||||||
|
"lifecycle": "current",
|
||||||
|
"visual_evidence": "available"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"""Seal and verify the complete source-paced TRAVEL TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
RESULT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-lab/v1"
|
||||||
|
REPORT_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-report/v1"
|
||||||
|
WORKER_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-result/v1"
|
||||||
|
PREFIX: Final = "m49-tgs-full-shadow-"
|
||||||
|
PROFILE_SCHEMA: Final = "missioncore.m49-tgs-full-shadow-profile/v1"
|
||||||
|
EVIDENCE_FILES: Final = (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
)
|
||||||
|
_HASH_CHUNK_BYTES: Final = 1024 * 1024
|
||||||
|
_MAX_JSON_BYTES: Final = 4 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class M49TgsFullShadowError(RuntimeError):
|
||||||
|
"""The full TGS shadow is unavailable or violates its immutable contract."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class M49TgsFullShadowResult:
|
||||||
|
result_id: str
|
||||||
|
root: Path
|
||||||
|
manifest: dict[str, Any]
|
||||||
|
report: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_sha256(value: object) -> str:
|
||||||
|
content = json.dumps(
|
||||||
|
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
||||||
|
).encode("utf-8")
|
||||||
|
return hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _json(path: Path, label: str) -> dict[str, Any]:
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
|
||||||
|
raise M49TgsFullShadowError(f"{label} is unavailable")
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M49TgsFullShadowError(f"{label} is invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(path: Path, role: str, media_type: str) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"path": path.name,
|
||||||
|
"role": role,
|
||||||
|
"media_type": media_type,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": _sha256(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def seal_m49_tgs_full_shadow(
|
||||||
|
*,
|
||||||
|
source_root: Path,
|
||||||
|
destination_root: Path,
|
||||||
|
profile_path: Path,
|
||||||
|
linked_visual_result_id: str,
|
||||||
|
created_at_utc: str | None = None,
|
||||||
|
) -> M49TgsFullShadowResult:
|
||||||
|
source = source_root.expanduser().resolve(strict=True)
|
||||||
|
if source.is_symlink() or not source.is_dir():
|
||||||
|
raise M49TgsFullShadowError("Worker evidence root is unavailable")
|
||||||
|
destination = destination_root.expanduser().absolute()
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
if destination.is_symlink():
|
||||||
|
raise M49TgsFullShadowError("destination must not be a symlink")
|
||||||
|
profile = _json(profile_path, "full-shadow profile")
|
||||||
|
worker = _json(source / "result.json", "Worker result")
|
||||||
|
summary = _json(source / "worker-summary.json", "Worker summary")
|
||||||
|
timeline = worker.get("timeline")
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != PROFILE_SCHEMA
|
||||||
|
or worker.get("schema_version") != WORKER_SCHEMA
|
||||||
|
or not isinstance(timeline, dict)
|
||||||
|
or (
|
||||||
|
timeline.get("frame_count") != 4489
|
||||||
|
or timeline.get("available_lidar_frame_count") != 3928
|
||||||
|
or timeline.get("missing_lidar_frame_count") != 561
|
||||||
|
)
|
||||||
|
or worker.get("point_accounting", {}).get("unaccounted") != 0
|
||||||
|
or summary.get("schema_version") != "missioncore.m49-tgs-full-shadow-worker-summary/v1"
|
||||||
|
or summary.get("gpu_requested") is not False
|
||||||
|
or summary.get("aos_used") is not False
|
||||||
|
or summary.get("all_timeline_frames_accounted") is not True
|
||||||
|
or summary.get("all_eligible_points_accounted") is not True
|
||||||
|
or summary.get("canonical_triton_health") != "healthy"
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("Worker full-shadow contract changed")
|
||||||
|
if (
|
||||||
|
not linked_visual_result_id.startswith("m4-threat-replay-")
|
||||||
|
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("linked visual result is invalid")
|
||||||
|
for name in EVIDENCE_FILES:
|
||||||
|
path = source / name
|
||||||
|
proof = worker.get("files", {}).get(name, {})
|
||||||
|
if (
|
||||||
|
path.is_symlink()
|
||||||
|
or not path.is_file()
|
||||||
|
or proof.get("bytes") != path.stat().st_size
|
||||||
|
or proof.get("sha256") != _sha256(path)
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError(f"Worker evidence changed: {name}")
|
||||||
|
identity = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"source_pack_sha256": worker["source_pack_sha256"],
|
||||||
|
"input_manifest_sha256": worker["input_manifest_sha256"],
|
||||||
|
"config_sha256": worker["config_sha256"],
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"files": {name: worker["files"][name]["sha256"] for name in EVIDENCE_FILES},
|
||||||
|
"authority": {
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
identity_sha256 = _canonical_sha256(identity)
|
||||||
|
result_id = f"{PREFIX}{identity_sha256}"
|
||||||
|
target = destination / result_id
|
||||||
|
if target.exists():
|
||||||
|
return read_m49_tgs_full_shadow(target)
|
||||||
|
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
performance_accepted = worker.get("status") == "passed"
|
||||||
|
report = {
|
||||||
|
"schema_version": REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"source_pack_sha256": worker["source_pack_sha256"],
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"profile_id": profile["profile_id"],
|
||||||
|
"config_sha256": worker["config_sha256"],
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"history_seconds": profile["profile"]["history_seconds"],
|
||||||
|
"cell_size_m": worker["costmap"]["cell_size_m"],
|
||||||
|
"radius_m": worker["costmap"]["radius_m"],
|
||||||
|
"state_priority": profile["costmap"]["state_priority"],
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"worker": "Worker 006",
|
||||||
|
"device": "cpu",
|
||||||
|
"gpu_used": False,
|
||||||
|
"aos_used": False,
|
||||||
|
"wrapper_elapsed_seconds": summary["wall_seconds"],
|
||||||
|
"canonical_triton_id": summary["canonical_triton_id"],
|
||||||
|
"canonical_triton_health": summary["canonical_triton_health"],
|
||||||
|
},
|
||||||
|
"timeline": worker["timeline"],
|
||||||
|
"point_accounting": worker["point_accounting"],
|
||||||
|
"performance": worker["performance"],
|
||||||
|
"acceptance": {
|
||||||
|
**worker["acceptance"],
|
||||||
|
"representation_complete": True,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"state": (
|
||||||
|
"source-paced-qualified-visual-review-required"
|
||||||
|
if performance_accepted
|
||||||
|
else "performance-rejected"
|
||||||
|
),
|
||||||
|
"candidate_retained": performance_accepted,
|
||||||
|
"next_action": (
|
||||||
|
"Review the complete camera-synchronised TGS costmap timeline; "
|
||||||
|
"then measure the integrated graph regression separately."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"The run proves recorded source-paced CPU shadow performance, not live sensor transport.",
|
||||||
|
"No independent traversability truth or vehicle envelope is present.",
|
||||||
|
"Missing LiDAR frames are explicit all-cell UNOBSERVED and never inferred free.",
|
||||||
|
"Camera projection, navigation and actuation remain disabled.",
|
||||||
|
],
|
||||||
|
"authority": {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"commands_enabled": False,
|
||||||
|
"realtime_shadow_accepted": performance_accepted,
|
||||||
|
"integrated_graph_performance_accepted": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
"visual_review": {
|
||||||
|
"instrument": "m4-canonical-reference-graph",
|
||||||
|
"linked_visual_result_id": linked_visual_result_id,
|
||||||
|
"frame_count": 4489,
|
||||||
|
"state_codes": profile["state_codes"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-full-", dir=destination) as raw:
|
||||||
|
staging = Path(raw) / result_id
|
||||||
|
staging.mkdir()
|
||||||
|
for name in EVIDENCE_FILES:
|
||||||
|
shutil.copyfile(source / name, staging / name)
|
||||||
|
shutil.copyfile(source / "worker-summary.json", staging / "worker-summary.json")
|
||||||
|
(staging / "report.json").write_text(
|
||||||
|
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
artifacts = [
|
||||||
|
_artifact(staging / "report.json", "report", "application/json"),
|
||||||
|
_artifact(staging / "worker-summary.json", "runtime-summary", "application/json"),
|
||||||
|
]
|
||||||
|
artifacts.extend(
|
||||||
|
_artifact(
|
||||||
|
staging / name,
|
||||||
|
"frame-catalog" if name == "frames.ndjson" else "spatial-evidence",
|
||||||
|
"application/x-ndjson" if name == "frames.ndjson" else "application/x-npy",
|
||||||
|
)
|
||||||
|
for name in EVIDENCE_FILES
|
||||||
|
)
|
||||||
|
manifest = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": created,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
(staging / "manifest.json").write_text(
|
||||||
|
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
staging.replace(target)
|
||||||
|
return read_m49_tgs_full_shadow(target)
|
||||||
|
|
||||||
|
|
||||||
|
def read_m49_tgs_full_shadow(root: Path) -> M49TgsFullShadowResult:
|
||||||
|
candidate = root.expanduser().resolve(strict=True)
|
||||||
|
if candidate.is_symlink() or not candidate.is_dir() or not candidate.name.startswith(PREFIX):
|
||||||
|
raise M49TgsFullShadowError("full-shadow result root is invalid")
|
||||||
|
manifest = _json(candidate / "manifest.json", "full-shadow manifest")
|
||||||
|
report = _json(candidate / "report.json", "full-shadow report")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != RESULT_SCHEMA
|
||||||
|
or report.get("schema_version") != REPORT_SCHEMA
|
||||||
|
or manifest.get("result_id") != candidate.name
|
||||||
|
or report.get("result_id") != candidate.name
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or manifest.get("identity_sha256") != _canonical_sha256(identity)
|
||||||
|
or candidate.name != f"{PREFIX}{manifest['identity_sha256']}"
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("full-shadow identity changed")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact catalog changed")
|
||||||
|
for artifact in artifacts:
|
||||||
|
if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact entry changed")
|
||||||
|
path = candidate / artifact["path"]
|
||||||
|
if (
|
||||||
|
path.parent != candidate
|
||||||
|
or path.is_symlink()
|
||||||
|
or not path.is_file()
|
||||||
|
or artifact.get("byte_length") != path.stat().st_size
|
||||||
|
or artifact.get("sha256") != _sha256(path)
|
||||||
|
):
|
||||||
|
raise M49TgsFullShadowError("full-shadow artifact digest changed")
|
||||||
|
return M49TgsFullShadowResult(candidate.name, candidate, manifest, report)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"M49TgsFullShadowError",
|
||||||
|
"M49TgsFullShadowResult",
|
||||||
|
"PREFIX",
|
||||||
|
"read_m49_tgs_full_shadow",
|
||||||
|
"seal_m49_tgs_full_shadow",
|
||||||
|
]
|
||||||
@@ -134,6 +134,7 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
|
|||||||
)
|
)
|
||||||
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
|
||||||
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
|
||||||
|
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
|
||||||
from k1link.web.map_api import (
|
from k1link.web.map_api import (
|
||||||
MapGatewayConfiguration,
|
MapGatewayConfiguration,
|
||||||
MapGatewayProxy,
|
MapGatewayProxy,
|
||||||
@@ -1004,6 +1005,17 @@ app.include_router(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
app.include_router(
|
||||||
|
build_m49_tgs_full_shadow_router(
|
||||||
|
root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "m49"
|
||||||
|
/ "tgs-full-shadow-results"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
build_m48s_fixed_class_detector_lab_router(
|
build_m48s_fixed_class_detector_lab_router(
|
||||||
root_provider=lambda: (
|
root_provider=lambda: (
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"""Read-only API for the sealed complete TGS shadow."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from fastapi import APIRouter, HTTPException, Query, Response
|
||||||
|
|
||||||
|
from k1link.laboratory.m49_tgs_full_shadow import (
|
||||||
|
M49TgsFullShadowError,
|
||||||
|
M49TgsFullShadowResult,
|
||||||
|
PREFIX,
|
||||||
|
read_m49_tgs_full_shadow,
|
||||||
|
)
|
||||||
|
|
||||||
|
RootProvider = Callable[[], Path | None]
|
||||||
|
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
|
||||||
|
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-full-shadow"
|
||||||
|
|
||||||
|
|
||||||
|
def build_m49_tgs_full_shadow_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
|
||||||
|
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
|
||||||
|
|
||||||
|
def sealed(result_id: str) -> M49TgsFullShadowResult:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None or RESULT_ID.fullmatch(result_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||||
|
candidate = root / result_id
|
||||||
|
if candidate.is_symlink() or not candidate.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found")
|
||||||
|
try:
|
||||||
|
resolved = candidate.resolve(strict=True)
|
||||||
|
if resolved.parent != root:
|
||||||
|
raise ValueError("result escaped configured root")
|
||||||
|
return _read_cached(str(resolved), _signature(resolved))
|
||||||
|
except (M49TgsFullShadowError, OSError, ValueError):
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full shadow not found") from None
|
||||||
|
|
||||||
|
@router.get("/results")
|
||||||
|
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
return _catalog([], configured=False, invalid_total=0)
|
||||||
|
results: list[dict[str, object]] = []
|
||||||
|
invalid = 0
|
||||||
|
for candidate in sorted(root.iterdir()):
|
||||||
|
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
results.append(_project(_read_cached(str(candidate.resolve()), _signature(candidate))))
|
||||||
|
except (M49TgsFullShadowError, OSError, ValueError):
|
||||||
|
invalid += 1
|
||||||
|
results.sort(key=lambda row: (str(row["created_at_utc"]), str(row["result_id"])), reverse=True)
|
||||||
|
return _catalog(results[:limit], configured=True, invalid_total=invalid)
|
||||||
|
|
||||||
|
@router.get("/{result_id}")
|
||||||
|
def get_result(result_id: str) -> dict[str, object]:
|
||||||
|
return _project(sealed(result_id))
|
||||||
|
|
||||||
|
@router.get("/{result_id}/frames/{source_sequence}/spatial")
|
||||||
|
def get_spatial(result_id: str, source_sequence: int) -> Response:
|
||||||
|
if source_sequence < 0 or source_sequence >= 4489:
|
||||||
|
raise HTTPException(status_code=404, detail="M49 TGS full-shadow frame not found")
|
||||||
|
result = sealed(result_id)
|
||||||
|
try:
|
||||||
|
content = _frame_json_cached(
|
||||||
|
str(result.root), result_id, source_sequence, _evidence_signature(result.root)
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||||
|
raise HTTPException(status_code=503, detail="M49 TGS full-shadow spatial evidence failed verification") from None
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/json",
|
||||||
|
headers={"Cache-Control": "private, max-age=31536000, immutable", "X-Content-Type-Options": "nosniff"},
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{result_id}/spatial/chunk")
|
||||||
|
def get_spatial_chunk(
|
||||||
|
result_id: str,
|
||||||
|
start: int = Query(ge=0, lt=4489),
|
||||||
|
count: int = Query(default=24, ge=1, le=24),
|
||||||
|
) -> Response:
|
||||||
|
result = sealed(result_id)
|
||||||
|
bounded_count = min(count, 4489 - start)
|
||||||
|
try:
|
||||||
|
content = _chunk_json_cached(
|
||||||
|
str(result.root), result_id, start, bounded_count, _evidence_signature(result.root)
|
||||||
|
)
|
||||||
|
except (OSError, ValueError, KeyError, json.JSONDecodeError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="M49 TGS full-shadow spatial chunk failed verification",
|
||||||
|
) from None
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/json",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "private, max-age=31536000, immutable",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _read_cached(root: str, signature: tuple[int, ...]) -> M49TgsFullShadowResult:
|
||||||
|
del signature
|
||||||
|
return read_m49_tgs_full_shadow(Path(root))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _frames(root: str, signature: tuple[int, int]) -> tuple[dict[str, object], ...]:
|
||||||
|
del signature
|
||||||
|
path = Path(root) / "frames.ndjson"
|
||||||
|
rows = tuple(json.loads(line) for line in path.read_text(encoding="utf-8").splitlines())
|
||||||
|
if len(rows) != 4489:
|
||||||
|
raise ValueError("full-shadow frame catalog changed")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=96)
|
||||||
|
def _frame_json_cached(
|
||||||
|
root: str, result_id: str, source_sequence: int, signature: tuple[int, ...]
|
||||||
|
) -> bytes:
|
||||||
|
root_path = Path(root)
|
||||||
|
frame_signature = (signature[-2], signature[-1])
|
||||||
|
frame = _frames(root, frame_signature)[source_sequence]
|
||||||
|
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states = states_all[source_sequence]
|
||||||
|
z_bounds = z_all[source_sequence]
|
||||||
|
if (
|
||||||
|
centers.shape != (2244, 2)
|
||||||
|
or states_all.shape != (4489, 2244)
|
||||||
|
or z_all.shape != (4489, 2244, 2)
|
||||||
|
or not np.isfinite(centers).all()
|
||||||
|
or not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
|
||||||
|
):
|
||||||
|
raise ValueError("full-shadow spatial shape changed")
|
||||||
|
payload = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-spatial/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"source_sequence": source_sequence,
|
||||||
|
"source_frame_index": frame["source_frame_index"],
|
||||||
|
"session_seconds": frame["session_seconds"],
|
||||||
|
"sample_available": frame["sample_available"],
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"costmap": {
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"centers_xy_m": centers.astype(float).tolist(),
|
||||||
|
"states": states.astype(int).tolist(),
|
||||||
|
"z_bounds_m": [
|
||||||
|
[
|
||||||
|
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||||
|
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||||
|
]
|
||||||
|
for row in z_bounds
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"metrics": copy.deepcopy(frame),
|
||||||
|
"state_codes": {"UNOBSERVED": 0, "GROUND_SUPPORT": 1, "NONGROUND_OCCUPIED": 2, "UNKNOWN_REJECTED": 3},
|
||||||
|
"aos_used": False,
|
||||||
|
"gpu_used": False,
|
||||||
|
"authority": {"navigation_or_safety_accepted": False, "visual_quality_accepted": False},
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def _chunk_json_cached(
|
||||||
|
root: str,
|
||||||
|
result_id: str,
|
||||||
|
start: int,
|
||||||
|
count: int,
|
||||||
|
signature: tuple[int, ...],
|
||||||
|
) -> bytes:
|
||||||
|
root_path = Path(root)
|
||||||
|
frame_signature = (signature[-2], signature[-1])
|
||||||
|
frames = _frames(root, frame_signature)
|
||||||
|
centers = np.load(root_path / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
states_all = np.load(root_path / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
z_all = np.load(root_path / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False)
|
||||||
|
if (
|
||||||
|
centers.shape != (2244, 2)
|
||||||
|
or states_all.shape != (4489, 2244)
|
||||||
|
or z_all.shape != (4489, 2244, 2)
|
||||||
|
or not np.isfinite(centers).all()
|
||||||
|
):
|
||||||
|
raise ValueError("full-shadow spatial chunk shape changed")
|
||||||
|
rows: list[dict[str, object]] = []
|
||||||
|
for source_sequence in range(start, start + count):
|
||||||
|
frame = frames[source_sequence]
|
||||||
|
states = states_all[source_sequence]
|
||||||
|
z_bounds = z_all[source_sequence]
|
||||||
|
if not np.isin(states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all():
|
||||||
|
raise ValueError("full-shadow spatial state changed")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"source_sequence": source_sequence,
|
||||||
|
"source_frame_index": frame["source_frame_index"],
|
||||||
|
"session_seconds": frame["session_seconds"],
|
||||||
|
"sample_available": frame["sample_available"],
|
||||||
|
"states": states.astype(int).tolist(),
|
||||||
|
"z_bounds_m": [
|
||||||
|
[
|
||||||
|
float(row[0]) if math.isfinite(float(row[0])) else None,
|
||||||
|
float(row[1]) if math.isfinite(float(row[1])) else None,
|
||||||
|
]
|
||||||
|
for row in z_bounds
|
||||||
|
],
|
||||||
|
"metrics": copy.deepcopy(frame),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-spatial-chunk/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"start": start,
|
||||||
|
"count": count,
|
||||||
|
"coordinate_frame": "map-gravity-local",
|
||||||
|
"costmap": {
|
||||||
|
"cell_size_m": 0.45,
|
||||||
|
"radius_m": 12.0,
|
||||||
|
"centers_xy_m": centers.astype(float).tolist(),
|
||||||
|
},
|
||||||
|
"frames": rows,
|
||||||
|
"state_codes": {
|
||||||
|
"UNOBSERVED": 0,
|
||||||
|
"GROUND_SUPPORT": 1,
|
||||||
|
"NONGROUND_OCCUPIED": 2,
|
||||||
|
"UNKNOWN_REJECTED": 3,
|
||||||
|
},
|
||||||
|
"aos_used": False,
|
||||||
|
"gpu_used": False,
|
||||||
|
"authority": {
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"visual_quality_accepted": False,
|
||||||
|
},
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
return json.dumps(
|
||||||
|
payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _project(result: M49TgsFullShadowResult) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
**copy.deepcopy(result.report),
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-view/v1",
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"created_at_utc": result.manifest["created_at_utc"],
|
||||||
|
"ground_truth": False,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog(items: list[dict[str, object]], *, configured: bool, invalid_total: int) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": "missioncore.m49-tgs-full-shadow-catalog/v1",
|
||||||
|
"configured": configured,
|
||||||
|
"items": items,
|
||||||
|
"candidate_total": len(items) + invalid_total,
|
||||||
|
"invalid_total": invalid_total,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_root(provider: RootProvider) -> Path | None:
|
||||||
|
value = provider()
|
||||||
|
if value is None or value.is_symlink() or not value.is_dir():
|
||||||
|
return None
|
||||||
|
return value.resolve(strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _signature(root: Path) -> tuple[int, ...]:
|
||||||
|
values: list[int] = []
|
||||||
|
for name in ("manifest.json", "report.json", "worker-summary.json", *EVIDENCE_FILES):
|
||||||
|
path = root / name
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise ValueError("full-shadow artifact unavailable")
|
||||||
|
stat = path.stat()
|
||||||
|
values.extend((stat.st_size, stat.st_mtime_ns))
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence_signature(root: Path) -> tuple[int, ...]:
|
||||||
|
return _signature(root)
|
||||||
|
|
||||||
|
|
||||||
|
EVIDENCE_FILES: Final = (
|
||||||
|
"costmap-cell-centers-xy-m.npy",
|
||||||
|
"costmap-cell-indices-xy.npy",
|
||||||
|
"costmap-states.npy",
|
||||||
|
"costmap-z-bounds-m.npy",
|
||||||
|
"frames.ndjson",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["build_m49_tgs_full_shadow_router"]
|
||||||
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
repository_root / "config" / "laboratories"
|
repository_root / "config" / "laboratories"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.definitions) == 41
|
assert len(registry.definitions) == 42
|
||||||
assert {item.work_id for item in registry.definitions} >= {
|
assert {item.work_id for item in registry.definitions} >= {
|
||||||
"e31-source-binding",
|
"e31-source-binding",
|
||||||
"e46j-raw-fisheye-realtime",
|
"e46j-raw-fisheye-realtime",
|
||||||
@@ -145,6 +145,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
m48 = next(
|
m48 = next(
|
||||||
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
|
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
by_work_id = {row.work_id: row for row in execution.definitions}
|
by_work_id = {row.work_id: row for row in execution.definitions}
|
||||||
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
|
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
|
||||||
@@ -121,6 +122,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
assert by_work_id["m48t-risk-quality-temporal"].isolation == "bounded-adapter"
|
assert by_work_id["m48t-risk-quality-temporal"].isolation == "bounded-adapter"
|
||||||
assert by_work_id["m49-tgs-fail-closed-evidence"].lifecycle == "experimental"
|
assert by_work_id["m49-tgs-fail-closed-evidence"].lifecycle == "experimental"
|
||||||
assert by_work_id["m49-tgs-fail-closed-evidence"].isolation == "bounded-adapter"
|
assert by_work_id["m49-tgs-fail-closed-evidence"].isolation == "bounded-adapter"
|
||||||
|
assert by_work_id["m49-tgs-full-shadow"].lifecycle == "experimental"
|
||||||
|
assert by_work_id["m49-tgs-full-shadow"].isolation == "bounded-adapter"
|
||||||
assert all(
|
assert all(
|
||||||
row.lifecycle == "canonical"
|
row.lifecycle == "canonical"
|
||||||
for row in execution.definitions
|
for row in execution.definitions
|
||||||
@@ -130,6 +133,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert len(execution.definitions) + len(execution.legacy_work_ids) == len(
|
assert len(execution.definitions) + len(execution.legacy_work_ids) == len(
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
root / "config" / "laboratory-value-review.json"
|
root / "config" / "laboratory-value-review.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.entries) == 39
|
assert len(registry.entries) == 40
|
||||||
assert {entry.catalog_id for entry in registry.entries} >= {
|
assert {entry.catalog_id for entry in registry.entries} >= {
|
||||||
"e28-local-surface",
|
"e28-local-surface",
|
||||||
"e46d-temporal-failure-audit",
|
"e46d-temporal-failure-audit",
|
||||||
@@ -92,4 +92,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m49-tgs-fail-closed-evidence",
|
"m49-tgs-fail-closed-evidence",
|
||||||
|
"m49-tgs-full-shadow",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.web import m49_tgs_full_shadow_api as full_shadow_api
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
WORKER_ROOT = REPOSITORY_ROOT / "experiments" / "perception" / "worker" / "m49_t3_travel"
|
||||||
|
sys.path.insert(0, str(WORKER_ROOT))
|
||||||
|
|
||||||
|
EVIDENCE_SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"m49_tgs_full_shadow_evidence", WORKER_ROOT / "build_tgs_full_shadow_evidence.py"
|
||||||
|
)
|
||||||
|
assert EVIDENCE_SPEC and EVIDENCE_SPEC.loader
|
||||||
|
EVIDENCE = importlib.util.module_from_spec(EVIDENCE_SPEC)
|
||||||
|
EVIDENCE_SPEC.loader.exec_module(EVIDENCE)
|
||||||
|
|
||||||
|
ARTIFACT_SPEC = importlib.util.spec_from_file_location(
|
||||||
|
"m49_tgs_full_shadow_artifact",
|
||||||
|
REPOSITORY_ROOT / "scripts" / "build_m49_tgs_full_shadow_worker_artifact.py",
|
||||||
|
)
|
||||||
|
assert ARTIFACT_SPEC and ARTIFACT_SPEC.loader
|
||||||
|
ARTIFACT = importlib.util.module_from_spec(ARTIFACT_SPEC)
|
||||||
|
ARTIFACT_SPEC.loader.exec_module(ARTIFACT)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_exact_multiset_and_costmap_priority() -> None:
|
||||||
|
native = np.asarray(
|
||||||
|
[[2.0, 0.0, 0.0, 0.0], [2.0, 0.0, 0.0, 0.0], [3.0, 0.0, 1.0, 0.0], [4.0, 0.0, 2.0, 0.0]],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
points, states = EVIDENCE.classify_exact_input(native, native[[0]], native[[1, 2]])
|
||||||
|
assert sorted(states.tolist()) == [1, 2, 2, 3]
|
||||||
|
grid = EVIDENCE.costmap_grid(12.0, 0.45)
|
||||||
|
cell_states, _ = EVIDENCE.rasterize(points, states, grid, 0.45)
|
||||||
|
assert np.count_nonzero(cell_states == 2) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_worker_artifact_is_deterministic_and_cpu_only(tmp_path: Path) -> None:
|
||||||
|
revision = "a" * 40
|
||||||
|
first = ARTIFACT.build("m49-tgs-full-test", tmp_path / "one", revision=revision)
|
||||||
|
second = ARTIFACT.build("m49-tgs-full-test", tmp_path / "two", revision=revision)
|
||||||
|
assert first["sha256"] == second["sha256"]
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||||
|
release = archive.extractfile("payload/release.json")
|
||||||
|
runner = archive.extractfile("payload/Invoke-M49TgsFullShadow.ps1")
|
||||||
|
assert release is not None and runner is not None
|
||||||
|
release_text = release.read().decode()
|
||||||
|
runner_text = runner.read().decode()
|
||||||
|
assert '"candidate_id": "travel-tgs-full-shadow"' in release_text
|
||||||
|
assert "--gpus" not in runner_text
|
||||||
|
assert "gpu_requested = $false" in runner_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_shadow_chunk_contract_keeps_missing_lidar_unobserved(
|
||||||
|
monkeypatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
frames = tuple(
|
||||||
|
{
|
||||||
|
"source_frame_index": index,
|
||||||
|
"session_seconds": index / 10,
|
||||||
|
"sample_available": index != 1,
|
||||||
|
"eligible_point_count": 0 if index == 1 else 1,
|
||||||
|
"ground_point_count": 0,
|
||||||
|
"nonground_point_count": 0 if index == 1 else 1,
|
||||||
|
"rejected_point_count": 0,
|
||||||
|
"occupied_cell_count": 0 if index == 1 else 1,
|
||||||
|
}
|
||||||
|
for index in range(4489)
|
||||||
|
)
|
||||||
|
centers = np.zeros((2244, 2), dtype=np.float32)
|
||||||
|
states = np.broadcast_to(np.zeros((1, 2244), dtype=np.uint8), (4489, 2244))
|
||||||
|
z_bounds = np.broadcast_to(
|
||||||
|
np.full((1, 2244, 2), np.nan, dtype=np.float32),
|
||||||
|
(4489, 2244, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(full_shadow_api, "_frames", lambda *_args: frames)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
full_shadow_api.np,
|
||||||
|
"load",
|
||||||
|
lambda path, **_kwargs: (
|
||||||
|
centers if "centers" in str(path) else states if "states" in str(path) else z_bounds
|
||||||
|
),
|
||||||
|
)
|
||||||
|
content = full_shadow_api._chunk_json_cached.__wrapped__(
|
||||||
|
str(tmp_path),
|
||||||
|
"m49-tgs-full-shadow-" + "a" * 64,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
(0, 0),
|
||||||
|
)
|
||||||
|
payload = json.loads(content)
|
||||||
|
assert payload["schema_version"] == "missioncore.m49-tgs-full-shadow-spatial-chunk/v1"
|
||||||
|
assert payload["count"] == 2
|
||||||
|
assert len(payload["costmap"]["centers_xy_m"]) == 2244
|
||||||
|
assert payload["frames"][1]["sample_available"] is False
|
||||||
|
assert set(payload["frames"][1]["states"]) == {0}
|
||||||
Reference in New Issue
Block a user