8 Commits
34 changed files with 2956 additions and 142 deletions
@@ -26,6 +26,7 @@ export interface LaboratoryMetricObstacleVisual {
state: "current" | "retained" | "held" | "expired"; state: "current" | "retained" | "held" | "expired";
centroidBodyXyzM: LaboratoryMetricPoint3; centroidBodyXyzM: LaboratoryMetricPoint3;
cellCentersBodyXyzM: readonly LaboratoryMetricPoint3[]; cellCentersBodyXyzM: readonly LaboratoryMetricPoint3[];
occupancySource?: "baseline" | "mixed" | "additive-low-step";
} }
export interface LaboratoryMetricRigVisual { export interface LaboratoryMetricRigVisual {
@@ -41,7 +42,7 @@ export interface LaboratoryMetricCorridorVisual {
} }
export interface LaboratoryMetricLegendEntry { export interface LaboratoryMetricLegendEntry {
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling"; id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling" | "low-step";
label: string; label: string;
} }
@@ -52,6 +53,7 @@ export function laboratoryMetricLegendEntries({
showCurrentIncrement, showCurrentIncrement,
showLocalSurface, showLocalSurface,
showRollingMap, showRollingMap,
showLowStep = true,
}: { }: {
pointCloudCount: number; pointCloudCount: number;
localSurfaceCount: number; localSurfaceCount: number;
@@ -59,19 +61,29 @@ export function laboratoryMetricLegendEntries({
showCurrentIncrement: boolean; showCurrentIncrement: boolean;
showLocalSurface: boolean; showLocalSurface: boolean;
showRollingMap: boolean; showRollingMap: boolean;
showLowStep?: boolean;
}): readonly LaboratoryMetricLegendEntry[] { }): readonly LaboratoryMetricLegendEntry[] {
const visibleObstacles = obstacles.filter((obstacle) => ( const visibleObstacles = obstacles.filter((obstacle) => (
obstacle.state === "current" (showLowStep || obstacle.occupancySource === undefined || obstacle.occupancySource === "baseline")
&& (obstacle.state === "current"
? showCurrentIncrement ? showCurrentIncrement
: obstacle.state === "retained" : obstacle.state === "retained"
? showRollingMap ? showRollingMap
: false : false)
)); ));
const decisions = new Set(visibleObstacles.map(({ decision }) => decision)); const decisions = new Set(visibleObstacles.map(({ decision }) => decision));
const entries: LaboratoryMetricLegendEntry[] = []; const entries: LaboratoryMetricLegendEntry[] = [];
if (decisions.has("threat")) entries.push({ id: "threat", label: "Угроза" }); if (decisions.has("threat")) entries.push({ id: "threat", label: "Угроза" });
if (decisions.has("not-threat")) entries.push({ id: "not-threat", label: "Вне коридора" }); if (decisions.has("not-threat")) entries.push({ id: "not-threat", label: "Вне коридора" });
if (decisions.has("unknown")) entries.push({ id: "unknown", label: "Неизвестно" }); if (decisions.has("unknown")) entries.push({ id: "unknown", label: "Неизвестно" });
if (
showLowStep
&& visibleObstacles.some((obstacle) => (
obstacle.occupancySource !== undefined && obstacle.occupancySource !== "baseline"
))
) {
entries.push({ id: "low-step", label: "LOW-STEP · добавлено системой" });
}
if (showCurrentIncrement && pointCloudCount > 0) { if (showCurrentIncrement && pointCloudCount > 0) {
entries.push({ id: "context", label: "Текущий кадр" }); entries.push({ id: "context", label: "Текущий кадр" });
} }
@@ -153,6 +165,16 @@ function decisionColor(
return tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92]); return tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92]);
} }
function obstacleColor(
host: HTMLElement,
obstacle: LaboratoryMetricObstacleVisual,
): THREE.Color {
if (obstacle.occupancySource !== "baseline") {
return tokenColor(host, "--nodedc-accent-rgb", [232, 56, 126]);
}
return decisionColor(host, obstacle.decision);
}
export interface LaboratoryMetricEvidenceSceneHandle { export interface LaboratoryMetricEvidenceSceneHandle {
resetView: () => void; resetView: () => void;
} }
@@ -171,6 +193,7 @@ LaboratoryMetricEvidenceSceneHandle,
showCurrentIncrement: boolean; showCurrentIncrement: boolean;
showLocalSurface: boolean; showLocalSurface: boolean;
showRollingMap: boolean; showRollingMap: boolean;
showLowStep?: boolean;
pointSemanticClassIds?: readonly (number | null)[]; pointSemanticClassIds?: readonly (number | null)[];
semanticClasses?: readonly RecordedEvidenceSemanticClass[]; semanticClasses?: readonly RecordedEvidenceSemanticClass[];
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[]; semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
@@ -187,6 +210,7 @@ LaboratoryMetricEvidenceSceneHandle,
showCurrentIncrement, showCurrentIncrement,
showLocalSurface, showLocalSurface,
showRollingMap, showRollingMap,
showLowStep = true,
pointSemanticClassIds, pointSemanticClassIds,
semanticClasses, semanticClasses,
semanticPalette, semanticPalette,
@@ -352,6 +376,12 @@ LaboratoryMetricEvidenceSceneHandle,
for (const obstacle of obstacles) { for (const obstacle of obstacles) {
if ( if (
(
!showLowStep
&& obstacle.occupancySource !== undefined
&& obstacle.occupancySource !== "baseline"
)
||
(obstacle.state === "current" && !showCurrentIncrement) (obstacle.state === "current" && !showCurrentIncrement)
|| (obstacle.state === "retained" && !showRollingMap) || (obstacle.state === "retained" && !showRollingMap)
|| obstacle.state === "held" || obstacle.state === "held"
@@ -359,7 +389,7 @@ LaboratoryMetricEvidenceSceneHandle,
) { ) {
continue; continue;
} }
const color = decisionColor(host, obstacle.decision); const color = obstacleColor(host, obstacle);
if (obstacle.state === "retained") { if (obstacle.state === "retained") {
const geometry = new THREE.BoxGeometry( const geometry = new THREE.BoxGeometry(
occupiedVoxelSizeM * 0.82, occupiedVoxelSizeM * 0.82,
@@ -424,6 +454,7 @@ LaboratoryMetricEvidenceSceneHandle,
showCurrentIncrement, showCurrentIncrement,
showLocalSurface, showLocalSurface,
showRollingMap, showRollingMap,
showLowStep,
]); ]);
useEffect(() => { useEffect(() => {
@@ -538,6 +569,7 @@ LaboratoryMetricEvidenceSceneHandle,
showCurrentIncrement, showCurrentIncrement,
showLocalSurface, showLocalSurface,
showRollingMap, showRollingMap,
showLowStep,
}); });
return ( return (
@@ -1,10 +1,18 @@
import { import {
Application, Application,
Asset, Asset,
BLEND_NORMAL,
Color, Color,
CULLFACE_NONE,
Entity, Entity,
FILLMODE_NONE,
Mat4,
RESOLUTION_AUTO,
StandardMaterial,
Vec2, Vec2,
Vec3, Vec3,
type ContainerResource,
type RenderComponent,
type ScriptType, type ScriptType,
} from "playcanvas"; } from "playcanvas";
import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs"; import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs";
@@ -12,18 +20,58 @@ import { CameraControls } from "playcanvas/scripts/esm/camera-controls.mjs";
import type { SimulationWorldManifest } from "../../core/simulation/projects"; import type { SimulationWorldManifest } from "../../core/simulation/projects";
export type SimulationViewMode = "visual" | "collision" | "combined"; export type SimulationViewMode = "visual" | "collision" | "combined";
export type SimulationQuality = "auto" | "low" | "medium" | "high"; export type SimulationQuality = "low" | "medium" | "high" | "ultra" | "maximum";
export type SimulationLayer = "visual" | "collision";
export const PLAYCANVAS_IDENTITY_TRANSFORM = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
export const PLAYCANVAS_X_180_TRANSFORM = [
1, 0, 0, 0,
0, -1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,
];
export interface SimulationRuntime { export interface SimulationRuntime {
mount(canvas: HTMLCanvasElement): Promise<void>; mount(canvas: HTMLCanvasElement): Promise<void>;
loadWorld(manifest: SimulationWorldManifest): Promise<void>; loadWorld(manifest: SimulationWorldManifest): Promise<void>;
setViewMode(mode: SimulationViewMode): void; setViewMode(mode: SimulationViewMode): Promise<void>;
setQuality(quality: SimulationQuality): void; setQuality(quality: SimulationQuality): void;
setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void;
home(): void;
focusBounds(): void; focusBounds(): void;
dispose(): void; dispose(): void;
} }
type CameraController = ScriptType & Pick<CameraControls, "reset" | "focus">; type DesktopCameraInput = {
read(): { mouse: number[] } & Record<string, number[]>;
};
type CameraController = ScriptType & Pick<CameraControls, "reset" | "focus"> & {
_desktopInput?: DesktopCameraInput;
};
const HOME_POSITION = new Vec3(0, 1, 0);
const HOME_FOCUS = new Vec3(1, 1, 0);
const QUALITY_PROFILES: Record<SimulationQuality, {
lodBaseDistance: number;
lodMultiplier: number;
lodRangeMin: number;
lodRangeMax: number;
pixelRatio: number;
}> = {
low: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 3, lodRangeMax: 5, pixelRatio: 0.75 },
medium: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 2, lodRangeMax: 5, pixelRatio: 1 },
high: { lodBaseDistance: 5, lodMultiplier: 3, lodRangeMin: 1, lodRangeMax: 5, pixelRatio: 1.5 },
ultra: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2 },
maximum: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 0, pixelRatio: 2 },
};
export class PlayCanvasRuntime implements SimulationRuntime { export class PlayCanvasRuntime implements SimulationRuntime {
private app: Application | null = null; private app: Application | null = null;
@@ -32,15 +80,21 @@ export class PlayCanvasRuntime implements SimulationRuntime {
private cameraController: CameraController | null = null; private cameraController: CameraController | null = null;
private visualEntity: Entity | null = null; private visualEntity: Entity | null = null;
private visualAsset: Asset | null = null; private visualAsset: Asset | null = null;
private collisionEntity: Entity | null = null;
private collisionAsset: Asset | null = null;
private collisionMaterial: StandardMaterial | null = null;
private collisionSource: { meshUrl: string; projectId: string; worldTransform: number[] } | null = null;
private collisionLoadPromise: Promise<void> | null = null;
private resizeObserver: ResizeObserver | null = null; private resizeObserver: ResizeObserver | null = null;
private viewMode: SimulationViewMode = "visual"; private viewMode: SimulationViewMode = "visual";
private quality: SimulationQuality = "auto"; private quality: SimulationQuality = "maximum";
async mount(canvas: HTMLCanvasElement): Promise<void> { async mount(canvas: HTMLCanvasElement): Promise<void> {
if (this.app) throw new Error("PlayCanvas runtime уже смонтирован."); if (this.app) throw new Error("PlayCanvas runtime уже смонтирован.");
this.canvas = canvas; this.canvas = canvas;
canvas.tabIndex = 0; canvas.tabIndex = 0;
canvas.addEventListener("contextmenu", preventContextMenu); canvas.addEventListener("contextmenu", preventContextMenu);
canvas.addEventListener("pointerdown", focusCanvas, true);
const app = new Application(canvas, { const app = new Application(canvas, {
graphicsDeviceOptions: { graphicsDeviceOptions: {
antialias: true, antialias: true,
@@ -50,6 +104,8 @@ export class PlayCanvasRuntime implements SimulationRuntime {
}, },
}); });
this.app = app; this.app = app;
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
app.setCanvasResolution(RESOLUTION_AUTO);
app.scene.ambientLight = new Color(0.35, 0.37, 0.42); app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
const camera = new Entity("SimulationCamera"); const camera = new Entity("SimulationCamera");
@@ -59,8 +115,8 @@ export class PlayCanvasRuntime implements SimulationRuntime {
farClip: 20_000, farClip: 20_000,
fov: 58, fov: 58,
}); });
camera.setPosition(5, 3, 5); camera.setPosition(HOME_POSITION);
camera.lookAt(0, 0, 0); camera.lookAt(HOME_FOCUS);
camera.addComponent("script"); camera.addComponent("script");
const controller = camera.script?.create(CameraControls, { const controller = camera.script?.create(CameraControls, {
properties: { properties: {
@@ -70,6 +126,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
zoomRange: new Vec2(0.05, 20_000), zoomRange: new Vec2(0.05, 20_000),
}, },
}) as CameraController | null; }) as CameraController | null;
if (controller) invertHorizontalCameraDrag(controller);
app.root.addChild(camera); app.root.addChild(camera);
this.camera = camera; this.camera = camera;
this.cameraController = controller; this.cameraController = controller;
@@ -103,6 +160,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
const ready = (loaded: Asset) => { const ready = (loaded: Asset) => {
const visual = new Entity("GaussianWorld"); const visual = new Entity("GaussianWorld");
visual.enabled = false; visual.enabled = false;
applyWorldTransform(visual, manifest.transforms.worldFromVisual);
app.root.addChild(visual); app.root.addChild(visual);
visual.addComponent("gsplat", { visual.addComponent("gsplat", {
asset: loaded, asset: loaded,
@@ -112,7 +170,6 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.visualEntity = visual; this.visualEntity = visual;
this.applyQuality(); this.applyQuality();
this.applyViewMode(); this.applyViewMode();
this.focusBounds();
resolve(); resolve();
}; };
const failed = (error: unknown) => { const failed = (error: unknown) => {
@@ -122,11 +179,24 @@ export class PlayCanvasRuntime implements SimulationRuntime {
asset.once("error", failed); asset.once("error", failed);
app.assets.load(asset); app.assets.load(asset);
}); });
if (manifest.collision.available && manifest.collision.meshUrl) {
this.collisionSource = {
meshUrl: manifest.collision.meshUrl,
projectId: manifest.projectId,
worldTransform: manifest.transforms.worldFromCollision,
};
}
this.applyViewMode();
this.home();
} }
setViewMode(mode: SimulationViewMode): void { async setViewMode(mode: SimulationViewMode): Promise<void> {
this.viewMode = mode; this.viewMode = mode;
this.applyViewMode(); this.applyViewMode();
if (mode !== "visual" && !this.collisionEntity) {
await this.ensureCollision();
this.applyViewMode();
}
} }
setQuality(quality: SimulationQuality): void { setQuality(quality: SimulationQuality): void {
@@ -134,6 +204,28 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.applyQuality(); this.applyQuality();
} }
setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void {
if (transform.length !== 16) {
throw new Error("World transform должен содержать матрицу 4×4.");
}
if (layer === "visual") {
if (this.visualEntity) applyWorldTransform(this.visualEntity, transform);
return;
}
if (this.collisionSource) this.collisionSource.worldTransform = [...transform];
if (this.collisionEntity) applyWorldTransform(this.collisionEntity, transform);
}
home(): void {
if (this.cameraController) {
this.cameraController.reset(HOME_FOCUS, HOME_POSITION);
} else if (this.camera) {
this.camera.setPosition(HOME_POSITION);
this.camera.lookAt(HOME_FOCUS);
}
this.canvas?.focus({ preventScroll: true });
}
focusBounds(): void { focusBounds(): void {
const bounds = this.visualEntity?.gsplat?.customAabb; const bounds = this.visualEntity?.gsplat?.customAabb;
const focus = bounds?.center.clone() ?? new Vec3(0, 0, 0); const focus = bounds?.center.clone() ?? new Vec3(0, 0, 0);
@@ -153,7 +245,10 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.resizeObserver = null; this.resizeObserver = null;
this.unloadWorld(); this.unloadWorld();
if (this.app) this.app.destroy(); if (this.app) this.app.destroy();
if (this.canvas) this.canvas.removeEventListener("contextmenu", preventContextMenu); if (this.canvas) {
this.canvas.removeEventListener("contextmenu", preventContextMenu);
this.canvas.removeEventListener("pointerdown", focusCanvas, true);
}
this.app = null; this.app = null;
this.canvas = null; this.canvas = null;
this.camera = null; this.camera = null;
@@ -175,22 +270,100 @@ export class PlayCanvasRuntime implements SimulationRuntime {
private applyViewMode(): void { private applyViewMode(): void {
if (this.visualEntity) { if (this.visualEntity) {
this.visualEntity.enabled = this.viewMode !== "collision"; this.visualEntity.enabled = this.viewMode !== "collision" || !this.collisionEntity;
}
if (this.collisionEntity) {
this.collisionEntity.enabled = this.viewMode !== "visual";
}
if (this.collisionMaterial) {
const combined = this.viewMode === "combined";
this.collisionMaterial.opacity = combined ? 0.34 : 0.82;
this.collisionMaterial.depthWrite = !combined;
this.collisionMaterial.update();
} }
} }
private applyQuality(): void { private applyQuality(): void {
const gsplat = this.visualEntity?.gsplat; const gsplat = this.visualEntity?.gsplat;
if (!gsplat) return; const profile = QUALITY_PROFILES[this.quality];
const profiles: Record<SimulationQuality, [number, number]> = { if (gsplat) {
auto: [5, 2], gsplat.lodBaseDistance = profile.lodBaseDistance;
low: [2.5, 1.7], gsplat.lodMultiplier = profile.lodMultiplier;
medium: [5, 2], gsplat.lodRangeMin = profile.lodRangeMin;
high: [9, 2.3], gsplat.lodRangeMax = profile.lodRangeMax;
}; }
const [baseDistance, multiplier] = profiles[this.quality]; if (this.app) {
gsplat.lodBaseDistance = baseDistance; const devicePixelRatio = window.devicePixelRatio || 1;
gsplat.lodMultiplier = multiplier; this.app.graphicsDevice.maxPixelRatio = Math.min(devicePixelRatio, profile.pixelRatio);
this.resize();
}
}
private async loadCollision(meshUrl: string, projectId: string, worldTransform: number[]): Promise<void> {
const app = this.requiredApp();
const asset = new Asset(
`SimulationCollision:${projectId}`,
"container",
{ url: meshUrl, filename: "scene.collision.glb" },
);
app.assets.add(asset);
this.collisionAsset = asset;
await new Promise<void>((resolve, reject) => {
asset.ready((loaded) => {
const resource = loaded.resource as ContainerResource | null;
if (!resource) {
reject(new Error("PlayCanvas не открыл collision GLB."));
return;
}
const entity = resource.instantiateRenderEntity({
castShadows: false,
receiveShadows: false,
});
entity.name = "CollisionWorld";
entity.enabled = false;
applyWorldTransform(entity, worldTransform);
app.root.addChild(entity);
const material = new StandardMaterial();
material.name = "SimulationCollisionMaterial";
material.diffuse = new Color(0.2, 0.95, 0.42);
material.emissive = new Color(0.08, 0.36, 0.14);
material.opacity = 0.34;
material.blendType = BLEND_NORMAL;
material.depthWrite = false;
material.cull = CULLFACE_NONE;
material.update();
for (const render of entity.findComponents("render") as RenderComponent[]) {
for (const meshInstance of render.meshInstances) {
meshInstance.material = material;
}
}
this.collisionEntity = entity;
this.collisionMaterial = material;
resolve();
});
asset.once("error", (error: unknown) => {
reject(new Error(error instanceof Error ? error.message : "PlayCanvas не загрузил collision GLB."));
});
app.assets.load(asset);
});
}
private async ensureCollision(): Promise<void> {
if (this.collisionEntity) return;
if (!this.collisionSource) {
throw new Error("Collision GLB отсутствует в world manifest.");
}
if (!this.collisionLoadPromise) {
this.collisionLoadPromise = this.loadCollision(
this.collisionSource.meshUrl,
this.collisionSource.projectId,
this.collisionSource.worldTransform,
).finally(() => {
this.collisionLoadPromise = null;
});
}
await this.collisionLoadPromise;
} }
private unloadWorld(): void { private unloadWorld(): void {
@@ -203,9 +376,46 @@ export class PlayCanvasRuntime implements SimulationRuntime {
this.visualAsset.unload(); this.visualAsset.unload();
this.visualAsset = null; this.visualAsset = null;
} }
if (this.collisionEntity) {
this.collisionEntity.destroy();
this.collisionEntity = null;
}
if (this.collisionAsset && this.app) {
this.app.assets.remove(this.collisionAsset);
this.collisionAsset.unload();
this.collisionAsset = null;
}
this.collisionMaterial?.destroy();
this.collisionMaterial = null;
this.collisionSource = null;
this.collisionLoadPromise = null;
} }
} }
function preventContextMenu(event: Event): void { function preventContextMenu(event: Event): void {
event.preventDefault(); event.preventDefault();
} }
function focusCanvas(event: Event): void {
(event.currentTarget as HTMLCanvasElement | null)?.focus({ preventScroll: true });
}
function invertHorizontalCameraDrag(controller: CameraController): void {
// CameraControls 2.21.4 deliberately exposes one pinned input source here.
// Keep its pointer-capture and fly/orbit behavior, changing only horizontal drag semantics.
const input = controller._desktopInput;
if (!input) return;
const read = input.read.bind(input);
input.read = () => {
const frame = read();
if (frame.mouse.length > 0) frame.mouse[0] *= -1;
return frame;
};
}
function applyWorldTransform(entity: Entity, values: number[]): void {
const transform = new Mat4().set(values);
entity.setLocalPosition(transform.getTranslation());
entity.setLocalEulerAngles(transform.getEulerAngles());
entity.setLocalScale(transform.getScale());
}
@@ -1,8 +1,19 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useId, useRef, useState } from "react";
import { ActivityIndicator, Button, SegmentedControl, StatusBadge } from "@nodedc/ui-react"; import {
ActivityIndicator,
Button,
Icon,
IconButton,
SegmentedControl,
Select,
StatusBadge,
Switch,
} from "@nodedc/ui-react";
import type { SimulationProject } from "../../core/simulation/projects"; import type { SimulationProject } from "../../core/simulation/projects";
import { import {
PLAYCANVAS_IDENTITY_TRANSFORM,
PLAYCANVAS_X_180_TRANSFORM,
PlayCanvasRuntime, PlayCanvasRuntime,
type SimulationQuality, type SimulationQuality,
type SimulationViewMode, type SimulationViewMode,
@@ -11,10 +22,25 @@ import {
export function SimulationViewport({ project }: { project: SimulationProject }) { export function SimulationViewport({ project }: { project: SimulationProject }) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const runtimeRef = useRef<PlayCanvasRuntime | null>(null); const runtimeRef = useRef<PlayCanvasRuntime | null>(null);
const settingsId = useId();
const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting"); const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<SimulationViewMode>("visual"); const [viewMode, setViewMode] = useState<SimulationViewMode>("visual");
const [quality, setQuality] = useState<SimulationQuality>("auto"); const [quality, setQuality] = useState<SimulationQuality>("maximum");
const [collisionState, setCollisionState] = useState<"idle" | "loading" | "ready" | "failed">("idle");
const [settingsOpen, setSettingsOpen] = useState(false);
const [visualInverted, setVisualInverted] = useState(
() => isX180Transform(project.worldManifest?.transforms.worldFromVisual),
);
const [collisionInverted, setCollisionInverted] = useState(
() => isX180Transform(project.worldManifest?.transforms.worldFromCollision),
);
useEffect(() => {
setVisualInverted(isX180Transform(project.worldManifest?.transforms.worldFromVisual));
setCollisionInverted(isX180Transform(project.worldManifest?.transforms.worldFromCollision));
setSettingsOpen(false);
}, [project.projectId, project.worldManifest]);
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
@@ -48,17 +74,53 @@ export function SimulationViewport({ project }: { project: SimulationProject })
<section className="simulation-viewport" aria-label={`Сцена ${project.name}`}> <section className="simulation-viewport" aria-label={`Сцена ${project.name}`}>
<header className="simulation-viewport__toolbar"> <header className="simulation-viewport__toolbar">
<div> <div>
<StatusBadge tone={state === "ready" ? "success" : state === "failed" ? "warning" : "accent"}> <StatusBadge tone={state === "failed" || collisionState === "failed" ? "warning" : state === "ready" && collisionState !== "loading" ? "success" : "accent"}>
{state === "ready" ? "Runtime готов" : state === "failed" ? "Ошибка runtime" : "Загрузка сцены"} {collisionState === "loading"
? "Загрузка collision"
: collisionState === "failed"
? "Ошибка collision"
: state === "ready"
? "Runtime готов"
: state === "failed"
? "Ошибка runtime"
: "Загрузка сцены"}
</StatusBadge> </StatusBadge>
<span>PlayCanvas Engine 2.21.4</span> <span>PlayCanvas Engine 2.21.4</span>
</div> </div>
<div className="simulation-viewport__controls">
<Select
label="Качество Gaussian-сцены"
value={quality}
onChange={(next) => {
setQuality(next);
runtimeRef.current?.setQuality(next);
}}
options={[
{ value: "maximum", label: "Максимум", description: "Только полный LOD 0, Retina до 2×" },
{ value: "ultra", label: "Ультра", description: "LOD 0 и Retina до 2×" },
{ value: "high", label: "Высокое", description: "LOD 1 и Retina до 1,5×" },
{ value: "medium", label: "Среднее", description: "LOD 2 и обычное разрешение" },
{ value: "low", label: "Низкое", description: "LOD 3 для слабых устройств" },
]}
minMenuWidth={230}
menuWidth={230}
/>
<SegmentedControl <SegmentedControl
label="Слой сцены" label="Слой сцены"
value={viewMode} value={viewMode}
onChange={(next) => { onChange={(next) => {
setViewMode(next); setViewMode(next);
runtimeRef.current?.setViewMode(next); const runtime = runtimeRef.current;
if (!runtime) return;
if (next !== "visual" && collisionAvailable && collisionState !== "ready") {
setCollisionState("loading");
}
void runtime.setViewMode(next).then(() => {
if (next !== "visual") setCollisionState("ready");
}).catch((caught: unknown) => {
setCollisionState("failed");
setError(caught instanceof Error ? caught.message : "Не удалось открыть collision GLB.");
});
}} }}
items={[ items={[
{ value: "visual", label: "Визуал" }, { value: "visual", label: "Визуал" },
@@ -66,23 +128,70 @@ export function SimulationViewport({ project }: { project: SimulationProject })
{ value: "combined", label: "Вместе", disabled: !collisionAvailable }, { value: "combined", label: "Вместе", disabled: !collisionAvailable },
]} ]}
/> />
<SegmentedControl <Button size="compact" variant="secondary" onClick={() => runtimeRef.current?.home()}>
label="Качество Streamed SOG" Домой
value={quality}
onChange={(next) => {
setQuality(next);
runtimeRef.current?.setQuality(next);
}}
items={[
{ value: "auto", label: "Auto" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Med" },
{ value: "high", label: "High" },
]}
/>
<Button size="compact" variant="secondary" onClick={() => runtimeRef.current?.focusBounds()}>
Вписать сцену
</Button> </Button>
<div className="simulation-viewport__settings-anchor">
<IconButton
label="Настройки системы координат"
aria-controls={settingsId}
aria-expanded={settingsOpen}
aria-pressed={settingsOpen}
onClick={() => setSettingsOpen((open) => !open)}
>
<Icon name="settings" size={18} />
</IconButton>
{settingsOpen ? (
<div
id={settingsId}
className="simulation-viewport__settings"
role="dialog"
aria-label="Настройки системы координат"
>
<div className="simulation-viewport__settings-head">
<div>
<strong>Система координат</strong>
<span>Мир PlayCanvas · Y вверх</span>
</div>
<IconButton label="Закрыть настройки" onClick={() => setSettingsOpen(false)}>
<Icon name="close" size={16} />
</IconButton>
</div>
<p>
Коррекция применяется к слоям независимо и не меняет координаты камеры,
навигации и будущей физики.
</p>
<div className="simulation-viewport__settings-switches">
<Switch
checked={visualInverted}
disabled={state !== "ready"}
label="Инверсия визуального слоя"
onChange={(checked) => {
setVisualInverted(checked);
runtimeRef.current?.setLayerWorldTransform(
"visual",
checked ? PLAYCANVAS_X_180_TRANSFORM : PLAYCANVAS_IDENTITY_TRANSFORM,
);
}}
/>
<Switch
checked={collisionInverted}
disabled={state !== "ready"}
label="Инверсия collision-слоя"
onChange={(checked) => {
setCollisionInverted(checked);
runtimeRef.current?.setLayerWorldTransform(
"collision",
checked ? PLAYCANVAS_X_180_TRANSFORM : PLAYCANVAS_IDENTITY_TRANSFORM,
);
}}
/>
</div>
</div>
) : null}
</div>
</div>
<span className="simulation-viewport__toolbar-balance" aria-hidden="true" />
</header> </header>
<div className="simulation-viewport__stage"> <div className="simulation-viewport__stage">
<canvas ref={canvasRef} aria-label={`PlayCanvas сцена ${project.name}`} /> <canvas ref={canvasRef} aria-label={`PlayCanvas сцена ${project.name}`} />
@@ -99,7 +208,17 @@ export function SimulationViewport({ project }: { project: SimulationProject })
<StatusBadge tone="warning">Collision недоступен</StatusBadge> <StatusBadge tone="warning">Collision недоступен</StatusBadge>
<span>Для этой сборки collision GLB не был запрошен; визуальный слой настоящий и не подменяется.</span> <span>Для этой сборки collision GLB не был запрошен; визуальный слой настоящий и не подменяется.</span>
</footer> </footer>
) : collisionState === "failed" ? (
<footer className="simulation-viewport__notice" role="alert">
<StatusBadge tone="warning">Collision не открылся</StatusBadge>
<span>{error ?? "PlayCanvas не смог загрузить collision GLB."}</span>
</footer>
) : null} ) : null}
</section> </section>
); );
} }
function isX180Transform(transform: number[] | undefined): boolean {
if (!transform || transform.length !== 16) return false;
return transform.every((value, index) => value === PLAYCANVAS_X_180_TRANSFORM[index]);
}
@@ -43,6 +43,7 @@ import {
} from "./m48ObjectCentricQuality"; } from "./m48ObjectCentricQuality";
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression"; import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualification"; import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualification";
import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector"; import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality"; import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
@@ -50,6 +51,7 @@ export type AdvancedLaboratoryWorkId =
| "m48-object-centric-quality" | "m48-object-centric-quality"
| "m48-small-static-passage-regression" | "m48-small-static-passage-regression"
| "m48-static-occupancy-qualification" | "m48-static-occupancy-qualification"
| "m48r3-static-occupancy-shadow"
| "m48s-fixed-class-detector" | "m48s-fixed-class-detector"
| "m48t-risk-quality-temporal" | "m48t-risk-quality-temporal"
| "m47-reference-graph-shadow" | "m47-reference-graph-shadow"
@@ -97,6 +99,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"m48-object-centric-quality", "m48-object-centric-quality",
"m48-small-static-passage-regression", "m48-small-static-passage-regression",
"m48-static-occupancy-qualification", "m48-static-occupancy-qualification",
"m48r3-static-occupancy-shadow",
"m48s-fixed-class-detector", "m48s-fixed-class-detector",
"m48t-risk-quality-temporal", "m48t-risk-quality-temporal",
"m47-reference-graph-shadow", "m47-reference-graph-shadow",
@@ -139,6 +142,7 @@ const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)", "m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
"m48-small-static-passage-regression": "m48-small-static-passage-regression", "m48-small-static-passage-regression": "m48-small-static-passage-regression",
"m48-static-occupancy-qualification": "m48-static-occupancy-qualification", "m48-static-occupancy-qualification": "m48-static-occupancy-qualification",
"m48r3-static-occupancy-shadow": "m48r3-static-occupancy-shadow",
"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)",
"m47-reference-graph-shadow": "m47-reference-graph-lab", "m47-reference-graph-shadow": "m47-reference-graph-lab",
@@ -189,6 +193,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
m48: null, m48: null,
m48SmallStatic: null, m48SmallStatic: null,
m48StaticOccupancy: null, m48StaticOccupancy: null,
m48r3StaticOccupancy: null,
m48s: null, m48s: null,
m48t: null, m48t: null,
m4Threat: null, m4Threat: null,
@@ -318,6 +323,7 @@ export function advancedLaboratoryResultAvailable(
return workId === "m48-object-centric-quality" ? results.m48 !== null return workId === "m48-object-centric-quality" ? results.m48 !== null
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null : workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null : workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
: workId === "m48r3-static-occupancy-shadow" ? results.m48r3StaticOccupancy !== null
: 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 === "m47-reference-graph-shadow" ? results.m47Graph !== null : workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
@@ -378,6 +384,9 @@ export async function fetchAdvancedLaboratoryResult(
} else if (workId === "m48-static-occupancy-qualification") { } else if (workId === "m48-static-occupancy-qualification") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R2 qualification identity не выбрана."); if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R2 qualification identity не выбрана.");
results.m48StaticOccupancy = await fetchM48StaticOccupancyQualification(resultId, { fetcher, signal }); results.m48StaticOccupancy = await fetchM48StaticOccupancyQualification(resultId, { fetcher, signal });
} else if (workId === "m48r3-static-occupancy-shadow") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R3 Worker shadow identity не выбрана.");
results.m48r3StaticOccupancy = await fetchM48R3StaticOccupancyShadow(resultId, { fetcher, signal });
} else if (workId === "m48s-fixed-class-detector") { } else if (workId === "m48s-fixed-class-detector") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана."); if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal }); results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
@@ -37,6 +37,7 @@ import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
import type { M48AdvancedResult } from "./m48ObjectCentricQuality"; import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression"; import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancyQualification"; import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancyQualification";
import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancyShadow";
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector"; import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import type { M48TRiskQualityResult } from "./m48tRiskQuality"; import type { M48TRiskQualityResult } from "./m48tRiskQuality";
@@ -45,6 +46,7 @@ export interface AdvancedLaboratoryResults {
m48: M48AdvancedResult | null; m48: M48AdvancedResult | null;
m48SmallStatic: M48SmallStaticRegressionResult | null; m48SmallStatic: M48SmallStaticRegressionResult | null;
m48StaticOccupancy: M48StaticOccupancyQualificationResult | null; m48StaticOccupancy: M48StaticOccupancyQualificationResult | null;
m48r3StaticOccupancy: M48R3StaticOccupancyShadowResult | null;
m48s: M48SFixedClassDetectorResult | null; m48s: M48SFixedClassDetectorResult | null;
m48t: M48TRiskQualityResult | null; m48t: M48TRiskQualityResult | null;
m4Threat: M4ThreatReplayResult | null; m4Threat: M4ThreatReplayResult | null;
@@ -968,6 +968,7 @@ export async function fetchAdvancedLaboratoryResults({
const e40 = settledCatalogValue(settled[8]); const e40 = settledCatalogValue(settled[8]);
return { return {
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null, m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
m48r3StaticOccupancy: null,
m48s: null, m48t: null, m4Threat: null, m48s: null, m48t: null, m4Threat: null,
l3: null, l31: null, l32: null, l33: null, l3: null, l31: null, l32: null, l33: null,
e31, e31,
@@ -0,0 +1,343 @@
import type { LaboratoryFetch } from "./advancedResults";
import type { M48Authority } from "./m48ObjectCentricQuality";
const RESULT_ID = /^m48r3-static-occupancy-shadow-[a-f0-9]{64}$/;
interface M48R3WorkerPerformance {
admittedFrames: number;
deliveredFrames: number;
fps: number;
worldP95Ms: number;
worldP99Ms: number;
geometryP95Ms: number;
geometryP99Ms: number;
}
export interface M48R3StaticOccupancyShadowResult {
resultId: string;
createdAtUtc: string;
accepted: boolean;
profile: {
id: string;
sha256: string;
minimumPoints: number;
};
metrics: {
frames: { expected: number; baselineDelivered: number; candidateDelivered: number };
performance: {
baseline: M48R3WorkerPerformance;
candidate: M48R3WorkerPerformance;
fpsRegressionFraction: number;
worldStateP95DeltaMs: number;
};
occupancy: {
baselineCellTotal: number;
candidateCellTotal: number;
addedCellTotal: number;
lostCellTotal: number;
baselineComponentTotal: number;
candidateComponentTotal: number;
meanCellGrowthFraction: number;
meanComponentGrowthFraction: number;
maximumAddedCellsPerFrame: number;
maximumCandidateComponentsPerFrame: number;
};
provider: {
additiveObservationCount: number;
additiveVoxelCount: number;
framesWithAdditions: number;
additiveMeanMsPerFrame: number;
peakTemporalComponents: number;
peakRollingCells: number;
};
anchors: {
count: number;
criticalNearCount: number;
criticalNearRecall: number;
matchedCount: number;
canonicalEngineeringRecall: number;
separation: readonly {
displayFrame: number;
expectedMinimumComponents: number;
observedComponents: number;
passed: boolean;
interpretation: string;
}[];
};
capacityDropCount: number;
falseFreeCount: number;
};
gates: Readonly<Record<string, boolean>>;
decision: {
state: "accepted-bounded-worker-shadow" | "rejected-bounded-worker-shadow";
candidateAccepted: boolean;
productionAccepted: false;
nextAction: string;
};
limitations: readonly string[];
authority: M48Authority;
}
export interface M48R3StaticOccupancyCase {
anchorId: string;
displayFrame: number;
sourceSequence: number;
extentXyxy: readonly [number, number, number, number];
distanceBand: "critical-near" | "approach" | "outside-qualified-bands";
componentCount: number;
matched: boolean;
}
export class M48R3StaticOccupancyContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new M48R3StaticOccupancyContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new M48R3StaticOccupancyContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new M48R3StaticOccupancyContractError(`${label}: ожидалось число.`);
}
return value;
}
function integer(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new M48R3StaticOccupancyContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function bool(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new M48R3StaticOccupancyContractError(`${label}: ожидался флаг.`);
}
return value;
}
function exact(value: unknown, expected: string | boolean, label: string): void {
if (value !== expected) {
throw new M48R3StaticOccupancyContractError(`${label}: нарушен контракт.`);
}
}
function sha256(value: unknown, label: string): string {
const parsed = text(value, label);
if (!/^[a-f0-9]{64}$/.test(parsed)) {
throw new M48R3StaticOccupancyContractError(`${label}: неверный SHA-256.`);
}
return parsed;
}
function extent(value: unknown): readonly [number, number, number, number] {
if (!Array.isArray(value) || value.length !== 4) {
throw new M48R3StaticOccupancyContractError("M4.8R3 extent: нарушен контракт.");
}
const parsed = value.map((item) => numberValue(item, "M4.8R3 extent"));
return [parsed[0]!, parsed[1]!, parsed[2]!, parsed[3]!];
}
function authority(value: unknown): M48Authority {
const row = objectValue(value, "M4.8R3 authority");
exact(row.mode, "replay-simulated", "M4.8R3 authority.mode");
exact(row.physical_live, false, "M4.8R3 authority.physical_live");
exact(row.commands_enabled, false, "M4.8R3 authority.commands_enabled");
exact(row.actuation_allowed, false, "M4.8R3 authority.actuation_allowed");
exact(
row.navigation_or_safety_accepted,
false,
"M4.8R3 authority.navigation_or_safety_accepted",
);
return {
mode: "replay-simulated",
physicalLive: false,
commandsEnabled: false,
actuationAllowed: false,
navigationOrSafetyAccepted: false,
};
}
function workerPerformance(value: unknown, label: string): M48R3WorkerPerformance {
const row = objectValue(value, label);
return {
admittedFrames: integer(row.admitted_frames, `${label}.admitted_frames`),
deliveredFrames: integer(row.delivered_frames, `${label}.delivered_frames`),
fps: numberValue(row.fps, `${label}.fps`),
worldP95Ms: numberValue(row.world_p95_ms, `${label}.world_p95_ms`),
worldP99Ms: numberValue(row.world_p99_ms, `${label}.world_p99_ms`),
geometryP95Ms: numberValue(row.geometry_p95_ms, `${label}.geometry_p95_ms`),
geometryP99Ms: numberValue(row.geometry_p99_ms, `${label}.geometry_p99_ms`),
};
}
export async function fetchM48R3StaticOccupancyShadow(
resultId: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M48R3StaticOccupancyShadowResult> {
if (!RESULT_ID.test(resultId)) {
throw new M48R3StaticOccupancyContractError("M4.8R3 identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/m48r3/static-occupancy/${encodeURIComponent(resultId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new M48R3StaticOccupancyContractError(`M4.8R3 недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "M4.8R3");
exact(
payload.schema_version,
"missioncore.m48r3-static-occupancy-shadow-view/v1",
"M4.8R3 schema",
);
exact(payload.result_id, resultId, "M4.8R3 result");
exact(payload.ground_truth, false, "M4.8R3 ground truth");
const profile = objectValue(payload.profile, "M4.8R3 profile");
const componentization = objectValue(profile.componentization, "M4.8R3 componentization");
const metrics = objectValue(payload.metrics, "M4.8R3 metrics");
const frames = objectValue(metrics.frames, "M4.8R3 frames");
const performance = objectValue(metrics.performance, "M4.8R3 performance");
const occupancy = objectValue(metrics.occupancy, "M4.8R3 occupancy");
const provider = objectValue(metrics.provider, "M4.8R3 provider");
const anchors = objectValue(metrics.assisted_anchors, "M4.8R3 anchors");
if (!Array.isArray(anchors.separation)) {
throw new M48R3StaticOccupancyContractError("M4.8R3 separation: ожидался массив.");
}
const gates = objectValue(payload.gates, "M4.8R3 gates");
const parsedGates = Object.fromEntries(
Object.entries(gates).map(([key, value]) => [key, bool(value, `M4.8R3 gate ${key}`)]),
);
const decision = objectValue(payload.decision, "M4.8R3 decision");
const state = text(decision.state, "M4.8R3 decision.state");
if (state !== "accepted-bounded-worker-shadow" && state !== "rejected-bounded-worker-shadow") {
throw new M48R3StaticOccupancyContractError("M4.8R3 decision.state: неизвестное состояние.");
}
exact(decision.production_accepted, false, "M4.8R3 production acceptance");
if (!Array.isArray(payload.limitations)) {
throw new M48R3StaticOccupancyContractError("M4.8R3 limitations: ожидался массив.");
}
return {
resultId,
createdAtUtc: text(payload.created_at_utc, "M4.8R3 created"),
accepted: bool(payload.accepted, "M4.8R3 accepted"),
profile: {
id: text(profile.id, "M4.8R3 profile id"),
sha256: sha256(profile.sha256, "M4.8R3 profile sha"),
minimumPoints: integer(componentization.minimum_points, "M4.8R3 minimum points"),
},
metrics: {
frames: {
expected: integer(frames.expected, "M4.8R3 expected frames"),
baselineDelivered: integer(frames.baseline_delivered, "M4.8R3 baseline frames"),
candidateDelivered: integer(frames.candidate_delivered, "M4.8R3 candidate frames"),
},
performance: {
baseline: workerPerformance(performance.baseline, "M4.8R3 baseline"),
candidate: workerPerformance(performance.candidate, "M4.8R3 candidate"),
fpsRegressionFraction: numberValue(performance.fps_regression_fraction, "M4.8R3 FPS regression"),
worldStateP95DeltaMs: numberValue(performance.world_state_p95_delta_ms, "M4.8R3 p95 delta"),
},
occupancy: {
baselineCellTotal: integer(occupancy.baseline_cell_total, "M4.8R3 baseline cells"),
candidateCellTotal: integer(occupancy.candidate_cell_total, "M4.8R3 candidate cells"),
addedCellTotal: integer(occupancy.added_cell_total, "M4.8R3 added cells"),
lostCellTotal: integer(occupancy.lost_cell_total, "M4.8R3 lost cells"),
baselineComponentTotal: integer(occupancy.baseline_component_total, "M4.8R3 baseline components"),
candidateComponentTotal: integer(occupancy.candidate_component_total, "M4.8R3 candidate components"),
meanCellGrowthFraction: numberValue(occupancy.mean_cell_growth_fraction, "M4.8R3 cell growth"),
meanComponentGrowthFraction: numberValue(occupancy.mean_component_growth_fraction, "M4.8R3 component growth"),
maximumAddedCellsPerFrame: integer(occupancy.maximum_added_cells_per_frame, "M4.8R3 maximum added cells"),
maximumCandidateComponentsPerFrame: integer(occupancy.maximum_candidate_components_per_frame, "M4.8R3 maximum components"),
},
provider: {
additiveObservationCount: integer(provider.additive_observation_count, "M4.8R3 observations"),
additiveVoxelCount: integer(provider.additive_voxel_count, "M4.8R3 voxels"),
framesWithAdditions: integer(provider.frames_with_additions, "M4.8R3 added frames"),
additiveMeanMsPerFrame: numberValue(provider.additive_mean_ms_per_frame, "M4.8R3 additive mean"),
peakTemporalComponents: integer(provider.peak_temporal_components, "M4.8R3 temporal peak"),
peakRollingCells: integer(provider.peak_rolling_cells, "M4.8R3 rolling peak"),
},
anchors: {
count: integer(anchors.count, "M4.8R3 anchor count"),
criticalNearCount: integer(anchors.critical_near_count, "M4.8R3 near anchors"),
criticalNearRecall: numberValue(anchors.critical_near_recall, "M4.8R3 near recall"),
matchedCount: integer(anchors.matched_count, "M4.8R3 matched anchors"),
canonicalEngineeringRecall: numberValue(anchors.canonical_engineering_recall, "M4.8R3 canonical recall"),
separation: anchors.separation.map((value, index) => {
const row = objectValue(value, `M4.8R3 separation ${index}`);
return {
displayFrame: integer(row.display_frame, "M4.8R3 separation frame"),
expectedMinimumComponents: integer(row.expected_minimum_components, "M4.8R3 expected components"),
observedComponents: integer(row.observed_components, "M4.8R3 observed components"),
passed: bool(row.passed, "M4.8R3 separation passed"),
interpretation: text(row.interpretation, "M4.8R3 separation interpretation"),
};
}),
},
capacityDropCount: integer(metrics.capacity_drop_count, "M4.8R3 capacity drops"),
falseFreeCount: integer(metrics.false_free_count, "M4.8R3 false free"),
},
gates: parsedGates,
decision: {
state,
candidateAccepted: bool(decision.candidate_accepted, "M4.8R3 candidate accepted"),
productionAccepted: false,
nextAction: text(decision.next_action, "M4.8R3 next action"),
},
limitations: payload.limitations.map((value, index) => text(value, `M4.8R3 limitation ${index}`)),
authority: authority(payload.authority),
};
}
export async function fetchM48R3StaticOccupancyCases(
resultId: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<readonly M48R3StaticOccupancyCase[]> {
if (!RESULT_ID.test(resultId)) {
throw new M48R3StaticOccupancyContractError("M4.8R3 identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/m48r3/static-occupancy/${encodeURIComponent(resultId)}/cases`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new M48R3StaticOccupancyContractError(`M4.8R3 cases недоступны: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "M4.8R3 cases");
exact(
payload.schema_version,
"missioncore.m48r3-static-occupancy-shadow-cases/v1",
"M4.8R3 cases schema",
);
exact(payload.result_id, resultId, "M4.8R3 cases result");
if (!Array.isArray(payload.cases) || payload.cases.length !== integer(payload.case_count, "M4.8R3 case count")) {
throw new M48R3StaticOccupancyContractError("M4.8R3 cases: нарушен размер.");
}
return payload.cases.map((value, index) => {
const row = objectValue(value, `M4.8R3 case ${index}`);
const distanceBand = text(row.distance_band, "M4.8R3 distance band");
if (distanceBand !== "critical-near" && distanceBand !== "approach" && distanceBand !== "outside-qualified-bands") {
throw new M48R3StaticOccupancyContractError("M4.8R3 distance band: неизвестное значение.");
}
return {
anchorId: text(row.anchor_id, "M4.8R3 anchor id"),
displayFrame: integer(row.display_frame, "M4.8R3 display frame"),
sourceSequence: integer(row.source_sequence, "M4.8R3 source sequence"),
extentXyxy: extent(row.extent_xyxy),
distanceBand,
componentCount: integer(row.component_count, "M4.8R3 component count"),
matched: bool(row.matched, "M4.8R3 matched"),
};
});
}
@@ -1,5 +1,6 @@
export type M4ThreatDecision = "threat" | "not-threat" | "unknown"; export type M4ThreatDecision = "threat" | "not-threat" | "unknown";
export type M4ThreatMotion = "moving" | "stationary" | "unknown"; export type M4ThreatMotion = "moving" | "stationary" | "unknown";
export type M4OccupancySource = "baseline" | "mixed" | "additive-low-step";
export type M4Point3 = readonly [number, number, number]; export type M4Point3 = readonly [number, number, number];
export type M4Matrix3 = readonly [M4Point3, M4Point3, M4Point3]; export type M4Matrix3 = readonly [M4Point3, M4Point3, M4Point3];
@@ -76,6 +77,7 @@ export interface M4ThreatMetricVisual {
centroidBodyXyzM: M4Point3; centroidBodyXyzM: M4Point3;
cellCentersBodyXyzM: readonly M4Point3[]; cellCentersBodyXyzM: readonly M4Point3[];
assessment: M4ThreatAssessment; assessment: M4ThreatAssessment;
occupancySource: M4OccupancySource;
} }
export interface M4ThreatCameraProposal { export interface M4ThreatCameraProposal {
@@ -174,6 +176,7 @@ export interface M4ThreatTimeline {
cameraPointWindowSeconds: number; cameraPointWindowSeconds: number;
cameraPointSampleLimit: number; cameraPointSampleLimit: number;
worldStateDelivery: "source-paced-latest-wins" | null; worldStateDelivery: "source-paced-latest-wins" | null;
occupancyProvenanceDelivery: "baseline-versus-additive-component-diff" | null;
worldStateFrameCount: number; worldStateFrameCount: number;
supersededFrameCount: number; supersededFrameCount: number;
sourceRepresentationId: "registered-map-increment-v1"; sourceRepresentationId: "registered-map-increment-v1";
@@ -331,6 +334,9 @@ function parseMetricVisual(value: unknown): M4ThreatMetricVisual {
) { ) {
throw new M4ThreatContractError("M4.6 temporal state: неизвестное состояние."); throw new M4ThreatContractError("M4.6 temporal state: неизвестное состояние.");
} }
const occupancySource = item.occupancy_source === undefined
? "baseline"
: memberOccupancySource(item.occupancy_source);
return { return {
componentId: text(item.component_id, "M4.6 visual component"), componentId: text(item.component_id, "M4.6 visual component"),
state, state,
@@ -340,9 +346,17 @@ function parseMetricVisual(value: unknown): M4ThreatMetricVisual {
(point) => vector(point, 3, "M4.6 cell") as [number, number, number], (point) => vector(point, 3, "M4.6 cell") as [number, number, number],
), ),
assessment: parseAssessment(item.assessment), assessment: parseAssessment(item.assessment),
occupancySource,
}; };
} }
function memberOccupancySource(value: unknown): M4OccupancySource {
if (value !== "baseline" && value !== "mixed" && value !== "additive-low-step") {
throw new M4ThreatContractError("M4.8R3 occupancy source: неизвестное состояние.");
}
return value;
}
export async function fetchM4ThreatReplayResult({ export async function fetchM4ThreatReplayResult({
resultId: requestedResultId, resultId: requestedResultId,
fetcher = fetch, fetcher = fetch,
@@ -683,6 +697,13 @@ export async function fetchM4ThreatTimeline(
"source-paced-latest-wins", "source-paced-latest-wins",
"M4.6 world-state delivery", "M4.6 world-state delivery",
), ),
occupancyProvenanceDelivery: payload.occupancy_provenance_delivery == null
? null
: exact(
payload.occupancy_provenance_delivery,
"baseline-versus-additive-component-diff",
"M4.8R3 occupancy provenance",
),
worldStateFrameCount: payload.world_state_frame_count === undefined worldStateFrameCount: payload.world_state_frame_count === undefined
? frameCount ? frameCount
: integer(payload.world_state_frame_count, "M4.6 world-state frames"), : integer(payload.world_state_frame_count, "M4.6 world-state frames"),
@@ -334,6 +334,12 @@
background: transparent; background: transparent;
} }
.laboratory-metric-evidence-scene__legend span[data-decision="low-step"]::before {
box-sizing: border-box;
border: 1px solid rgb(var(--nodedc-foreground-rgb));
background: rgb(var(--nodedc-accent-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="local-surface"]::before { .laboratory-metric-evidence-scene__legend span[data-decision="local-surface"]::before {
background: rgb(var(--nodedc-accent-rgb)); background: rgb(var(--nodedc-accent-rgb));
opacity: 0.72; opacity: 0.72;
+97 -1
View File
@@ -332,6 +332,7 @@
} }
.simulation-viewport { .simulation-viewport {
container-type: inline-size;
display: grid; display: grid;
min-height: 0; min-height: 0;
grid-template-rows: auto minmax(0, 1fr) auto; grid-template-rows: auto minmax(0, 1fr) auto;
@@ -341,7 +342,9 @@
} }
.simulation-viewport__toolbar { .simulation-viewport__toolbar {
flex-wrap: wrap; display: grid;
grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 1fr);
align-items: center;
gap: 0.55rem; gap: 0.55rem;
border-bottom: 1px solid var(--station-hairline); border-bottom: 1px solid var(--station-hairline);
background: var(--nodedc-glass-panel-bg-soft); background: var(--nodedc-glass-panel-bg-soft);
@@ -356,6 +359,74 @@
gap: 0.45rem; gap: 0.45rem;
} }
.simulation-viewport__controls {
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
gap: 0.55rem;
}
.simulation-viewport__settings-anchor {
position: relative;
display: grid;
place-items: center;
}
.simulation-viewport__settings {
position: absolute;
z-index: 12;
top: calc(100% + 0.65rem);
right: 0;
display: grid;
width: min(19rem, calc(100cqw - 1.3rem));
gap: 0.75rem;
border-radius: 0.9rem;
background: rgb(25 27 31 / 0.98);
box-shadow: 0 1rem 3rem rgb(0 0 0 / 0.42);
padding: 0.8rem;
}
.simulation-viewport__settings-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.simulation-viewport__settings-head > div,
.simulation-viewport__settings-switches {
display: grid;
gap: 0.32rem;
}
.simulation-viewport__settings strong {
color: var(--nodedc-text-primary);
font-size: 0.72rem;
}
.simulation-viewport__settings span,
.simulation-viewport__settings p {
color: var(--nodedc-text-muted);
font-size: 0.56rem;
line-height: 1.45;
}
.simulation-viewport__settings p {
margin: 0;
}
.simulation-viewport__settings-switches {
gap: 0.55rem;
border-radius: 0.72rem;
background: rgb(255 255 255 / 0.035);
padding: 0.65rem;
}
.simulation-viewport__toolbar-balance {
min-width: 0;
}
.simulation-viewport__toolbar > div:first-child > span:last-child, .simulation-viewport__toolbar > div:first-child > span:last-child,
.simulation-viewport__notice > span:last-child { .simulation-viewport__notice > span:last-child {
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
@@ -364,14 +435,24 @@
.simulation-viewport__stage { .simulation-viewport__stage {
position: relative; position: relative;
min-width: 0;
min-height: 30rem; min-height: 30rem;
overflow: hidden;
} }
.simulation-viewport__stage canvas { .simulation-viewport__stage canvas {
display: block; display: block;
max-width: 100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
outline: none; outline: none;
cursor: grab;
touch-action: none;
user-select: none;
}
.simulation-viewport__stage canvas:active {
cursor: grabbing;
} }
.simulation-viewport__stage canvas:focus-visible { .simulation-viewport__stage canvas:focus-visible {
@@ -407,6 +488,21 @@
padding: 0.48rem 0.65rem; padding: 0.48rem 0.65rem;
} }
@container (max-width: 68rem) {
.simulation-viewport__toolbar {
grid-template-columns: 1fr;
}
.simulation-viewport__controls {
flex-wrap: wrap;
justify-content: center;
}
.simulation-viewport__toolbar-balance {
display: none;
}
}
.simulation-workspace__processing { .simulation-workspace__processing {
display: flex; display: flex;
min-height: 18rem; min-height: 18rem;
@@ -45,6 +45,7 @@ import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult"; import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult"; import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQualificationResult"; import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQualificationResult";
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult"; import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult"; import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
@@ -98,6 +99,9 @@ export function AdvancedLaboratoryResult({
if (workId === "m48-static-occupancy-qualification" && results.m48StaticOccupancy) { if (workId === "m48-static-occupancy-qualification" && results.m48StaticOccupancy) {
return <M48StaticOccupancyQualificationResultView rigLabel={rigLabel} result={results.m48StaticOccupancy} />; return <M48StaticOccupancyQualificationResultView rigLabel={rigLabel} result={results.m48StaticOccupancy} />;
} }
if (workId === "m48r3-static-occupancy-shadow" && results.m48r3StaticOccupancy) {
return <M48R3StaticOccupancyShadowResultView rigLabel={rigLabel} result={results.m48r3StaticOccupancy} />;
}
if (workId === "m48s-fixed-class-detector" && results.m48s) { if (workId === "m48s-fixed-class-detector" && results.m48s) {
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />; return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
} }
@@ -0,0 +1,81 @@
import { useEffect, useMemo, useState } from "react";
import { Icon } from "@nodedc/ui-react";
import {
fetchM48R3StaticOccupancyCases,
type M48R3StaticOccupancyCase,
type M48R3StaticOccupancyShadowResult,
} from "../../core/laboratory/m48r3StaticOccupancyShadow";
import {
M4ReplayThreatVisual,
type M4ReplayThreatReviewAnchor,
} from "./M4ReplayThreatVisual";
function message(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "M4.8R3 timeline недоступен.";
}
export function M48R3StaticOccupancyShadowEvidence({
result,
}: {
result: M48R3StaticOccupancyShadowResult;
}) {
const [cases, setCases] = useState<readonly M48R3StaticOccupancyCase[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchM48R3StaticOccupancyCases(result.resultId, { signal: controller.signal })
.then((nextCases) => {
if (!controller.signal.aborted) setCases(nextCases);
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) setError(message(caught));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [result.resultId]);
const reviewAnchors = useMemo<readonly M4ReplayThreatReviewAnchor[]>(() => (
cases.map((item) => ({
id: item.anchorId,
sourceSequence: item.sourceSequence,
extentXyxyNormalized: item.extentXyxy,
matchedAtThreshold: item.matched,
}))
), [cases]);
if (loading) {
return (
<div className="l3-visual-audit__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Открываем полный M4.8R3 Worker timeline</span>
</div>
);
}
if (error) {
return (
<div className="l3-visual-audit__state" role="alert">
<Icon name="alert" size={18} />
<span>{error}</span>
</div>
);
}
return (
<M4ReplayThreatVisual
resultId={result.resultId}
reviewAnchors={reviewAnchors}
showReviewAnchorBoxes={false}
reviewLabel="Контрольные кадры M4.8R3 · без ручных рамок"
timelineEndpointRoot="/api/v1/laboratory/m48r3/static-occupancy"
evidenceLabel="M4.8R3"
/>
);
}
@@ -0,0 +1,101 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { M48R3StaticOccupancyShadowResult } from "../../core/laboratory/m48r3StaticOccupancyShadow";
import { M48R3StaticOccupancyShadowEvidence } from "./M48R3StaticOccupancyShadowEvidence";
function number(value: number, digits = 2): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
function percent(value: number): string {
return `${number(value * 100, 1)}%`;
}
export function M48R3StaticOccupancyShadowResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M48R3StaticOccupancyShadowResult;
}) {
const candidate = result.metrics.performance.candidate;
const occupancy = result.metrics.occupancy;
const separation = result.metrics.anchors.separation;
const separated = separation.every((item) => item.passed);
const status = result.accepted
? "Полный Worker shadow принят: realtime и раздельные препятствия сохранены"
: "Worker shadow не прошёл один или несколько предобъявленных gate";
const separatedLabel = separation.length
? separation.map((item) => `${item.observedComponents}/${item.expectedMinimumComponents}`).join(" · ")
: "—";
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.8R3 · full Worker static occupancy shadow"
description="Полный 4 489-кадровый прогон проверяет additive low-step occupied-only слой внутри штатного graph pipeline. Камера и RF-DETR не получают дополнительного inference; ручные прямоугольники используются только для перехода к контрольным кадрам и не рисуются как системный результат."
status={status}
statusTone={result.accepted ? "success" : "warning"}
facts={[
{ label: "Конфигурация", value: `${rigLabel} RIGHT · VIDEO/CAMERA/3D/PLAN · LOW-STEP provenance` },
{ label: "Профиль", value: `${result.profile.id} · minimum points ${result.profile.minimumPoints}` },
{ label: "Прогон", value: `${result.metrics.frames.candidateDelivered}/${result.metrics.frames.expected} · immutable ${result.resultId}` },
{ label: "Нагрузка", value: `12 Hz source-paced · ${number(candidate.fps, 3)} effective FPS · +0 inference` },
{ label: "Authority", value: "REPLAY-SIMULATED · production/navigation/actuation OFF" },
]}
brief={{
question: "Можно ли добавить геометрическое обнаружение низких статических препятствий, не разрушив realtime и не склеив отдельные столбики/шары в один объект?",
approach: `Кандидат сравнен покадрово с native baseline на всех ${result.metrics.frames.expected} кадрах. Проверены latency, FPS, рост occupancy, capacity drops, отсутствие потерянных baseline-ячеек и отдельные компоненты на кадре 1856.`,
principalResult: `${number(candidate.fps, 3)} FPS; world-state p95 ${number(candidate.worldP95Ms, 2)} мс; geometry p95/p99 ${number(candidate.geometryP95Ms, 2)}/${number(candidate.geometryP99Ms, 2)} мс; разделение ${separatedLabel}.`,
limitation: "Это воспроизводимый Worker shadow, а не доказательство физической проходимости. Просвет между компонентами сохраняется как геометрия, но допустимость проезда зависит от будущего габарита шасси и отдельного free-space контракта.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "m48r3-native-plus-low-step-reference-graph/v1",
components: [
{ kind: "source", name: "native reference graph baseline", version: "M4.7/M4.8R2", role: "immutable occupied/unknown baseline", identitySha256: null },
{ kind: "algorithm", name: "additive low-step occupied-only", version: "v1", role: "CPU geometry; never clearing; no semantic class", identitySha256: result.profile.sha256 },
{ kind: "runtime", name: "full source-paced Worker shadow", version: "4 489 frames", role: "predeclared realtime, growth, separation and safety gates", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.8R3 VISUAL EVIDENCE · SYSTEM COMPONENTS"
title="Полный timeline; LOW-STEP показывает добавленные системой компоненты, ручные рамки скрыты"
kind="recorded-replay"
resizable
>
<M48R3StaticOccupancyShadowEvidence result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Что доказал прогон"
status={status}
statusTone={result.accepted ? "success" : "warning"}
metrics={[
{ label: "Realtime", value: `${number(candidate.fps, 3)} FPS`, hint: `p95 ${number(candidate.worldP95Ms, 2)} мс · gate ≥11,5 FPS / ≤60 мс` },
{ label: "Geometry", value: `${number(candidate.geometryP95Ms, 2)} / ${number(candidate.geometryP99Ms, 2)} мс`, hint: "p95 / p99 · gates 9 / 16 мс" },
{ label: "Occupancy delta", value: `+${occupancy.addedCellTotal.toLocaleString("ru-RU")}`, hint: `${percent(occupancy.meanCellGrowthFraction)} cells · ${percent(occupancy.meanComponentGrowthFraction)} components` },
{ label: "Кадр 1856", value: separated ? `раздельно · ${separatedLabel}` : `не принят · ${separatedLabel}`, hint: "два столбика и две полусферы проверяются отдельными component gates" },
{ label: "Потери / false free", value: `${occupancy.lostCellTotal} / ${result.metrics.falseFreeCount}`, hint: `capacity drops ${result.metrics.capacityDropCount}` },
]}
conclusion={{
proved: `Система сама добавила ${result.metrics.provider.additiveObservationCount.toLocaleString("ru-RU")} low-step observations на ${result.metrics.provider.framesWithAdditions.toLocaleString("ru-RU")} кадрах, сохранила baseline без потерь и выдержала полный realtime shadow.`,
notProved: "Не доказаны физический clearance, planner-authoritative free space, независимые precision/recall и безопасность движения на реальном шасси.",
decision: result.accepted
? "Worker-кандидат принят в ограниченном replay-shadow контуре. Следующий шаг — отдельное решение о cutover и регрессия на новых сценах; ручная разметка не становится runtime-зависимостью."
: "Cutover запрещён. Исправить провалившийся gate и повторить полный immutable shadow без ослабления порогов.",
}}
/>
)}
/>
);
}
@@ -110,12 +110,16 @@ export function M4ReplayThreatVisual({
resultId, resultId,
semantic, semantic,
reviewAnchors = EMPTY_REVIEW_ANCHORS, reviewAnchors = EMPTY_REVIEW_ANCHORS,
showReviewAnchorBoxes = true,
reviewLabel = "Контрольные примеры M4.8R1",
timelineEndpointRoot, timelineEndpointRoot,
evidenceLabel = "M4.6", evidenceLabel = "M4.6",
}: { }: {
resultId: string; resultId: string;
semantic?: M4ReplayThreatSemanticLayer; semantic?: M4ReplayThreatSemanticLayer;
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[]; reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
showReviewAnchorBoxes?: boolean;
reviewLabel?: string;
timelineEndpointRoot?: string; timelineEndpointRoot?: string;
evidenceLabel?: string; evidenceLabel?: string;
}) { }) {
@@ -124,6 +128,7 @@ export function M4ReplayThreatVisual({
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true); const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true); const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true); const [showRollingMap, setShowRollingMap] = useState(true);
const [showLowStep, setShowLowStep] = useState(true);
const [showMediaSemantic, setShowMediaSemantic] = useState(true); const [showMediaSemantic, setShowMediaSemantic] = useState(true);
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true); const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
const [showMediaPoints, setShowMediaPoints] = useState(false); const [showMediaPoints, setShowMediaPoints] = useState(false);
@@ -262,7 +267,7 @@ export function M4ReplayThreatVisual({
}, [metadata.timeline, resultId, reviewAnchorIdentity, reviewAnchors, seekPlayback, setPlaybackPlaying]); }, [metadata.timeline, resultId, reviewAnchorIdentity, reviewAnchors, seekPlayback, setPlaybackPlaying]);
const reviewAnchorBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => { const reviewAnchorBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
const timeline = metadata.timeline; const timeline = metadata.timeline;
if (!frame || !timeline) return []; if (!frame || !timeline || !showReviewAnchorBoxes) return [];
return reviewAnchors return reviewAnchors
.filter((anchor) => anchor.sourceSequence === frame.sequence) .filter((anchor) => anchor.sourceSequence === frame.sequence)
.map((anchor) => { .map((anchor) => {
@@ -281,7 +286,7 @@ export function M4ReplayThreatVisual({
dashed: true, dashed: true,
}; };
}); });
}, [frame, metadata.timeline, reviewAnchors]); }, [frame, metadata.timeline, reviewAnchors, showReviewAnchorBoxes]);
const activeBoxes = useMemo( const activeBoxes = useMemo(
() => [...boxes(frame?.cameraProposals ?? []), ...reviewAnchorBoxes], () => [...boxes(frame?.cameraProposals ?? []), ...reviewAnchorBoxes],
[frame, reviewAnchorBoxes], [frame, reviewAnchorBoxes],
@@ -345,6 +350,7 @@ export function M4ReplayThreatVisual({
state: obstacle.state, state: obstacle.state,
centroidBodyXyzM: obstacle.centroidBodyXyzM, centroidBodyXyzM: obstacle.centroidBodyXyzM,
cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM, cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM,
occupancySource: obstacle.occupancySource,
})) ?? [], [spatialFrame]); })) ?? [], [spatialFrame]);
const currentIncrementObstacles = spatialFrame?.metricObstacles.filter( const currentIncrementObstacles = spatialFrame?.metricObstacles.filter(
(item) => item.state === "current", (item) => item.state === "current",
@@ -352,6 +358,9 @@ export function M4ReplayThreatVisual({
const rollingMapObstacles = spatialFrame?.metricObstacles.filter( const rollingMapObstacles = spatialFrame?.metricObstacles.filter(
(item) => item.state === "retained", (item) => item.state === "retained",
) ?? []; ) ?? [];
const lowStepObstacles = spatialFrame?.metricObstacles.filter(
(item) => item.occupancySource !== "baseline",
) ?? [];
const nearest = spatialFrame?.metricObstacles const nearest = spatialFrame?.metricObstacles
.map((item) => item.assessment.closestApproachM) .map((item) => item.assessment.closestApproachM)
.filter((value): value is number => value !== null) .filter((value): value is number => value !== null)
@@ -513,6 +522,18 @@ export function M4ReplayThreatVisual({
> >
ROLLING ROLLING
</Button> </Button>
{metadata.timeline?.occupancyProvenanceDelivery ? (
<Button
size="compact"
shape="pill"
variant={showLowStep ? "primary" : "secondary"}
aria-pressed={showLowStep}
title="Добавочные occupied-only компоненты low-step; без ручных рамок"
onClick={() => setShowLowStep((visible) => !visible)}
>
LOW-STEP
</Button>
) : null}
{semantic ? ( {semantic ? (
<Button <Button
size="compact" size="compact"
@@ -567,7 +588,7 @@ export function M4ReplayThreatVisual({
<Icon name="chevron-right" size={16} /> <Icon name="chevron-right" size={16} />
</IconButton> </IconButton>
<Select <Select
label="Контрольные примеры M4.8R1" label={reviewLabel}
value={String(selectedReviewAnchorIndex)} value={String(selectedReviewAnchorIndex)}
options={reviewAnchors.map((anchor, index) => ({ options={reviewAnchors.map((anchor, index) => ({
value: String(index), value: String(index),
@@ -618,6 +639,9 @@ export function M4ReplayThreatVisual({
<span>Spatial evidence</span> <span>Spatial evidence</span>
<strong> <strong>
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling {currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
{metadata.timeline.occupancyProvenanceDelivery
? ` · ${lowStepObstacles.length} low-step`
: ""}
</strong> </strong>
<small> <small>
{spatialFrame {spatialFrame
@@ -758,6 +782,7 @@ export function M4ReplayThreatVisual({
showCurrentIncrement={showCurrentIncrement} showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface} showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap} showRollingMap={showRollingMap}
showLowStep={showLowStep}
pointSemanticClassIds={alignedSemanticPointIds} pointSemanticClassIds={alignedSemanticPointIds}
semanticClasses={semanticClasses} semanticClasses={semanticClasses}
semanticPalette={semanticPalette} semanticPalette={semanticPalette}
@@ -84,6 +84,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
experimentName: "M4.8 · conservative static occupancy qualification", experimentName: "M4.8 · conservative static occupancy qualification",
variantName: "M4.8R2 · current/rolling + low-step occupied-only candidate", variantName: "M4.8R2 · current/rolling + low-step occupied-only candidate",
}, },
"m48r3-static-occupancy-shadow": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Canonical Reference Graph`,
experimentId: "m48r3-static-occupancy-shadow",
experimentName: "M4.8R3 · full Worker static occupancy shadow",
variantName: "M4.8R3 · 4 489 frames · additive low-step occupied-only",
},
"m48s-fixed-class-detector": { "m48s-fixed-class-detector": {
profileId: "rig-ravnoves-perception-gate-v1", profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`, profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
@@ -22,6 +22,7 @@ function mergeResults(
m48: next.m48 ?? current.m48, m48: next.m48 ?? current.m48,
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic, m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
m48StaticOccupancy: next.m48StaticOccupancy ?? current.m48StaticOccupancy, m48StaticOccupancy: next.m48StaticOccupancy ?? current.m48StaticOccupancy,
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
m48s: next.m48s ?? current.m48s, m48s: next.m48s ?? current.m48s,
m48t: next.m48t ?? current.m48t, m48t: next.m48t ?? current.m48t,
m4Threat: next.m4Threat ?? current.m4Threat, m4Threat: next.m4Threat ?? current.m4Threat,
@@ -122,6 +123,7 @@ export function useAdvancedLaboratoryCatalog({
"m48-object-centric-quality", "m48-object-centric-quality",
"m48-small-static-passage-regression", "m48-small-static-passage-regression",
"m48-static-occupancy-qualification", "m48-static-occupancy-qualification",
"m48r3-static-occupancy-shadow",
].includes(selectedWorkId) ].includes(selectedWorkId)
&& !indexedResultId && !indexedResultId
) return; ) return;
@@ -63,11 +63,29 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(packageDocument, /"playcanvas": "2\.21\.4"/); assert.match(packageDocument, /"playcanvas": "2\.21\.4"/);
assert.doesNotMatch(packageDocument, /@playcanvas\/react/); assert.doesNotMatch(packageDocument, /@playcanvas\/react/);
assert.match(runtime, /new Application\(canvas/); assert.match(runtime, /new Application\(canvas/);
assert.match(runtime, /setCanvasFillMode\(FILLMODE_NONE/);
assert.match(runtime, /setCanvasResolution\(RESOLUTION_AUTO\)/);
assert.match(runtime, /new Asset\([^,]+, "gsplat"/); assert.match(runtime, /new Asset\([^,]+, "gsplat"/);
assert.match(runtime, /camera\.script\?\.create\(CameraControls/); assert.match(runtime, /camera\.script\?\.create\(CameraControls/);
assert.match(runtime, /const HOME_POSITION = new Vec3\(0, 1, 0\)/);
assert.match(runtime, /const HOME_FOCUS = new Vec3\(1, 1, 0\)/);
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 0, pixelRatio: 2/);
assert.match(runtime, /frame\.mouse\[0\] \*= -1/);
assert.match(runtime, /new Asset\([\s\S]*"container"/);
assert.match(runtime, /instantiateRenderEntity/);
assert.match(runtime, /applyWorldTransform/);
assert.match(runtime, /setLayerWorldTransform/);
assert.match(runtime, /PLAYCANVAS_X_180_TRANSFORM/);
assert.match(runtime, /ensureCollision/);
assert.match(runtime, /app\.root\.addChild/); assert.match(runtime, /app\.root\.addChild/);
assert.match(runtime, /dispose\(\)/); assert.match(runtime, /dispose\(\)/);
assert.doesNotMatch(`${runtime}\n${viewport}`, /iframe|<GSplat/); assert.doesNotMatch(`${runtime}\n${viewport}`, /iframe|<GSplat/);
assert.match(viewport, /Collision недоступен/); assert.match(viewport, /Collision недоступен/);
assert.match(viewport, /Для этой сборки collision GLB не был запрошен/); assert.match(viewport, /Для этой сборки collision GLB не был запрошен/);
assert.match(viewport, /<Select[\s\S]*Качество Gaussian-сцены/);
assert.match(viewport, /Максимум/);
assert.match(viewport, /runtimeRef\.current\?\.home\(\)/);
assert.match(viewport, /Настройки системы координат/);
assert.match(viewport, /Инверсия визуального слоя/);
assert.match(viewport, /Инверсия collision-слоя/);
}); });
@@ -0,0 +1,10 @@
{
"schema_version": "missioncore.laboratory-evidence-definition/v1",
"work_id": "m48r3-static-occupancy-shadow",
"evidence": {
"runtime_relative_root": "m48/static-occupancy-shadow-results",
"result_id_prefix": "m48r3-static-occupancy-shadow",
"document_name": "manifest.json",
"schema_version": "missioncore.m48r3-static-occupancy-shadow-result/v1"
}
}
+1
View File
@@ -165,6 +165,7 @@
} }
], ],
"legacy_work_ids": [ "legacy_work_ids": [
"m48r3-static-occupancy-shadow",
"m47-reference-graph-shadow", "m47-reference-graph-shadow",
"e31-source-binding", "e31-source-binding",
"e32-track-geometry", "e32-track-geometry",
@@ -20,6 +20,10 @@
"voxel_size_m": 0.45, "voxel_size_m": 0.45,
"neighbor_radius_cells": 1, "neighbor_radius_cells": 1,
"minimum_points": 5, "minimum_points": 5,
"sparse_persistence_minimum_points": 2,
"sparse_persistence_window_frames": 6,
"sparse_persistence_minimum_hits": 6,
"sparse_persistence_maximum_range_m": 8.0,
"minimum_voxels": 1, "minimum_voxels": 1,
"local_radius_m": 10.0, "local_radius_m": 10.0,
"maximum_candidate_points_per_frame": 768, "maximum_candidate_points_per_frame": 768,
@@ -22,7 +22,7 @@
"provider_id": "ravnoves00-additive-low-step-geometry/v1", "provider_id": "ravnoves00-additive-low-step-geometry/v1",
"version": "0.1.0", "version": "0.1.0",
"revision": "m48r3-ravnoves00-additive-low-step/v1", "revision": "m48r3-ravnoves00-additive-low-step/v1",
"sha256": "0d46dcb28542902849ee31de4c4594ef90407834d9e5551474fc4191cadbf1a9" "sha256": "00fc197ee7200e2f0447b6bec7cc2f21b3c2970d7dd9414cc41e6787d712fee5"
}, },
{ {
"role": "temporal", "role": "temporal",
@@ -1504,6 +1504,42 @@ anchors onto its immutable timeline; no third perception instrument exists.
Navigation, commands, actuation, physical clearance and collision-safety Navigation, commands, actuation, physical clearance and collision-safety
authority remain false. authority remain false.
### 2026-08-26 — M4.8R3 bounded static-occupancy Worker shadow accepted
M4.8R3 closes the bounded Worker integration requested by M4.8R2. The sealed
result is
`m48r3-static-occupancy-shadow-d1577870b098bcd0b67cc51798234f224b348ad3954ead72f3ce6df414875a29`.
It composes the accepted camera/geometry graph with an occupied-only low-step
provider; RF-DETR, the native `800×600` fisheye raster and the semantic risk
policy remain unchanged. Weak two-to-four-point geometry is published only
when the exact `0.45 m` voxel is present in all six causal frames and its
current range is at most `8 m`. Strong five-point components remain immediate.
The provider never uses a semantic class, clears a cell, infers free space or
runs another neural-network pass.
The isolated Worker 006 replay delivered all `4,489/4,489` frames at
`11.79902 FPS`. World-state completion p95 was `56.74324 ms`; geometry p95/p99
were `8.420913/13.732692 ms`; GPU utilization p95/maximum were `54%/58%` and
maximum used GPU memory was `9,743 MiB`. The additive provider contributed a
mean `0.587893 ms` per frame. Relative to the frozen native baseline, FPS
regression was `0.3745552%` and world-state p95 increased by `8.802461 ms`,
both inside the predeclared envelope.
All `9/9` critical-near assisted anchors are occupied in the delivered graph.
At display frame `1856`, the two thin posts remain independent (`5` observed
components in their review extent) and the two concrete hemispheres remain
independent (`6` observed components); the system does not inherit the
operator's old paired boxes. The ledger adds `613,146` occupied cells, loses
zero baseline cells, makes zero false-free claims and records zero capacity
drops. The two `8–12 m` approach anchors intentionally remain unknown because
the sparse-persistence policy is bounded to the critical `0–8 m` band.
This accepts the reproducible bounded Worker shadow and its LAB evidence, not a
production navigation cutover. The result remains replay-simulated;
physical-live authority, commands, actuation, planner-authoritative free space
and collision-safety acceptance are false. The next milestone boundary is
M4.9 recorded-realtime release-candidate validation using this frozen graph.
## Implementation order ## Implementation order
The implementation sequence is intentionally strict: The implementation sequence is intentionally strict:
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Seal a complete M4.8R3 Worker shadow against the accepted native baseline."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from k1link.laboratory.m48r3_static_occupancy_shadow import (
M48R3StaticOccupancyShadowError,
build_m48r3_static_occupancy_shadow,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repository-root", type=Path, required=True)
parser.add_argument("--profile", type=Path, required=True)
parser.add_argument("--baseline-result", type=Path, required=True)
parser.add_argument("--baseline-frames", type=Path, required=True)
parser.add_argument("--candidate-result", type=Path, required=True)
parser.add_argument("--candidate-frames", type=Path, required=True)
parser.add_argument("--m48r2-result-root", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
arguments = parser.parse_args()
try:
result = build_m48r3_static_occupancy_shadow(
repository_root=arguments.repository_root,
profile_path=arguments.profile,
baseline_result_path=arguments.baseline_result,
baseline_frames_path=arguments.baseline_frames,
candidate_result_path=arguments.candidate_result,
candidate_frames_path=arguments.candidate_frames,
m48r2_result_root=arguments.m48r2_result_root,
output_root=arguments.output_root,
)
except (M48R3StaticOccupancyShadowError, OSError, ValueError) as exc:
parser.error(str(exc))
print(
json.dumps(
{
"result_id": result.result_id,
"result_root": str(result.result_root),
"accepted": result.manifest["accepted"],
"gates": result.report["gates"],
},
ensure_ascii=False,
indent=2,
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -113,10 +113,10 @@ $expectedConfigs = [ordered]@{
if ($AdditiveLowStep) { if ($AdditiveLowStep) {
$expectedConfigs.Remove("m48n-rf-detr-native-reference-graph-shadow-v0.json") $expectedConfigs.Remove("m48n-rf-detr-native-reference-graph-shadow-v0.json")
$expectedConfigs["m48r3-native-low-step-reference-graph-shadow-v1.json"] = ( $expectedConfigs["m48r3-native-low-step-reference-graph-shadow-v1.json"] = (
"5f5832a0a0c1879374b166071efed02de14aeda815046da5711d6f4053eb9af4" "e2e307347265076908c4ec1b7da75028998490dfa74ba64f12939e233406d9d0"
) )
$expectedConfigs["m48r3-additive-low-step-occupancy-v1.json"] = ( $expectedConfigs["m48r3-additive-low-step-occupancy-v1.json"] = (
"0d46dcb28542902849ee31de4c4594ef90407834d9e5551474fc4191cadbf1a9" "00fc197ee7200e2f0447b6bec7cc2f21b3c2970d7dd9414cc41e6787d712fee5"
) )
} }
foreach ($entry in $expectedConfigs.GetEnumerator()) { foreach ($entry in $expectedConfigs.GetEnumerator()) {
@@ -0,0 +1,845 @@
"""Seal the full M4.8R3 occupied-only Worker shadow and its bounded diff."""
from __future__ import annotations
import hashlib
import json
import math
import os
import shutil
import uuid
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
import numpy as np
from k1link.laboratory.m48_static_occupancy_qualification import (
M48StaticOccupancyQualificationError,
read_m48_static_occupancy_qualification,
)
from k1link.perception.geometry import RecordedGeometryStore
from k1link.perception.geometry_math import project_map_points_kb4
from k1link.perception.m48_low_step_occupancy import M48_LOW_STEP_PROFILE_SCHEMA
M48R3_SHADOW_RESULT_SCHEMA: Final = (
"missioncore.m48r3-static-occupancy-shadow-result/v1"
)
M48R3_SHADOW_REPORT_SCHEMA: Final = (
"missioncore.m48r3-static-occupancy-shadow-report/v1"
)
M48R3_SHADOW_CASE_SCHEMA: Final = (
"missioncore.m48r3-static-occupancy-shadow-case/v1"
)
M48R3_SHADOW_DIFF_SCHEMA: Final = (
"missioncore.m48r3-static-occupancy-frame-diff/v1"
)
M48R3_SHADOW_PREFIX: Final = "m48r3-static-occupancy-shadow-"
FRAME_EVIDENCE_SCHEMA: Final = "missioncore.m48s-reference-graph-frame-evidence/v1"
WORKER_RESULT_SCHEMA: Final = "missioncore.m48s-reference-graph-shadow-load/v5"
EXPECTED_FRAMES: Final = 4_489
VOXEL_SIZE_M: Final = 0.45
_AUTHORITY: Final = {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
Cell = tuple[int, int, int]
class M48R3StaticOccupancyShadowError(RuntimeError):
"""The M4.8R3 Worker evidence or immutable result is invalid."""
@dataclass(frozen=True, slots=True)
class M48R3StaticOccupancyShadowResult:
result_id: str
result_root: Path
manifest: dict[str, Any]
report: dict[str, Any]
cases: tuple[dict[str, Any], ...]
@dataclass(frozen=True, slots=True)
class LedgerComparison:
frame_count: int
baseline_cell_total: int
candidate_cell_total: int
added_cell_total: int
lost_cell_total: int
baseline_component_total: int
candidate_component_total: int
false_free_count: int
maximum_added_cells_per_frame: int
maximum_candidate_components_per_frame: int
mean_cell_growth_fraction: float
mean_component_growth_fraction: float
diff_rows: tuple[dict[str, Any], ...]
selected_candidate_rows: dict[int, dict[str, Any]]
def build_m48r3_static_occupancy_shadow(
*,
repository_root: Path,
profile_path: Path,
baseline_result_path: Path,
baseline_frames_path: Path,
candidate_result_path: Path,
candidate_frames_path: Path,
m48r2_result_root: Path,
output_root: Path,
) -> M48R3StaticOccupancyShadowResult:
"""Compare full ledgers and publish one append-only M4.8R3 result."""
repository = repository_root.resolve(strict=True)
profile_bytes = profile_path.resolve(strict=True).read_bytes()
profile = _object(json.loads(profile_bytes), "M4.8R3 profile")
if profile.get("schema_version") != M48_LOW_STEP_PROFILE_SCHEMA:
raise M48R3StaticOccupancyShadowError("M4.8R3 profile changed")
profile_sha256 = hashlib.sha256(profile_bytes).hexdigest()
acceptance = _object(profile.get("acceptance"), "M4.8R3 acceptance")
source = _object(profile.get("source"), "M4.8R3 source")
if source.get("frame_count") != EXPECTED_FRAMES:
raise M48R3StaticOccupancyShadowError("M4.8R3 frame contract changed")
baseline = _read_worker_result(baseline_result_path)
candidate = _read_worker_result(candidate_result_path)
_validate_worker_binding(
baseline,
baseline_frames_path,
expected_frames=EXPECTED_FRAMES,
additive_profile_sha256=None,
)
_validate_worker_binding(
candidate,
candidate_frames_path,
expected_frames=EXPECTED_FRAMES,
additive_profile_sha256=profile_sha256,
)
try:
m48r2 = read_m48_static_occupancy_qualification(m48r2_result_root)
except M48StaticOccupancyQualificationError as exc:
raise M48R3StaticOccupancyShadowError("M4.8R2 binding changed") from exc
if (
m48r2.result_id != source.get("m48r2_result_id")
or _file_sha256(m48r2.result_root / "cases.jsonl")
!= source.get("m48r2_cases_sha256")
):
raise M48R3StaticOccupancyShadowError("M4.8R2 source changed")
selected_sequences = {int(row["sequence"]) - 1 for row in m48r2.cases}
comparison = compare_m48r3_frame_ledgers(
baseline_frames_path,
candidate_frames_path,
selected_sequences=selected_sequences,
expected_frames=EXPECTED_FRAMES,
)
store = RecordedGeometryStore.from_repository(repository)
cases = _evaluate_cases(
m48r2.cases,
comparison.selected_candidate_rows,
store,
)
separation = _evaluate_separation(profile, cases)
baseline_metrics = _worker_metrics(baseline)
candidate_metrics = _worker_metrics(candidate)
provider = _provider_metrics(candidate)
critical = [row for row in cases if row["distance_band"] == "critical-near"]
near_recall = _rate(critical, "matched")
canonical_recall = (
1.0
if comparison.lost_cell_total == 0
and m48r2.report["metrics"]["canonical_engineering_recall"] == 1.0
else 0.0
)
capacity_drop_count = (
int(provider["geometry_failed_frames"])
+ int(provider["temporal_failed_frames"])
+ int(provider["rolling_capacity_evicted_cells"])
)
fps_regression = max(
0.0,
(baseline_metrics["fps"] - candidate_metrics["fps"])
/ baseline_metrics["fps"],
)
world_p95_delta = (
candidate_metrics["world_p95_ms"] - baseline_metrics["world_p95_ms"]
)
gates = {
"complete_frame_accounting": candidate_metrics["delivered_frames"]
== EXPECTED_FRAMES,
"minimum_effective_world_state_fps": candidate_metrics["fps"]
>= float(acceptance["minimum_effective_world_state_fps"]),
"maximum_world_state_completion_p95_ms": candidate_metrics[
"world_p95_ms"
]
<= float(acceptance["maximum_world_state_completion_p95_ms"]),
"maximum_geometry_stage_p95_ms": candidate_metrics["geometry_p95_ms"]
<= float(acceptance["maximum_geometry_stage_p95_ms"]),
"maximum_geometry_stage_p99_ms": candidate_metrics["geometry_p99_ms"]
<= float(acceptance["maximum_geometry_stage_p99_ms"]),
"maximum_fps_regression_fraction": fps_regression
<= float(acceptance["maximum_fps_regression_fraction_vs_native_baseline"]),
"maximum_world_state_p95_delta_ms": world_p95_delta
<= float(acceptance["maximum_world_state_p95_delta_ms_vs_native_baseline"]),
"maximum_component_mean_growth_fraction": comparison.mean_component_growth_fraction
<= float(acceptance["maximum_additive_component_mean_growth_fraction"]),
"maximum_cell_mean_growth_fraction": comparison.mean_cell_growth_fraction
<= float(acceptance["maximum_additive_cell_mean_growth_fraction"]),
"zero_capacity_drops": capacity_drop_count
<= int(acceptance["maximum_capacity_drop_count"]),
"zero_baseline_cell_loss": comparison.lost_cell_total == 0,
"critical_near_recall": near_recall
>= float(acceptance["minimum_critical_near_recall"]),
"canonical_engineering_recall": canonical_recall
>= float(acceptance["minimum_canonical_engineering_recall"]),
"zero_false_free": comparison.false_free_count
<= int(acceptance["maximum_false_free_count"]),
"separation_expectations": all(row["passed"] for row in separation),
}
accepted = all(gates.values())
producer_sha256 = _file_sha256(Path(__file__).resolve())
candidate_frame_sha256 = _file_sha256(candidate_frames_path)
baseline_frame_sha256 = _file_sha256(baseline_frames_path)
created_at = _worker_completed_at(candidate)
identity = {
"schema_version": M48R3_SHADOW_RESULT_SCHEMA,
"created_at_utc": created_at,
"profile_id": profile["profile_id"],
"profile_sha256": profile_sha256,
"producer_sha256": producer_sha256,
"baseline_result_sha256": _file_sha256(baseline_result_path),
"baseline_frames_sha256": baseline_frame_sha256,
"candidate_result_sha256": _file_sha256(candidate_result_path),
"candidate_frames_sha256": candidate_frame_sha256,
"m48r2_result_id": m48r2.result_id,
"authority": dict(_AUTHORITY),
}
result_id = M48R3_SHADOW_PREFIX + _canonical_sha256(identity)
metrics = {
"frames": {
"expected": EXPECTED_FRAMES,
"baseline_delivered": baseline_metrics["delivered_frames"],
"candidate_delivered": candidate_metrics["delivered_frames"],
},
"performance": {
"baseline": baseline_metrics,
"candidate": candidate_metrics,
"fps_regression_fraction": round(fps_regression, 9),
"world_state_p95_delta_ms": round(world_p95_delta, 6),
},
"occupancy": {
"baseline_cell_total": comparison.baseline_cell_total,
"candidate_cell_total": comparison.candidate_cell_total,
"added_cell_total": comparison.added_cell_total,
"lost_cell_total": comparison.lost_cell_total,
"baseline_component_total": comparison.baseline_component_total,
"candidate_component_total": comparison.candidate_component_total,
"mean_cell_growth_fraction": round(
comparison.mean_cell_growth_fraction, 9
),
"mean_component_growth_fraction": round(
comparison.mean_component_growth_fraction, 9
),
"maximum_added_cells_per_frame": comparison.maximum_added_cells_per_frame,
"maximum_candidate_components_per_frame": (
comparison.maximum_candidate_components_per_frame
),
},
"provider": provider,
"assisted_anchors": {
"count": len(cases),
"critical_near_count": len(critical),
"critical_near_recall": near_recall,
"matched_count": sum(bool(row["matched"]) for row in cases),
"canonical_engineering_recall": canonical_recall,
"separation": separation,
},
"capacity_drop_count": capacity_drop_count,
"false_free_count": comparison.false_free_count,
}
report = {
"schema_version": M48R3_SHADOW_REPORT_SCHEMA,
"result_id": result_id,
"accepted": accepted,
"profile": {
"id": profile["profile_id"],
"sha256": profile_sha256,
"componentization": profile["componentization"],
"acceptance": acceptance,
},
"metrics": metrics,
"gates": gates,
"decision": {
"state": "accepted-bounded-worker-shadow"
if accepted
else "rejected-bounded-worker-shadow",
"candidate_accepted": accepted,
"production_accepted": False,
"next_action": "product-cutover-decision" if accepted else "reduce-load-or-coverage",
},
"limitations": [
"Operator-assisted rectangles are development anchors, not independent truth.",
"Separated occupied components do not assert passability for an unknown chassis.",
(
"No free-space, navigation, command, actuation or collision-safety "
"authority is granted."
),
],
"authority": dict(_AUTHORITY),
}
destination = output_root.resolve(strict=False) / result_id
_publish(
destination,
identity=identity,
report=report,
cases=cases,
diff_rows=comparison.diff_rows,
candidate_result_path=candidate_result_path,
candidate_frames_path=candidate_frames_path,
)
return read_m48r3_static_occupancy_shadow(destination)
def compare_m48r3_frame_ledgers(
baseline_frames_path: Path,
candidate_frames_path: Path,
*,
selected_sequences: set[int],
expected_frames: int,
) -> LedgerComparison:
"""Stream two aligned ledgers and retain only a bounded cell-diff."""
diffs: list[dict[str, Any]] = []
selected: dict[int, dict[str, Any]] = {}
baseline_cells_total = 0
candidate_cells_total = 0
baseline_components_total = 0
candidate_components_total = 0
added_total = 0
lost_total = 0
false_free = 0
maximum_added = 0
maximum_components = 0
count = 0
with baseline_frames_path.open("r", encoding="utf-8") as baseline_stream, (
candidate_frames_path.open("r", encoding="utf-8")
) as candidate_stream:
for baseline_line, candidate_line in zip(
baseline_stream,
candidate_stream,
strict=True,
):
baseline = _frame_row(json.loads(baseline_line))
candidate = _frame_row(json.loads(candidate_line))
baseline_sequence = _sequence(baseline)
candidate_sequence = _sequence(candidate)
if baseline_sequence != candidate_sequence or baseline_sequence != count:
raise M48R3StaticOccupancyShadowError("frame ledgers are not aligned")
baseline_map = _obstacle_map(baseline)
candidate_map = _obstacle_map(candidate)
baseline_components = _active_components(baseline_map)
candidate_components = _active_components(candidate_map)
baseline_cells = _cell_union(baseline_components)
candidate_cells = _cell_union(candidate_components)
added = candidate_cells - baseline_cells
lost = baseline_cells - candidate_cells
provenance: dict[str, str] = {}
for component_id, cells in candidate_components:
if not cells.intersection(added):
continue
provenance[component_id] = (
"mixed" if cells.intersection(baseline_cells) else "additive-low-step"
)
diffs.append(
{
"schema_version": M48R3_SHADOW_DIFF_SCHEMA,
"sequence": baseline_sequence,
"baseline_cell_count": len(baseline_cells),
"candidate_cell_count": len(candidate_cells),
"added_cells": [list(cell) for cell in sorted(added)],
"lost_cell_count": len(lost),
"component_provenance": provenance,
}
)
if baseline_sequence in selected_sequences:
selected[baseline_sequence] = candidate
baseline_cells_total += len(baseline_cells)
candidate_cells_total += len(candidate_cells)
baseline_components_total += len(baseline_components)
candidate_components_total += len(candidate_components)
added_total += len(added)
lost_total += len(lost)
false_free += candidate_map.get("free_space_claimed") is True
maximum_added = max(maximum_added, len(added))
maximum_components = max(maximum_components, len(candidate_components))
count += 1
if count != expected_frames or set(selected) != selected_sequences:
raise M48R3StaticOccupancyShadowError("frame ledgers are incomplete")
return LedgerComparison(
frame_count=count,
baseline_cell_total=baseline_cells_total,
candidate_cell_total=candidate_cells_total,
added_cell_total=added_total,
lost_cell_total=lost_total,
baseline_component_total=baseline_components_total,
candidate_component_total=candidate_components_total,
false_free_count=false_free,
maximum_added_cells_per_frame=maximum_added,
maximum_candidate_components_per_frame=maximum_components,
mean_cell_growth_fraction=_growth(candidate_cells_total, baseline_cells_total),
mean_component_growth_fraction=_growth(
candidate_components_total,
baseline_components_total,
),
diff_rows=tuple(diffs),
selected_candidate_rows=selected,
)
def read_m48r3_static_occupancy_shadow(
result_root: Path,
) -> M48R3StaticOccupancyShadowResult:
root = result_root.resolve(strict=True)
if result_root.is_symlink() or not root.name.startswith(M48R3_SHADOW_PREFIX):
raise M48R3StaticOccupancyShadowError("M4.8R3 result root is invalid")
manifest = _read_json(root / "manifest.json", maximum=4 * 1024 * 1024)
report = _read_json(root / "report.json", maximum=8 * 1024 * 1024)
cases = tuple(_read_jsonl(root / "cases.jsonl", maximum=8 * 1024 * 1024))
if (
manifest.get("schema_version") != M48R3_SHADOW_RESULT_SCHEMA
or manifest.get("result_id") != root.name
or report.get("schema_version") != M48R3_SHADOW_REPORT_SCHEMA
or report.get("result_id") != root.name
or manifest.get("authority") != _AUTHORITY
):
raise M48R3StaticOccupancyShadowError("M4.8R3 result identity changed")
identity = _object(manifest.get("identity"), "M4.8R3 identity")
if (
root.name != M48R3_SHADOW_PREFIX + _canonical_sha256(identity)
or manifest.get("identity_sha256") != _canonical_sha256(identity)
):
raise M48R3StaticOccupancyShadowError("M4.8R3 digest changed")
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
raise M48R3StaticOccupancyShadowError("M4.8R3 artifacts changed")
for artifact in artifacts:
item = _object(artifact, "M4.8R3 artifact")
path = (root / str(item.get("path"))).resolve(strict=True)
if path.parent != root or path.is_symlink() or _file_sha256(path) != item.get("sha256"):
raise M48R3StaticOccupancyShadowError("M4.8R3 artifact changed")
return M48R3StaticOccupancyShadowResult(root.name, root, manifest, report, cases)
def _evaluate_cases(
source_cases: Iterable[dict[str, Any]],
candidate_rows: dict[int, dict[str, Any]],
store: RecordedGeometryStore,
) -> tuple[dict[str, Any], ...]:
result: list[dict[str, Any]] = []
for source in source_cases:
display_frame = int(source["sequence"])
sequence = display_frame - 1
frame = store.frame_for_index(sequence)
if frame is None:
raise M48R3StaticOccupancyShadowError("anchor frame is unavailable")
extent = source.get("extent_xyxy")
if not isinstance(extent, list) or len(extent) != 4:
raise M48R3StaticOccupancyShadowError("anchor extent changed")
bbox = (
float(extent[0]) * frame.projection.width,
float(extent[1]) * frame.projection.height,
float(extent[2]) * frame.projection.width,
float(extent[3]) * frame.projection.height,
)
matches = _component_matches(candidate_rows[sequence], frame, bbox)
result.append(
{
"schema_version": M48R3_SHADOW_CASE_SCHEMA,
"anchor_id": source["anchor_id"],
"display_frame": display_frame,
"source_sequence": sequence,
"extent_xyxy": extent,
"distance_band": source["distance_band"],
"component_count": len(matches),
"components": matches,
"matched": bool(matches),
"authority": "operator-assisted-development-anchor-not-truth",
}
)
return tuple(result)
def _evaluate_separation(
profile: dict[str, Any],
cases: tuple[dict[str, Any], ...],
) -> list[dict[str, Any]]:
by_anchor = {str(row["anchor_id"]): row for row in cases}
result: list[dict[str, Any]] = []
for value in profile.get("separation_expectations", []):
item = _object(value, "separation expectation")
anchor_id = str(item["anchor_id"])
case = by_anchor.get(anchor_id)
if case is None or case["display_frame"] != item["sequence"]:
raise M48R3StaticOccupancyShadowError("separation anchor changed")
expected = int(item["expected_minimum_components"])
observed = int(case["component_count"])
result.append(
{
"anchor_id": anchor_id,
"display_frame": case["display_frame"],
"source_sequence": case["source_sequence"],
"expected_minimum_components": expected,
"observed_components": observed,
"passed": observed >= expected,
"interpretation": item["interpretation"],
}
)
return result
def _component_matches(
graph_row: dict[str, Any],
frame: Any,
bbox: tuple[float, float, float, float],
) -> list[dict[str, Any]]:
projected_rows: list[dict[str, Any]] = []
for component_id, cells in _active_components(_obstacle_map(graph_row)):
if not cells:
continue
points = np.asarray(
[
(
(cell[0] + 0.5) * VOXEL_SIZE_M,
(cell[1] + 0.5) * VOXEL_SIZE_M,
(cell[2] + 0.5) * VOXEL_SIZE_M,
)
for cell in cells
],
dtype=np.float64,
)
projected = project_map_points_kb4(
points,
position_map_xyz=frame.sensor_position_map,
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
profile=frame.projection,
)
inside = (
(projected.pixels_xy[:, 0] >= bbox[0])
& (projected.pixels_xy[:, 0] <= bbox[2])
& (projected.pixels_xy[:, 1] >= bbox[1])
& (projected.pixels_xy[:, 1] <= bbox[3])
)
if not np.any(inside):
continue
depths = projected.depths_m[inside]
projected_rows.append(
{
"component_id": component_id,
"projected_cell_count": int(depths.size),
"nearest_depth_m": round(float(np.min(depths)), 6),
}
)
return sorted(projected_rows, key=lambda row: (row["nearest_depth_m"], row["component_id"]))
def _worker_metrics(result: dict[str, Any]) -> dict[str, Any]:
execution = _object(result.get("execution"), "worker execution")
timing = _object(
_object(result.get("metrics"), "worker metrics").get("pipeline_timing"),
"pipeline timing",
)
geometry = _object(
_object(timing.get("provider_ms"), "provider timing").get("geometry"),
"geometry timing",
)
world = _object(
_object(result.get("metrics"), "worker metrics").get(
"world_state_completion_age_ms"
),
"world timing",
)
return {
"admitted_frames": int(execution["admitted_frames"]),
"delivered_frames": int(execution["delivered_world_states"]),
"fps": float(execution["effective_world_state_fps"]),
"world_p95_ms": float(world["p95"]),
"world_p99_ms": float(world["p99"]),
"geometry_p95_ms": float(geometry["p95"]),
"geometry_p99_ms": float(geometry["p99"]),
}
def _provider_metrics(result: dict[str, Any]) -> dict[str, Any]:
loops = _object(result["execution"]["loops"][0], "worker loop")
providers = _object(loops.get("providers"), "providers")
geometry = _object(providers.get("geometry"), "geometry provider")
temporal = _object(providers.get("temporal"), "temporal provider")
rolling = _object(providers.get("rolling"), "rolling provider")
additive_duration_ns = int(geometry["additive_core_duration_ns"])
completed = int(geometry["completed_frames"])
return {
"additive_observation_count": int(geometry["additive_observation_count"]),
"additive_voxel_count": int(geometry["additive_voxel_count"]),
"frames_with_additions": int(geometry["frames_with_additions"]),
"peak_additive_observations_per_frame": int(
geometry["peak_additive_observations_per_frame"]
),
"peak_candidate_points_per_frame": int(geometry["peak_candidate_points_per_frame"]),
"peak_voxels_per_component": int(geometry["peak_voxels_per_component"]),
"additive_mean_ms_per_frame": round(
additive_duration_ns / max(1, completed) / 1_000_000,
6,
),
"geometry_failed_frames": int(geometry["failed_frames"]),
"temporal_failed_frames": int(temporal["failed_frames"]),
"rolling_capacity_evicted_cells": int(rolling["capacity_evicted_cells"]),
"peak_temporal_components": int(temporal["peak_active_components"]),
"peak_rolling_cells": int(rolling["peak_active_cells"]),
}
def _validate_worker_binding(
result: dict[str, Any],
frames_path: Path,
*,
expected_frames: int,
additive_profile_sha256: str | None,
) -> None:
execution = _object(result.get("execution"), "worker execution")
evidence = _object(execution.get("frame_evidence"), "frame evidence")
identity = _object(result.get("identity"), "worker identity")
inputs = _object(identity.get("inputs"), "worker inputs")
if (
result.get("schema_version") != WORKER_RESULT_SCHEMA
or result.get("completed") is not True
or execution.get("admitted_frames") != expected_frames
or evidence.get("schema_version") != FRAME_EVIDENCE_SCHEMA
or evidence.get("sha256") != _file_sha256(frames_path)
or evidence.get("row_count") != execution.get("delivered_world_states")
or identity.get("worker_id") != "worker-006"
or result.get("authority")
!= {
"actuation_allowed": False,
"candidate_accepted": False,
"commands_enabled": False,
"ground_truth": False,
"navigation_or_safety_accepted": False,
}
):
raise M48R3StaticOccupancyShadowError("worker result binding changed")
if additive_profile_sha256 is None:
if "additive_low_step_profile" in inputs:
raise M48R3StaticOccupancyShadowError("baseline is not native-only")
elif inputs.get("additive_low_step_profile") != additive_profile_sha256:
raise M48R3StaticOccupancyShadowError("candidate profile binding changed")
def _read_worker_result(path: Path) -> dict[str, Any]:
return _read_json(path, maximum=128 * 1024 * 1024)
def _frame_row(value: object) -> dict[str, Any]:
row = _object(value, "frame row")
if row.get("schema_version") != FRAME_EVIDENCE_SCHEMA:
raise M48R3StaticOccupancyShadowError("frame schema changed")
return row
def _sequence(row: dict[str, Any]) -> int:
envelope = _object(row.get("source_envelope"), "source envelope")
value = envelope.get("sequence")
if not isinstance(value, int) or isinstance(value, bool):
raise M48R3StaticOccupancyShadowError("frame sequence changed")
return value
def _obstacle_map(row: dict[str, Any]) -> dict[str, Any]:
delivery = _object(row.get("delivery"), "delivery")
return _object(delivery.get("obstacle_map"), "obstacle map")
def _active_components(obstacle_map: dict[str, Any]) -> list[tuple[str, set[Cell]]]:
result: list[tuple[str, set[Cell]]] = []
for collection in (obstacle_map.get("occupied"), obstacle_map.get("unknown")):
if not isinstance(collection, list):
raise M48R3StaticOccupancyShadowError("obstacle collection changed")
for value in collection:
item = _object(value, "obstacle")
component_id = item.get("component_id")
cells = item.get("cells")
if not isinstance(component_id, str) or not isinstance(cells, list):
raise M48R3StaticOccupancyShadowError("obstacle component changed")
parsed: set[Cell] = set()
for cell in cells:
row = _object(cell, "obstacle cell")
x, y, z = row.get("x"), row.get("y"), row.get("z")
if any(
not isinstance(item, int) or isinstance(item, bool)
for item in (x, y, z)
):
raise M48R3StaticOccupancyShadowError("obstacle cell changed")
assert isinstance(x, int) and isinstance(y, int) and isinstance(z, int)
parsed.add((x, y, z))
if parsed:
result.append((component_id, parsed))
return result
def _cell_union(components: Iterable[tuple[str, set[Cell]]]) -> set[Cell]:
result: set[Cell] = set()
for _, cells in components:
result.update(cells)
return result
def _growth(candidate: int, baseline: int) -> float:
if baseline <= 0:
return math.inf if candidate > 0 else 0.0
return max(0.0, (candidate - baseline) / baseline)
def _rate(rows: list[dict[str, Any]], key: str) -> float:
return sum(bool(row[key]) for row in rows) / len(rows) if rows else 0.0
def _worker_completed_at(result: dict[str, Any]) -> str:
value = result.get("completed_utc_ns")
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
raise M48R3StaticOccupancyShadowError("worker completion time changed")
return datetime.fromtimestamp(value / 1_000_000_000, UTC).isoformat().replace(
"+00:00", "Z"
)
def _publish(
destination: Path,
*,
identity: dict[str, Any],
report: dict[str, Any],
cases: tuple[dict[str, Any], ...],
diff_rows: tuple[dict[str, Any], ...],
candidate_result_path: Path,
candidate_frames_path: Path,
) -> None:
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
staging = destination.parent / f".{destination.name}.{uuid.uuid4().hex}.tmp"
staging.mkdir(mode=0o700)
try:
_write_json(staging / "report.json", report)
_write_jsonl(staging / "cases.jsonl", cases)
_write_jsonl(staging / "frame-diff.jsonl", diff_rows)
shutil.copyfile(candidate_result_path, staging / "worker-result.json")
shutil.copyfile(candidate_frames_path, staging / "frames.jsonl")
artifacts = [
_artifact(staging / name, role)
for name, role in (
("report.json", "m48r3-shadow-report"),
("cases.jsonl", "operator-assisted-anchor-projection"),
("frame-diff.jsonl", "baseline-versus-candidate-occupied-diff"),
("worker-result.json", "worker-load-result"),
("frames.jsonl", "candidate-frame-ledger"),
)
]
manifest = {
"schema_version": M48R3_SHADOW_RESULT_SCHEMA,
"result_id": destination.name,
"identity_sha256": _canonical_sha256(identity),
"identity": identity,
"created_at_utc": identity["created_at_utc"],
"accepted": report["accepted"],
"ground_truth": False,
"authority": dict(_AUTHORITY),
"artifacts": artifacts,
}
_write_json(staging / "manifest.json", manifest)
if destination.exists():
raise M48R3StaticOccupancyShadowError("immutable result already exists")
os.replace(staging, destination)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
def _artifact(path: Path, role: str) -> dict[str, Any]:
return {
"path": path.name,
"role": role,
"byte_length": path.stat().st_size,
"sha256": _file_sha256(path),
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
}
def _read_json(path: Path, *, maximum: int) -> dict[str, Any]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > maximum:
raise M48R3StaticOccupancyShadowError(f"{path.name} is unavailable")
return _object(json.loads(path.read_text("utf-8")), path.name)
def _read_jsonl(path: Path, *, maximum: int) -> list[dict[str, Any]]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > maximum:
raise M48R3StaticOccupancyShadowError(f"{path.name} is unavailable")
return [
_object(json.loads(line), path.name)
for line in path.read_text("utf-8").splitlines()
if line.strip()
]
def _write_json(path: Path, value: object) -> None:
path.write_text(
json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
"utf-8",
)
def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
with path.open("w", encoding="utf-8") as stream:
for row in rows:
stream.write(
json.dumps(
row,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
+ "\n"
)
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode(
"utf-8"
)
).hexdigest()
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise M48R3StaticOccupancyShadowError(f"{label} is invalid")
return value
__all__ = [
"LedgerComparison",
"M48R3StaticOccupancyShadowError",
"M48R3StaticOccupancyShadowResult",
"build_m48r3_static_occupancy_shadow",
"compare_m48r3_frame_ledgers",
"read_m48r3_static_occupancy_shadow",
]
+132 -51
View File
@@ -36,9 +36,7 @@ from .geometry import (
from .geometry_math import POINT_OCCUPIED from .geometry_math import POINT_OCCUPIED
from .providers import SourcePacket from .providers import SourcePacket
M48_LOW_STEP_PROFILE_SCHEMA: Final = ( M48_LOW_STEP_PROFILE_SCHEMA: Final = "missioncore.m48-additive-low-step-occupancy-profile/v1"
"missioncore.m48-additive-low-step-occupancy-profile/v1"
)
M48_LOW_STEP_PROVIDER_ID: Final = "ravnoves00-additive-low-step-geometry/v1" M48_LOW_STEP_PROVIDER_ID: Final = "ravnoves00-additive-low-step-geometry/v1"
IntArray = npt.NDArray[np.int64] IntArray = npt.NDArray[np.int64]
@@ -53,6 +51,10 @@ class LowStepComponentProfile:
voxel_size_m: float voxel_size_m: float
neighbor_radius_cells: int neighbor_radius_cells: int
minimum_points: int minimum_points: int
sparse_persistence_minimum_points: int
sparse_persistence_window_frames: int
sparse_persistence_minimum_hits: int
sparse_persistence_maximum_range_m: float
minimum_voxels: int minimum_voxels: int
local_radius_m: float local_radius_m: float
maximum_candidate_points_per_frame: int maximum_candidate_points_per_frame: int
@@ -65,6 +67,13 @@ class LowStepComponentProfile:
or not 0.05 <= self.voxel_size_m <= 2.0 or not 0.05 <= self.voxel_size_m <= 2.0
or self.neighbor_radius_cells != 1 or self.neighbor_radius_cells != 1
or not 1 <= self.minimum_points <= 256 or not 1 <= self.minimum_points <= 256
or not 1 <= self.sparse_persistence_minimum_points < self.minimum_points
or not 2 <= self.sparse_persistence_window_frames <= 32
or not 2
<= self.sparse_persistence_minimum_hits
<= self.sparse_persistence_window_frames
or not math.isfinite(self.sparse_persistence_maximum_range_m)
or not 1.0 <= self.sparse_persistence_maximum_range_m <= self.local_radius_m
or not 1 <= self.minimum_voxels <= 128 or not 1 <= self.minimum_voxels <= 128
or not math.isfinite(self.local_radius_m) or not math.isfinite(self.local_radius_m)
or not 1.0 <= self.local_radius_m <= 100.0 or not 1.0 <= self.local_radius_m <= 100.0
@@ -156,6 +165,11 @@ class M48AdditiveLowStepGeometryProvider:
self._peak_additive_observations = 0 self._peak_additive_observations = 0
self._peak_component_voxels = 0 self._peak_component_voxels = 0
self._additive_core_duration_ns = 0 self._additive_core_duration_ns = 0
self._sparse_lock = Lock()
self._sparse_history: deque[tuple[int, tuple[frozenset[tuple[int, int, int]], ...]]] = (
deque()
)
self._last_sparse_sequence: int | None = None
def associate( def associate(
self, self,
@@ -175,9 +189,7 @@ class M48AdditiveLowStepGeometryProvider:
except Exception: except Exception:
with self._lock: with self._lock:
self._failed_frames += 1 self._failed_frames += 1
self._additive_core_duration_ns += max( self._additive_core_duration_ns += max(0, time.perf_counter_ns() - started)
0, time.perf_counter_ns() - started
)
raise raise
with self._lock: with self._lock:
self._completed_frames += 1 self._completed_frames += 1
@@ -185,18 +197,10 @@ class M48AdditiveLowStepGeometryProvider:
self._candidate_points += candidate_points self._candidate_points += candidate_points
self._additive_observation_count += len(additive) self._additive_observation_count += len(additive)
self._additive_voxels += voxel_count self._additive_voxels += voxel_count
self._peak_candidate_points = max( self._peak_candidate_points = max(self._peak_candidate_points, candidate_points)
self._peak_candidate_points, candidate_points self._peak_additive_observations = max(self._peak_additive_observations, len(additive))
) self._peak_component_voxels = max(self._peak_component_voxels, peak_component_voxels)
self._peak_additive_observations = max( self._additive_core_duration_ns += max(0, time.perf_counter_ns() - started)
self._peak_additive_observations, len(additive)
)
self._peak_component_voxels = max(
self._peak_component_voxels, peak_component_voxels
)
self._additive_core_duration_ns += max(
0, time.perf_counter_ns() - started
)
return tuple(result) return tuple(result)
def _build_additive_observations( def _build_additive_observations(
@@ -213,9 +217,9 @@ class M48AdditiveLowStepGeometryProvider:
claimed = { claimed = {
point_id for observation in baseline for point_id in observation.source_point_ids point_id for observation in baseline for point_id in observation.source_point_ids
} }
candidate = np.flatnonzero( candidate = np.flatnonzero((step > 0) & (frame.point_class != POINT_OCCUPIED)).astype(
(step > 0) & (frame.point_class != POINT_OCCUPIED) np.int64
).astype(np.int64) )
if claimed and candidate.size: if claimed and candidate.size:
candidate = candidate[ candidate = candidate[
np.fromiter( np.fromiter(
@@ -240,12 +244,19 @@ class M48AdditiveLowStepGeometryProvider:
candidate, candidate,
self.profile.component, self.profile.component,
) )
qualified = tuple( strong = tuple(
item item
for item in components for item in components
if item[0].size >= self.profile.component.minimum_points if item[0].size >= self.profile.component.minimum_points
and item[1] >= self.profile.component.minimum_voxels and item[1] >= self.profile.component.minimum_voxels
) )
persistent_sparse = self._persistent_sparse_components(
sequence=packet.envelope.sequence,
components=components,
points_map=frame.points_map,
sensor_position_map=frame.sensor_position_map,
)
qualified = (*strong, *persistent_sparse)
qualified = tuple( qualified = tuple(
sorted( sorted(
qualified, qualified,
@@ -253,8 +264,7 @@ class M48AdditiveLowStepGeometryProvider:
float( float(
np.min( np.min(
np.linalg.norm( np.linalg.norm(
frame.points_map[item[0]] frame.points_map[item[0]] - frame.sensor_position_map,
- frame.sensor_position_map,
axis=1, axis=1,
) )
) )
@@ -279,17 +289,11 @@ class M48AdditiveLowStepGeometryProvider:
points = frame.points_map[indices] points = frame.points_map[indices]
centroid = np.median(points, axis=0) centroid = np.median(points, axis=0)
covariance = points.var(axis=0) covariance = points.var(axis=0)
nearest = float( nearest = float(np.min(np.linalg.norm(points - frame.sensor_position_map, axis=1)))
np.min(np.linalg.norm(points - frame.sensor_position_map, axis=1))
)
observations.append( observations.append(
ObstacleObservation( ObstacleObservation(
observation_id=( observation_id=(f"{packet.envelope.frame_id}:low-step:{component_index}"),
f"{packet.envelope.frame_id}:low-step:{component_index}" occupancy_key=(f"{packet.envelope.frame_id}:low-step:{component_index}"),
),
occupancy_key=(
f"{packet.envelope.frame_id}:low-step:{component_index}"
),
source_id=packet.envelope.source_id, source_id=packet.envelope.source_id,
frame_id=packet.envelope.frame_id, frame_id=packet.envelope.frame_id,
evidence_time_ns=packet.envelope.timestamps.source_ns, evidence_time_ns=packet.envelope.timestamps.source_ns,
@@ -323,6 +327,65 @@ class M48AdditiveLowStepGeometryProvider:
peak_voxels = max(peak_voxels, cells) peak_voxels = max(peak_voxels, cells)
return tuple(observations), candidate_count, voxel_count, peak_voxels return tuple(observations), candidate_count, voxel_count, peak_voxels
def _persistent_sparse_components(
self,
*,
sequence: int,
components: tuple[tuple[IntArray, int], ...],
points_map: npt.NDArray[np.float64],
sensor_position_map: npt.NDArray[np.float64],
) -> tuple[tuple[IntArray, int], ...]:
"""Promote only weak geometry repeated in a bounded causal window."""
component_profile = self.profile.component
current = tuple(
(
item,
_component_cells(
points_map,
item[0],
voxel_size_m=component_profile.voxel_size_m,
),
)
for item in components
if item[0].size >= component_profile.sparse_persistence_minimum_points
and item[1] >= component_profile.minimum_voxels
)
with self._sparse_lock:
if self._last_sparse_sequence is not None and (
sequence <= self._last_sparse_sequence
or sequence - self._last_sparse_sequence
> component_profile.sparse_persistence_window_frames
):
self._sparse_history.clear()
first_allowed = sequence - component_profile.sparse_persistence_window_frames + 1
while self._sparse_history and self._sparse_history[0][0] < first_allowed:
self._sparse_history.popleft()
promoted: list[tuple[IntArray, int]] = []
for item, cells in current:
if item[0].size >= component_profile.minimum_points:
continue
hit_count = 1 + sum(
any(not cells.isdisjoint(previous) for previous in previous_components)
for _, previous_components in self._sparse_history
)
if (
hit_count >= component_profile.sparse_persistence_minimum_hits
and float(
np.min(
np.linalg.norm(
points_map[item[0]] - sensor_position_map,
axis=1,
)
)
)
<= component_profile.sparse_persistence_maximum_range_m
):
promoted.append(item)
self._sparse_history.append((sequence, tuple(cells for _, cells in current)))
self._last_sparse_sequence = sequence
return tuple(promoted)
def snapshot(self) -> M48LowStepOccupancySnapshot: def snapshot(self) -> M48LowStepOccupancySnapshot:
with self._lock: with self._lock:
return M48LowStepOccupancySnapshot( return M48LowStepOccupancySnapshot(
@@ -335,9 +398,7 @@ class M48AdditiveLowStepGeometryProvider:
additive_observation_count=self._additive_observation_count, additive_observation_count=self._additive_observation_count,
additive_voxel_count=self._additive_voxels, additive_voxel_count=self._additive_voxels,
peak_candidate_points_per_frame=self._peak_candidate_points, peak_candidate_points_per_frame=self._peak_candidate_points,
peak_additive_observations_per_frame=( peak_additive_observations_per_frame=(self._peak_additive_observations),
self._peak_additive_observations
),
peak_voxels_per_component=self._peak_component_voxels, peak_voxels_per_component=self._peak_component_voxels,
additive_core_duration_ns=self._additive_core_duration_ns, additive_core_duration_ns=self._additive_core_duration_ns,
) )
@@ -401,6 +462,10 @@ def load_m48_low_step_occupancy_profile(
"voxel_size_m", "voxel_size_m",
"neighbor_radius_cells", "neighbor_radius_cells",
"minimum_points", "minimum_points",
"sparse_persistence_minimum_points",
"sparse_persistence_window_frames",
"sparse_persistence_minimum_hits",
"sparse_persistence_maximum_range_m",
"minimum_voxels", "minimum_voxels",
"local_radius_m", "local_radius_m",
"maximum_candidate_points_per_frame", "maximum_candidate_points_per_frame",
@@ -460,9 +525,7 @@ def load_m48_low_step_occupancy_profile(
if _number(acceptance, key) < 0.0: if _number(acceptance, key) < 0.0:
raise M48LowStepOccupancyError("low-step acceptance bounds are invalid") raise M48LowStepOccupancyError("low-step acceptance bounds are invalid")
if ( if (
not _string(source, "m48r2_result_id").startswith( not _string(source, "m48r2_result_id").startswith("m48-static-occupancy-qualification-")
"m48-static-occupancy-qualification-"
)
or len(_string(source, "m48r2_result_id")) or len(_string(source, "m48r2_result_id"))
!= len("m48-static-occupancy-qualification-") + 64 != len("m48-static-occupancy-qualification-") + 64
): ):
@@ -500,9 +563,7 @@ def load_m48_low_step_occupancy_profile(
LowStepSeparationExpectation( LowStepSeparationExpectation(
anchor_id=_string(item, "anchor_id"), anchor_id=_string(item, "anchor_id"),
sequence=_positive_integer(item, "sequence"), sequence=_positive_integer(item, "sequence"),
expected_minimum_components=_positive_integer( expected_minimum_components=_positive_integer(item, "expected_minimum_components"),
item, "expected_minimum_components"
),
interpretation=_string(item, "interpretation"), interpretation=_string(item, "interpretation"),
) )
) )
@@ -519,18 +580,30 @@ def load_m48_low_step_occupancy_profile(
base_geometry_profile_sha256=_digest(base, "sha256"), base_geometry_profile_sha256=_digest(base, "sha256"),
component=LowStepComponentProfile( component=LowStepComponentProfile(
voxel_size_m=_number(component, "voxel_size_m"), voxel_size_m=_number(component, "voxel_size_m"),
neighbor_radius_cells=_positive_integer( neighbor_radius_cells=_positive_integer(component, "neighbor_radius_cells"),
component, "neighbor_radius_cells"
),
minimum_points=_positive_integer(component, "minimum_points"), minimum_points=_positive_integer(component, "minimum_points"),
sparse_persistence_minimum_points=_positive_integer(
component,
"sparse_persistence_minimum_points",
),
sparse_persistence_window_frames=_positive_integer(
component,
"sparse_persistence_window_frames",
),
sparse_persistence_minimum_hits=_positive_integer(
component,
"sparse_persistence_minimum_hits",
),
sparse_persistence_maximum_range_m=_number(
component,
"sparse_persistence_maximum_range_m",
),
minimum_voxels=_positive_integer(component, "minimum_voxels"), minimum_voxels=_positive_integer(component, "minimum_voxels"),
local_radius_m=_number(component, "local_radius_m"), local_radius_m=_number(component, "local_radius_m"),
maximum_candidate_points_per_frame=_positive_integer( maximum_candidate_points_per_frame=_positive_integer(
component, "maximum_candidate_points_per_frame" component, "maximum_candidate_points_per_frame"
), ),
maximum_cells_per_component=_positive_integer( maximum_cells_per_component=_positive_integer(component, "maximum_cells_per_component"),
component, "maximum_cells_per_component"
),
maximum_components_per_frame=_positive_integer( maximum_components_per_frame=_positive_integer(
component, "maximum_components_per_frame" component, "maximum_components_per_frame"
), ),
@@ -547,9 +620,7 @@ def _voxel_components(
) -> tuple[tuple[IntArray, int], ...]: ) -> tuple[tuple[IntArray, int], ...]:
if source_indices.size == 0: if source_indices.size == 0:
return () return ()
cells = np.floor( cells = np.floor(points_map[source_indices] / profile.voxel_size_m).astype(np.int64)
points_map[source_indices] / profile.voxel_size_m
).astype(np.int64)
cell_points: dict[tuple[int, int, int], list[int]] = {} cell_points: dict[tuple[int, int, int], list[int]] = {}
for local_index, row in enumerate(cells): for local_index, row in enumerate(cells):
key = (int(row[0]), int(row[1]), int(row[2])) key = (int(row[0]), int(row[1]), int(row[2]))
@@ -591,6 +662,16 @@ def _voxel_components(
return tuple(components) return tuple(components)
def _component_cells(
points_map: npt.NDArray[np.float64],
source_indices: IntArray,
*,
voxel_size_m: float,
) -> frozenset[tuple[int, int, int]]:
rows = np.floor(points_map[source_indices] / voxel_size_m).astype(np.int64)
return frozenset((int(row[0]), int(row[1]), int(row[2])) for row in rows)
def _object(value: object, label: str) -> dict[str, object]: def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise M48LowStepOccupancyError(f"{label} must be an object") raise M48LowStepOccupancyError(f"{label} must be an object")
@@ -70,6 +70,7 @@ class M48sReplayTimeline:
frames_name: str = "reference-graph-replay-frames.jsonl", frames_name: str = "reference-graph-replay-frames.jsonl",
worker_result_name: str = "reference-graph-replay-worker-result.json", worker_result_name: str = "reference-graph-replay-worker-result.json",
frame_evidence_schema: str = FRAME_EVIDENCE_SCHEMA, frame_evidence_schema: str = FRAME_EVIDENCE_SCHEMA,
frame_diff_name: str | None = None,
camera_endpoint_root: str = ( camera_endpoint_root: str = (
"/api/v1/laboratory/m48s/fixed-class-detector" "/api/v1/laboratory/m48s/fixed-class-detector"
), ),
@@ -80,6 +81,10 @@ class M48sReplayTimeline:
if ( if (
Path(frames_name).name != frames_name Path(frames_name).name != frames_name
or Path(worker_result_name).name != worker_result_name or Path(worker_result_name).name != worker_result_name
or (
frame_diff_name is not None
and Path(frame_diff_name).name != frame_diff_name
)
or frame_evidence_schema not in { or frame_evidence_schema not in {
FRAME_EVIDENCE_SCHEMA, FRAME_EVIDENCE_SCHEMA,
M48R3_FRAME_EVIDENCE_SCHEMA, M48R3_FRAME_EVIDENCE_SCHEMA,
@@ -92,11 +97,23 @@ class M48sReplayTimeline:
self.camera_endpoint_root = camera_endpoint_root self.camera_endpoint_root = camera_endpoint_root
self.frames_path = (self.result_root / frames_name).resolve(strict=True) self.frames_path = (self.result_root / frames_name).resolve(strict=True)
self.worker_path = (self.result_root / worker_result_name).resolve(strict=True) self.worker_path = (self.result_root / worker_result_name).resolve(strict=True)
self.frame_diff_path = (
None
if frame_diff_name is None
else (self.result_root / frame_diff_name).resolve(strict=True)
)
if ( if (
self.frames_path.parent != self.result_root self.frames_path.parent != self.result_root
or self.worker_path.parent != self.result_root or self.worker_path.parent != self.result_root
or self.frames_path.is_symlink() or self.frames_path.is_symlink()
or self.worker_path.is_symlink() or self.worker_path.is_symlink()
or (
self.frame_diff_path is not None
and (
self.frame_diff_path.parent != self.result_root
or self.frame_diff_path.is_symlink()
)
)
): ):
raise M48sReplayTimelineError("M4.8S replay artifacts are invalid") raise M48sReplayTimelineError("M4.8S replay artifacts are invalid")
self.profile = load_replay_threat_profile( self.profile = load_replay_threat_profile(
@@ -123,6 +140,11 @@ class M48sReplayTimeline:
self.outcomes, self.outcomes,
frame_evidence_schema=self.frame_evidence_schema, frame_evidence_schema=self.frame_evidence_schema,
) )
self.frame_diff_offsets = (
{}
if self.frame_diff_path is None
else _index_frame_diff(self.frame_diff_path)
)
self._cache_lock = Lock() self._cache_lock = Lock()
self._chunk_json_cache: OrderedDict[tuple[int, int], bytes] = OrderedDict() self._chunk_json_cache: OrderedDict[tuple[int, int], bytes] = OrderedDict()
self._camera_point_json_cache: OrderedDict[int, bytes] = OrderedDict() self._camera_point_json_cache: OrderedDict[int, bytes] = OrderedDict()
@@ -156,6 +178,11 @@ class M48sReplayTimeline:
"camera_point_window_seconds": CAMERA_ACCUMULATION_WINDOW_SECONDS, "camera_point_window_seconds": CAMERA_ACCUMULATION_WINDOW_SECONDS,
"camera_point_sample_limit": CAMERA_ACCUMULATION_POINT_LIMIT, "camera_point_sample_limit": CAMERA_ACCUMULATION_POINT_LIMIT,
"world_state_delivery": "source-paced-latest-wins", "world_state_delivery": "source-paced-latest-wins",
"occupancy_provenance_delivery": (
"baseline-versus-additive-component-diff"
if self.frame_diff_path is not None
else None
),
"world_state_frame_count": len(self.index.offsets_by_sequence), "world_state_frame_count": len(self.index.offsets_by_sequence),
"superseded_frame_count": sum( "superseded_frame_count": sum(
value == "superseded" for value in self.outcomes.values() value == "superseded" for value in self.outcomes.values()
@@ -399,6 +426,12 @@ class M48sReplayTimeline:
body_frame, body_frame,
occupied_voxel_size_m=self.profile.corridor.occupied_voxel_size_m, occupied_voxel_size_m=self.profile.corridor.occupied_voxel_size_m,
) )
provenance = self._component_provenance(sequence)
for visual in metric_visuals:
visual["occupancy_source"] = provenance.get(
str(visual["component_id"]),
"baseline",
)
associated = set(_strings(row.get("associated_proposal_ids"), "associated ids")) associated = set(_strings(row.get("associated_proposal_ids"), "associated ids"))
for proposal in _objects(row.get("detector_proposals"), "detector proposals"): for proposal in _objects(row.get("detector_proposals"), "detector proposals"):
proposal_id = _text(proposal.get("proposal_id"), "proposal id") proposal_id = _text(proposal.get("proposal_id"), "proposal id")
@@ -480,6 +513,27 @@ class M48sReplayTimeline:
raise M48sReplayTimelineError("M4.8S frame row is invalid") raise M48sReplayTimelineError("M4.8S frame row is invalid")
return value return value
def _component_provenance(self, sequence: int) -> dict[str, str]:
if self.frame_diff_path is None:
return {}
offset = self.frame_diff_offsets.get(sequence)
if offset is None:
raise M48sReplayTimelineError("M4.8R3 frame diff is incomplete")
with self.frame_diff_path.open("rb") as stream:
stream.seek(offset)
line = stream.readline()
value = _object(json.loads(line), "frame diff")
if value.get("sequence") != sequence:
raise M48sReplayTimelineError("M4.8R3 frame diff binding changed")
provenance = value.get("component_provenance")
if not isinstance(provenance, dict) or any(
not isinstance(key, str)
or item not in {"mixed", "additive-low-step"}
for key, item in provenance.items()
):
raise M48sReplayTimelineError("M4.8R3 component provenance changed")
return provenance
def _index_ledger( def _index_ledger(
path: Path, path: Path,
@@ -517,6 +571,33 @@ def _index_ledger(
return _LedgerIndex(offsets) return _LedgerIndex(offsets)
def _index_frame_diff(path: Path) -> dict[int, int]:
offsets: dict[int, int] = {}
with path.open("rb") as stream:
while True:
offset = stream.tell()
line = stream.readline()
if not line:
break
try:
value = json.loads(line)
except json.JSONDecodeError:
raise M48sReplayTimelineError("M4.8R3 frame diff is invalid") from None
if not isinstance(value, dict):
raise M48sReplayTimelineError("M4.8R3 frame diff is invalid")
sequence = value.get("sequence")
if (
not isinstance(sequence, int)
or isinstance(sequence, bool)
or sequence != len(offsets)
):
raise M48sReplayTimelineError("M4.8R3 frame diff sequence changed")
offsets[sequence] = offset
if len(offsets) != EXPECTED_FRAME_COUNT:
raise M48sReplayTimelineError("M4.8R3 frame diff is incomplete")
return offsets
def _ledger_source_envelope( def _ledger_source_envelope(
line: bytes, line: bytes,
*, *,
+23 -3
View File
@@ -297,9 +297,17 @@ class SimulationProjectStore:
result: dict[str, Any], result: dict[str, Any],
artifacts: list[dict[str, Any]], artifacts: list[dict[str, Any]],
world_manifest: dict[str, Any], world_manifest: dict[str, Any],
provider_job_id: str | None = None,
provider_progress: object = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
with self._lock: with self._lock:
document = self._read(project_id) document = self._read(project_id)
if provider_job_id is not None:
if PROVIDER_JOB_ID_PATTERN.fullmatch(provider_job_id) is None:
raise SimulationProjectError("simulation provider job id is invalid")
document["provider"]["job_id"] = provider_job_id
if provider_progress is not None:
document["provider"]["progress"] = provider_progress
document["status"] = "ready" document["status"] = "ready"
document["provider"]["state"] = "ready" document["provider"]["state"] = "ready"
document["provider"]["runtime"] = result.get("runtime") document["provider"]["runtime"] = result.get("runtime")
@@ -464,10 +472,10 @@ class SimulationProjectService:
"outputs": { "outputs": {
"preview_sog": True, "preview_sog": True,
"streamed_sog": True, "streamed_sog": True,
"collision": False, "collision": True,
}, },
"preview_lod": "coarsest", "preview_lod": "coarsest",
"collision_profile": None, "collision_profile": _collision_profile(str(project["scene_type"])),
} }
submitted = provider.submit_build(request) submitted = provider.submit_build(request)
job_id = submitted.get("job_id") job_id = submitted.get("job_id")
@@ -607,12 +615,24 @@ def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[st
"available": url_for("collision-mesh") is not None, "available": url_for("collision-mesh") is not None,
}, },
"transforms": { "transforms": {
"world_from_visual": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], "world_from_visual": [1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1],
"world_from_collision": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], "world_from_collision": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
}, },
} }
def _collision_profile(scene_type: str) -> dict[str, Any]:
"""Build the portable walkable-volume contract around the scanner origin."""
return {
"scene_type": scene_type,
"seed_position": [0, 1, 0],
"capsule_height": 1.6,
"capsule_radius": 0.2,
"voxel_size": 0.05,
"mesh_shape": "smooth",
}
def _project_name(value: str) -> str: def _project_name(value: str) -> str:
normalized = " ".join(value.split()) normalized = " ".join(value.split())
if not 1 <= len(normalized) <= 120: if not 1 <= len(normalized) <= 120:
+20
View File
@@ -126,6 +126,9 @@ from k1link.web.lidar_api import build_lidar_router
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
from k1link.web.m48_object_quality_api import build_m48_object_quality_router from k1link.web.m48_object_quality_api import build_m48_object_quality_router
from k1link.web.m48r3_static_occupancy_api import (
build_m48r3_static_occupancy_router,
)
from k1link.web.m48s_fixed_class_detector_lab_api import ( from k1link.web.m48s_fixed_class_detector_lab_api import (
build_m48s_fixed_class_detector_lab_router, build_m48s_fixed_class_detector_lab_router,
) )
@@ -961,6 +964,23 @@ app.include_router(
), ),
) )
) )
app.include_router(
build_m48r3_static_occupancy_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m48"
/ "static-occupancy-shadow-results"
),
repository_root_provider=lambda: REPOSITORY_ROOT,
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
)
)
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,290 @@
"""Read-only API for sealed M4.8R3 static-occupancy Worker shadows."""
from __future__ import annotations
import copy
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Final
from fastapi import APIRouter, HTTPException, Query, Response
from k1link.laboratory.m48r3_static_occupancy_shadow import (
M48R3_SHADOW_PREFIX,
M48R3StaticOccupancyShadowError,
M48R3StaticOccupancyShadowResult,
read_m48r3_static_occupancy_shadow,
)
from k1link.perception.m48s_replay_timeline import (
M48R3_FRAME_EVIDENCE_SCHEMA,
M48sReplayTimeline,
M48sReplayTimelineError,
)
from k1link.perception.threat_timeline import RECORDED_SPATIAL_MAX_CHUNK_FRAMES
from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
RootProvider = Callable[[], Path | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
RESULT_ID: Final = re.compile(rf"^{re.escape(M48R3_SHADOW_PREFIX)}[a-f0-9]{{64}}$")
RESULT_VIEW_SCHEMA: Final = "missioncore.m48r3-static-occupancy-shadow-view/v1"
RESULT_CATALOG_SCHEMA: Final = "missioncore.m48r3-static-occupancy-shadow-catalog/v1"
CASE_CATALOG_SCHEMA: Final = "missioncore.m48r3-static-occupancy-shadow-cases/v1"
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m48r3/static-occupancy"
def build_m48r3_static_occupancy_router(
*,
root_provider: RootProvider = lambda: None,
repository_root_provider: RootProvider = lambda: None,
camera_frame_provider: CameraFrameProvider | None = None,
) -> APIRouter:
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
def result(result_id: str) -> M48R3StaticOccupancyShadowResult:
candidate = _resolve_candidate(root_provider, result_id)
try:
return _read_result_cached(str(candidate), _result_signature(candidate))
except (M48R3StaticOccupancyShadowError, OSError, ValueError):
raise HTTPException(status_code=404, detail="M4.8R3 result not found") from None
def timeline(result_id: str) -> M48sReplayTimeline:
candidate = _resolve_candidate(root_provider, result_id)
repository = _configured_root(repository_root_provider)
if repository is None:
raise HTTPException(status_code=503, detail="M4.8R3 timeline source unavailable")
result(result_id)
try:
return _read_timeline_cached(
str(repository),
str(candidate),
result_id,
_timeline_signature(candidate),
)
except (M48sReplayTimelineError, OSError, ValueError):
raise HTTPException(
status_code=503,
detail="M4.8R3 bounded timeline failed verification",
) 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 _empty_catalog(configured=False)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in sorted(root.iterdir()):
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
continue
try:
sealed = _read_result_cached(str(candidate), _result_signature(candidate))
items.append(_project_result(sealed))
except (M48R3StaticOccupancyShadowError, OSError, ValueError):
invalid_total += 1
items.sort(
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
reverse=True,
)
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"candidate_total": len(items) + invalid_total,
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]:
return _project_result(result(result_id))
@router.get("/{result_id}/cases")
def get_cases(result_id: str) -> dict[str, object]:
sealed = result(result_id)
return {
"schema_version": CASE_CATALOG_SCHEMA,
"result_id": result_id,
"cases": copy.deepcopy(sealed.cases),
"case_count": len(sealed.cases),
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only",
}
@router.get("/{result_id}/timeline")
def get_timeline(result_id: str) -> dict[str, object]:
return copy.deepcopy(timeline(result_id).metadata())
@router.get("/{result_id}/timeline/chunk")
def get_timeline_chunk(
result_id: str,
start: int = Query(default=0, ge=0),
count: int = Query(default=12, ge=1, le=RECORDED_SPATIAL_MAX_CHUNK_FRAMES),
) -> Response:
try:
content = timeline(result_id).chunk_json(
start_sequence=start,
frame_count=count,
)
except M48sReplayTimelineError:
raise HTTPException(status_code=404, detail="M4.8R3 chunk not found") from None
return _immutable_json(content)
@router.get("/{result_id}/timeline/frames/{sequence}/camera-points")
def get_camera_points(result_id: str, sequence: int) -> Response:
try:
content = timeline(result_id).camera_point_overlay_json(sequence=sequence)
except M48sReplayTimelineError:
raise HTTPException(
status_code=404,
detail="M4.8R3 camera points not found",
) from None
return _immutable_json(content)
@router.get("/{result_id}/timeline/frames/{sequence}/camera")
def get_camera(result_id: str, sequence: int) -> Response:
if camera_frame_provider is None:
raise HTTPException(status_code=503, detail="M4.8R3 camera decoder unavailable")
projected = timeline(result_id)
if not 0 <= sequence < len(projected.source_times_ns):
raise HTTPException(status_code=404, detail="M4.8R3 frame not found")
try:
camera = camera_frame_provider(projected.profile.session_id, sequence)
except (OSError, SessionIntegrityError, ValueError):
raise HTTPException(status_code=503, detail="M4.8R3 camera unavailable") from None
if camera.width != 800 or camera.height != 600:
raise HTTPException(status_code=503, detail="M4.8R3 camera size changed")
return Response(
content=camera.payload,
media_type=camera.media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{camera.sha256}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
@lru_cache(maxsize=2)
def _read_result_cached(
result_root: str,
signature: tuple[int, ...],
) -> M48R3StaticOccupancyShadowResult:
del signature
return read_m48r3_static_occupancy_shadow(Path(result_root))
@lru_cache(maxsize=2)
def _read_timeline_cached(
repository_root: str,
result_root: str,
result_id: str,
signature: tuple[int, ...],
) -> M48sReplayTimeline:
del signature
return M48sReplayTimeline(
repository_root=Path(repository_root),
result_root=Path(result_root),
result_id=result_id,
frames_name="frames.jsonl",
worker_result_name="worker-result.json",
frame_evidence_schema=M48R3_FRAME_EVIDENCE_SCHEMA,
frame_diff_name="frame-diff.jsonl",
camera_endpoint_root=ENDPOINT_ROOT,
)
def _project_result(result: M48R3StaticOccupancyShadowResult) -> dict[str, object]:
return {
"schema_version": RESULT_VIEW_SCHEMA,
"result_id": result.result_id,
"created_at_utc": result.manifest["created_at_utc"],
"accepted": result.manifest["accepted"],
"profile": copy.deepcopy(result.report["profile"]),
"metrics": copy.deepcopy(result.report["metrics"]),
"gates": copy.deepcopy(result.report["gates"]),
"decision": copy.deepcopy(result.report["decision"]),
"limitations": copy.deepcopy(result.report["limitations"]),
"ground_truth": False,
"authority": copy.deepcopy(result.report["authority"]),
"access": "read-only",
}
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
if RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M4.8R3 result not found")
root = _configured_root(provider)
if root is None:
raise HTTPException(status_code=404, detail="M4.8R3 result not found")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="M4.8R3 result not found")
resolved = candidate.resolve(strict=True)
if resolved.parent != root:
raise HTTPException(status_code=404, detail="M4.8R3 result not found")
return resolved
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
if value.is_symlink() or not value.is_dir():
return None
return value.resolve(strict=True)
def _result_signature(candidate: Path) -> tuple[int, ...]:
return _signature(
candidate,
("manifest.json", "report.json", "cases.jsonl", "worker-result.json", "frames.jsonl"),
)
def _timeline_signature(candidate: Path) -> tuple[int, ...]:
return _signature(
candidate,
("worker-result.json", "frames.jsonl", "frame-diff.jsonl"),
)
def _signature(candidate: Path, names: tuple[str, ...]) -> tuple[int, ...]:
result: list[int] = []
for name in names:
path = candidate / name
if path.is_symlink() or not path.is_file():
raise ValueError("M4.8R3 artifact unavailable")
stat = path.stat()
result.extend((stat.st_size, stat.st_mtime_ns))
return tuple(result)
def _immutable_json(content: bytes) -> Response:
return Response(
content=content,
media_type="application/json",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
)
def _empty_catalog(*, configured: bool) -> dict[str, object]:
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": configured,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
__all__ = ["build_m48r3_static_occupancy_router"]
+90 -15
View File
@@ -23,10 +23,7 @@ from k1link.perception.providers import SourcePacket
from k1link.perception.recorded_source import RecordedFrameReference from k1link.perception.recorded_source import RecordedFrameReference
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = ( PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m48r3-additive-low-step-occupancy-v1.json"
REPOSITORY_ROOT
/ "config/perception/m48r3-additive-low-step-occupancy-v1.json"
)
R2_CASES_PATH = ( R2_CASES_PATH = (
REPOSITORY_ROOT REPOSITORY_ROOT
/ ".runtime/compute-experiments/m48/static-occupancy-qualification-results" / ".runtime/compute-experiments/m48/static-occupancy-qualification-results"
@@ -126,9 +123,7 @@ def test_wide_operator_region_cannot_bridge_two_spatial_components() -> None:
observations = provider.associate(_packet(), ()) observations = provider.associate(_packet(), ())
additive = tuple( additive = tuple(
item item for item in observations if "additive-low-step-current-component" in item.reason_codes
for item in observations
if "additive-low-step-current-component" in item.reason_codes
) )
assert len(additive) == 2 assert len(additive) == 2
@@ -144,6 +139,42 @@ def test_wide_operator_region_cannot_bridge_two_spatial_components() -> None:
assert snapshot.failed_frames == 0 assert snapshot.failed_frames == 0
def test_sparse_component_requires_bounded_causal_persistence() -> None:
points = np.asarray(
((0.00, 0.0, 5.00), (0.04, 0.0, 5.00)),
dtype=np.float64,
)
store = _Store(_frame(points), np.ones(2, dtype=np.uint8))
provider = M48AdditiveLowStepGeometryProvider( # type: ignore[arg-type]
store=store,
profile=load_m48_low_step_occupancy_profile(PROFILE_PATH),
)
first_five = tuple(provider.associate(_packet(sequence), ()) for sequence in range(5))
sixth = provider.associate(_packet(5), ())
assert first_five == ((), (), (), (), ())
assert len(sixth) == 1
assert sixth[0].source_point_ids == (0, 1)
assert "additive-low-step-current-component" in sixth[0].reason_codes
def test_persistent_sparse_component_is_bounded_to_critical_range() -> None:
points = np.asarray(
((0.00, 0.0, 8.20), (0.04, 0.0, 8.20)),
dtype=np.float64,
)
store = _Store(_frame(points), np.ones(2, dtype=np.uint8))
provider = M48AdditiveLowStepGeometryProvider( # type: ignore[arg-type]
store=store,
profile=load_m48_low_step_occupancy_profile(PROFILE_PATH),
)
observations = tuple(provider.associate(_packet(sequence), ()) for sequence in range(6))
assert observations == ((), (), (), (), (), ())
def test_frame_1856_preserves_baseline_posts_and_splits_low_hemisphere_support() -> None: def test_frame_1856_preserves_baseline_posts_and_splits_low_hemisphere_support() -> None:
store = RecordedGeometryStore.from_repository(REPOSITORY_ROOT) store = RecordedGeometryStore.from_repository(REPOSITORY_ROOT)
provider = M48AdditiveLowStepGeometryProvider( provider = M48AdditiveLowStepGeometryProvider(
@@ -163,8 +194,7 @@ def test_frame_1856_preserves_baseline_posts_and_splits_low_hemisphere_support()
profile=frame.projection, profile=frame.projection,
) )
source_rows = { source_rows = {
int(source_index): row int(source_index): row for row, source_index in enumerate(projected.source_indices)
for row, source_index in enumerate(projected.source_indices)
} }
cases = [ cases = [
json.loads(line) json.loads(line)
@@ -176,14 +206,62 @@ def test_frame_1856_preserves_baseline_posts_and_splits_low_hemisphere_support()
hemispheres = by_anchor["anchor-924a4623077fe5df18816b47"] hemispheres = by_anchor["anchor-924a4623077fe5df18816b47"]
assert posts["accepted_graph"]["component_count"] >= 2 assert posts["accepted_graph"]["component_count"] >= 2
assert _component_hits( assert (
_component_hits(
observations, observations,
hemispheres["extent_xyxy"], hemispheres["extent_xyxy"],
projected.pixels_xy, projected.pixels_xy,
source_rows, source_rows,
width=frame.projection.width, width=frame.projection.width,
height=frame.projection.height, height=frame.projection.height,
) >= 2 )
>= 2
)
def test_exact_six_frame_persistence_recovers_critical_near_anchor() -> None:
store = RecordedGeometryStore.from_repository(REPOSITORY_ROOT)
provider = M48AdditiveLowStepGeometryProvider(
store=store,
profile=load_m48_low_step_occupancy_profile(PROFILE_PATH),
)
observations: tuple[ObstacleObservation, ...] = ()
for source_sequence in range(1084, 1093):
observations = provider.associate(_packet(source_sequence), ())
evidence_frame = store.frame_for_index(1092)
target_frame = store.frame_for_index(1093)
assert evidence_frame is not None
assert target_frame is not None
projected = project_map_points_kb4(
evidence_frame.points_map,
position_map_xyz=target_frame.sensor_position_map,
orientation_map_from_lidar_xyzw=target_frame.sensor_orientation_xyzw,
profile=target_frame.projection,
)
source_rows = {
int(source_index): row for row, source_index in enumerate(projected.source_indices)
}
critical = next(
json.loads(line)
for line in R2_CASES_PATH.read_text("utf-8").splitlines()
if "anchor-0df056d9c565b74a25d3cca3" in line
)
additive = tuple(
item for item in observations if "additive-low-step-current-component" in item.reason_codes
)
assert (
_component_hits(
additive,
critical["extent_xyxy"],
projected.pixels_xy,
source_rows,
width=target_frame.projection.width,
height=target_frame.projection.height,
)
>= 1
)
def _component_hits( def _component_hits(
@@ -207,9 +285,6 @@ def _component_hits(
if not indices: if not indices:
continue continue
rows = [source_rows[index] for index in indices if index in source_rows] rows = [source_rows[index] for index in indices if index in source_rows]
if any( if any(x1 <= pixels[row, 0] <= x2 and y1 <= pixels[row, 1] <= y2 for row in rows):
x1 <= pixels[row, 0] <= x2 and y1 <= pixels[row, 1] <= y2
for row in rows
):
count += 1 count += 1
return count return count
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.laboratory.m48r3_static_occupancy_shadow import (
M48R3StaticOccupancyShadowResult,
)
from k1link.web import m48r3_static_occupancy_api as api
def test_m48r3_api_projects_result_cases_and_provenance_timeline(
tmp_path: Path,
monkeypatch,
) -> None:
result_id = "m48r3-static-occupancy-shadow-" + "0" * 64
root = tmp_path / "results"
result_root = root / result_id
result_root.mkdir(parents=True)
for name in (
"manifest.json",
"report.json",
"cases.jsonl",
"worker-result.json",
"frames.jsonl",
"frame-diff.jsonl",
):
(result_root / name).write_text("{}\n", encoding="utf-8")
sealed = M48R3StaticOccupancyShadowResult(
result_id=result_id,
result_root=result_root,
manifest={"created_at_utc": "2026-08-26T00:00:00Z", "accepted": True},
report={
"profile": {"id": "profile"},
"metrics": {"frames": {"candidate_delivered": 4489}},
"gates": {"complete_frame_accounting": True},
"decision": {"state": "accepted-bounded-worker-shadow"},
"limitations": ["replay only"],
"authority": {"mode": "replay-simulated"},
},
cases=({"anchor_id": "anchor", "source_sequence": 1855},),
)
class Timeline:
source_times_ns = tuple(range(4489))
profile = SimpleNamespace(session_id="20260720T065719Z_viewer_live")
def metadata(self) -> dict[str, object]:
return {
"schema_version": "missioncore.recorded-spatial-evidence-timeline/v1",
"result_id": result_id,
"occupancy_provenance_delivery": (
"baseline-versus-additive-component-diff"
),
}
def chunk_json(self, *, start_sequence: int, frame_count: int) -> bytes:
return json.dumps(
{"start_sequence": start_sequence, "frame_count": frame_count}
).encode()
def camera_point_overlay_json(self, *, sequence: int) -> bytes:
return json.dumps({"sequence": sequence}).encode()
monkeypatch.setattr(api, "_read_result_cached", lambda *_args: sealed)
monkeypatch.setattr(api, "_read_timeline_cached", lambda *_args: Timeline())
app = FastAPI()
app.include_router(
api.build_m48r3_static_occupancy_router(
root_provider=lambda: root,
repository_root_provider=lambda: tmp_path,
)
)
client = TestClient(app)
catalog = client.get("/api/v1/laboratory/m48r3/static-occupancy/results")
assert catalog.status_code == 200
assert catalog.json()["items"][0]["result_id"] == result_id
result = client.get(f"/api/v1/laboratory/m48r3/static-occupancy/{result_id}")
assert result.status_code == 200
assert result.json()["accepted"] is True
cases = client.get(f"/api/v1/laboratory/m48r3/static-occupancy/{result_id}/cases")
assert cases.status_code == 200
assert cases.json()["cases"][0]["source_sequence"] == 1855
timeline = client.get(
f"/api/v1/laboratory/m48r3/static-occupancy/{result_id}/timeline"
)
assert timeline.status_code == 200
assert timeline.json()["occupancy_provenance_delivery"] == (
"baseline-versus-additive-component-diff"
)
chunk = client.get(
f"/api/v1/laboratory/m48r3/static-occupancy/{result_id}/timeline/chunk",
params={"start": 1855, "count": 1},
)
assert chunk.status_code == 200
assert chunk.json() == {"start_sequence": 1855, "frame_count": 1}
assert (
client.get("/api/v1/laboratory/m48r3/static-occupancy/not-a-result").status_code
== 404
)
@@ -0,0 +1,90 @@
from __future__ import annotations
import json
from pathlib import Path
from k1link.laboratory.m48r3_static_occupancy_shadow import (
FRAME_EVIDENCE_SCHEMA,
compare_m48r3_frame_ledgers,
)
def _component(component_id: str, cells: list[tuple[int, int, int]]) -> dict[str, object]:
return {
"component_id": component_id,
"cells": [{"x": x, "y": y, "z": z} for x, y, z in cells],
}
def _row(
sequence: int,
occupied: list[dict[str, object]],
*,
unknown: list[dict[str, object]] | None = None,
) -> dict[str, object]:
return {
"schema_version": FRAME_EVIDENCE_SCHEMA,
"source_envelope": {"sequence": sequence},
"delivery": {
"obstacle_map": {
"occupied": occupied,
"unknown": unknown or [],
"free_space_claimed": False,
}
},
}
def _write(path: Path, rows: list[dict[str, object]]) -> None:
path.write_text(
"".join(
json.dumps(row, sort_keys=True, separators=(",", ":")) + "\n"
for row in rows
),
"utf-8",
)
def test_streaming_diff_preserves_gaps_and_marks_additive_components(tmp_path: Path) -> None:
baseline = tmp_path / "baseline.jsonl"
candidate = tmp_path / "candidate.jsonl"
_write(
baseline,
[
_row(0, [_component("base-0", [(0, 0, 0)])]),
_row(1, [_component("base-1", [(10, 0, 0)])]),
],
)
_write(
candidate,
[
_row(0, [_component("mixed-0", [(0, 0, 0), (1, 0, 0)])]),
_row(
1,
[_component("base-1", [(10, 0, 0)])],
unknown=[_component("step-1", [(20, 0, 0)])],
),
],
)
result = compare_m48r3_frame_ledgers(
baseline,
candidate,
selected_sequences={1},
expected_frames=2,
)
assert result.frame_count == 2
assert result.baseline_cell_total == 2
assert result.candidate_cell_total == 4
assert result.added_cell_total == 2
assert result.lost_cell_total == 0
assert result.mean_cell_growth_fraction == 1.0
assert result.mean_component_growth_fraction == 0.5
assert result.diff_rows[0]["component_provenance"] == {"mixed-0": "mixed"}
assert result.diff_rows[1]["component_provenance"] == {
"step-1": "additive-low-step"
}
assert result.diff_rows[0]["added_cells"] == [[1, 0, 0]]
assert set(result.selected_candidate_rows) == {1}
+23 -1
View File
@@ -93,6 +93,7 @@ class _ReadyProvider:
self.deleted: list[str] = [] self.deleted: list[str] = []
self.upload_calls = 0 self.upload_calls = 0
self.submit_calls = 0 self.submit_calls = 0
self.submitted_document: dict[str, object] | None = None
def capabilities(self) -> dict[str, object]: def capabilities(self) -> dict[str, object]:
return {"outputs": ["preview.sog", "streamed-sog"]} return {"outputs": ["preview.sog", "streamed-sog"]}
@@ -118,8 +119,9 @@ class _ReadyProvider:
members=members, members=members,
) )
def submit_build(self, _document: dict[str, object]) -> dict[str, object]: def submit_build(self, document: dict[str, object]) -> dict[str, object]:
self.submit_calls += 1 self.submit_calls += 1
self.submitted_document = document
return { return {
"schema_version": "gaussian-pipeline.job/v1", "schema_version": "gaussian-pipeline.job/v1",
"job_id": "gsp-20260826000000-deadbeef", "job_id": "gsp-20260826000000-deadbeef",
@@ -195,8 +197,28 @@ def test_service_materializes_world_manifest_and_deletes_both_copies(tmp_path: P
assert ready["status"] == "ready" assert ready["status"] == "ready"
assert ready["world_manifest"]["visual"]["preview_sog_url"].endswith("/preview.sog") assert ready["world_manifest"]["visual"]["preview_sog_url"].endswith("/preview.sog")
assert ready["world_manifest"]["collision"]["available"] is False assert ready["world_manifest"]["collision"]["available"] is False
assert ready["world_manifest"]["transforms"]["world_from_visual"] == [
1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1,
]
assert ready["world_manifest"]["transforms"]["world_from_collision"] == [
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
]
assert provider.upload_calls == 1 assert provider.upload_calls == 1
assert provider.submit_calls == 1 assert provider.submit_calls == 1
assert provider.submitted_document is not None
assert provider.submitted_document["outputs"] == {
"preview_sog": True,
"streamed_sog": True,
"collision": True,
}
assert provider.submitted_document["collision_profile"] == {
"scene_type": "interior",
"seed_position": [0, 1, 0],
"capsule_height": 1.6,
"capsule_radius": 0.2,
"voxel_size": 0.05,
"mesh_shape": "smooth",
}
service.delete(project["project_id"]) service.delete(project["project_id"])
assert provider.deleted == ["gsp-20260826000000-deadbeef"] assert provider.deleted == ["gsp-20260826000000-deadbeef"]