feat(lab): visualize M4.8R3 system occupancy
This commit is contained in:
@@ -26,6 +26,7 @@ export interface LaboratoryMetricObstacleVisual {
|
||||
state: "current" | "retained" | "held" | "expired";
|
||||
centroidBodyXyzM: LaboratoryMetricPoint3;
|
||||
cellCentersBodyXyzM: readonly LaboratoryMetricPoint3[];
|
||||
occupancySource?: "baseline" | "mixed" | "additive-low-step";
|
||||
}
|
||||
|
||||
export interface LaboratoryMetricRigVisual {
|
||||
@@ -41,7 +42,7 @@ export interface LaboratoryMetricCorridorVisual {
|
||||
}
|
||||
|
||||
export interface LaboratoryMetricLegendEntry {
|
||||
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling";
|
||||
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling" | "low-step";
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ export function laboratoryMetricLegendEntries({
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
showLowStep = true,
|
||||
}: {
|
||||
pointCloudCount: number;
|
||||
localSurfaceCount: number;
|
||||
@@ -59,19 +61,29 @@ export function laboratoryMetricLegendEntries({
|
||||
showCurrentIncrement: boolean;
|
||||
showLocalSurface: boolean;
|
||||
showRollingMap: boolean;
|
||||
showLowStep?: boolean;
|
||||
}): readonly LaboratoryMetricLegendEntry[] {
|
||||
const visibleObstacles = obstacles.filter((obstacle) => (
|
||||
obstacle.state === "current"
|
||||
(showLowStep || obstacle.occupancySource === undefined || obstacle.occupancySource === "baseline")
|
||||
&& (obstacle.state === "current"
|
||||
? showCurrentIncrement
|
||||
: obstacle.state === "retained"
|
||||
? showRollingMap
|
||||
: false
|
||||
: false)
|
||||
));
|
||||
const decisions = new Set(visibleObstacles.map(({ decision }) => decision));
|
||||
const entries: LaboratoryMetricLegendEntry[] = [];
|
||||
if (decisions.has("threat")) entries.push({ id: "threat", label: "Угроза" });
|
||||
if (decisions.has("not-threat")) entries.push({ id: "not-threat", 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) {
|
||||
entries.push({ id: "context", label: "Текущий кадр" });
|
||||
}
|
||||
@@ -153,6 +165,16 @@ function decisionColor(
|
||||
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 {
|
||||
resetView: () => void;
|
||||
}
|
||||
@@ -171,6 +193,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement: boolean;
|
||||
showLocalSurface: boolean;
|
||||
showRollingMap: boolean;
|
||||
showLowStep?: boolean;
|
||||
pointSemanticClassIds?: readonly (number | null)[];
|
||||
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
|
||||
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
|
||||
@@ -187,6 +210,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
showLowStep = true,
|
||||
pointSemanticClassIds,
|
||||
semanticClasses,
|
||||
semanticPalette,
|
||||
@@ -352,6 +376,12 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
|
||||
for (const obstacle of obstacles) {
|
||||
if (
|
||||
(
|
||||
!showLowStep
|
||||
&& obstacle.occupancySource !== undefined
|
||||
&& obstacle.occupancySource !== "baseline"
|
||||
)
|
||||
||
|
||||
(obstacle.state === "current" && !showCurrentIncrement)
|
||||
|| (obstacle.state === "retained" && !showRollingMap)
|
||||
|| obstacle.state === "held"
|
||||
@@ -359,7 +389,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const color = decisionColor(host, obstacle.decision);
|
||||
const color = obstacleColor(host, obstacle);
|
||||
if (obstacle.state === "retained") {
|
||||
const geometry = new THREE.BoxGeometry(
|
||||
occupiedVoxelSizeM * 0.82,
|
||||
@@ -424,6 +454,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
showLowStep,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -538,6 +569,7 @@ LaboratoryMetricEvidenceSceneHandle,
|
||||
showCurrentIncrement,
|
||||
showLocalSurface,
|
||||
showRollingMap,
|
||||
showLowStep,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "./m48ObjectCentricQuality";
|
||||
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
|
||||
import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualification";
|
||||
import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
|
||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||
|
||||
@@ -50,6 +51,7 @@ export type AdvancedLaboratoryWorkId =
|
||||
| "m48-object-centric-quality"
|
||||
| "m48-small-static-passage-regression"
|
||||
| "m48-static-occupancy-qualification"
|
||||
| "m48r3-static-occupancy-shadow"
|
||||
| "m48s-fixed-class-detector"
|
||||
| "m48t-risk-quality-temporal"
|
||||
| "m47-reference-graph-shadow"
|
||||
@@ -97,6 +99,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-static-occupancy-qualification",
|
||||
"m48r3-static-occupancy-shadow",
|
||||
"m48s-fixed-class-detector",
|
||||
"m48t-risk-quality-temporal",
|
||||
"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-small-static-passage-regression": "m48-small-static-passage-regression",
|
||||
"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",
|
||||
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||
@@ -189,6 +193,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
m48: null,
|
||||
m48SmallStatic: null,
|
||||
m48StaticOccupancy: null,
|
||||
m48r3StaticOccupancy: null,
|
||||
m48s: null,
|
||||
m48t: null,
|
||||
m4Threat: null,
|
||||
@@ -318,6 +323,7 @@ export function advancedLaboratoryResultAvailable(
|
||||
return workId === "m48-object-centric-quality" ? results.m48 !== null
|
||||
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== 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 === "m48t-risk-quality-temporal" ? results.m48t !== null
|
||||
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
||||
@@ -378,6 +384,9 @@ export async function fetchAdvancedLaboratoryResult(
|
||||
} else if (workId === "m48-static-occupancy-qualification") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R2 qualification identity не выбрана.");
|
||||
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") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
|
||||
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
|
||||
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
|
||||
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
|
||||
import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancyQualification";
|
||||
import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancyShadow";
|
||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
||||
|
||||
@@ -45,6 +46,7 @@ export interface AdvancedLaboratoryResults {
|
||||
m48: M48AdvancedResult | null;
|
||||
m48SmallStatic: M48SmallStaticRegressionResult | null;
|
||||
m48StaticOccupancy: M48StaticOccupancyQualificationResult | null;
|
||||
m48r3StaticOccupancy: M48R3StaticOccupancyShadowResult | null;
|
||||
m48s: M48SFixedClassDetectorResult | null;
|
||||
m48t: M48TRiskQualityResult | null;
|
||||
m4Threat: M4ThreatReplayResult | null;
|
||||
|
||||
@@ -968,6 +968,7 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return {
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||
m48r3StaticOccupancy: null,
|
||||
m48s: null, m48t: null, m4Threat: null,
|
||||
l3: null, l31: null, l32: null, l33: null,
|
||||
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 M4ThreatMotion = "moving" | "stationary" | "unknown";
|
||||
export type M4OccupancySource = "baseline" | "mixed" | "additive-low-step";
|
||||
export type M4Point3 = readonly [number, number, number];
|
||||
export type M4Matrix3 = readonly [M4Point3, M4Point3, M4Point3];
|
||||
|
||||
@@ -76,6 +77,7 @@ export interface M4ThreatMetricVisual {
|
||||
centroidBodyXyzM: M4Point3;
|
||||
cellCentersBodyXyzM: readonly M4Point3[];
|
||||
assessment: M4ThreatAssessment;
|
||||
occupancySource: M4OccupancySource;
|
||||
}
|
||||
|
||||
export interface M4ThreatCameraProposal {
|
||||
@@ -174,6 +176,7 @@ export interface M4ThreatTimeline {
|
||||
cameraPointWindowSeconds: number;
|
||||
cameraPointSampleLimit: number;
|
||||
worldStateDelivery: "source-paced-latest-wins" | null;
|
||||
occupancyProvenanceDelivery: "baseline-versus-additive-component-diff" | null;
|
||||
worldStateFrameCount: number;
|
||||
supersededFrameCount: number;
|
||||
sourceRepresentationId: "registered-map-increment-v1";
|
||||
@@ -331,6 +334,9 @@ function parseMetricVisual(value: unknown): M4ThreatMetricVisual {
|
||||
) {
|
||||
throw new M4ThreatContractError("M4.6 temporal state: неизвестное состояние.");
|
||||
}
|
||||
const occupancySource = item.occupancy_source === undefined
|
||||
? "baseline"
|
||||
: memberOccupancySource(item.occupancy_source);
|
||||
return {
|
||||
componentId: text(item.component_id, "M4.6 visual component"),
|
||||
state,
|
||||
@@ -340,9 +346,17 @@ function parseMetricVisual(value: unknown): M4ThreatMetricVisual {
|
||||
(point) => vector(point, 3, "M4.6 cell") as [number, number, number],
|
||||
),
|
||||
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({
|
||||
resultId: requestedResultId,
|
||||
fetcher = fetch,
|
||||
@@ -683,6 +697,13 @@ export async function fetchM4ThreatTimeline(
|
||||
"source-paced-latest-wins",
|
||||
"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
|
||||
? frameCount
|
||||
: integer(payload.world_state_frame_count, "M4.6 world-state frames"),
|
||||
|
||||
@@ -334,6 +334,12 @@
|
||||
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 {
|
||||
background: rgb(var(--nodedc-accent-rgb));
|
||||
opacity: 0.72;
|
||||
|
||||
@@ -45,6 +45,7 @@ import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
|
||||
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
||||
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
||||
import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQualificationResult";
|
||||
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||
|
||||
@@ -98,6 +99,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m48-static-occupancy-qualification" && 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) {
|
||||
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,
|
||||
semantic,
|
||||
reviewAnchors = EMPTY_REVIEW_ANCHORS,
|
||||
showReviewAnchorBoxes = true,
|
||||
reviewLabel = "Контрольные примеры M4.8R1",
|
||||
timelineEndpointRoot,
|
||||
evidenceLabel = "M4.6",
|
||||
}: {
|
||||
resultId: string;
|
||||
semantic?: M4ReplayThreatSemanticLayer;
|
||||
reviewAnchors?: readonly M4ReplayThreatReviewAnchor[];
|
||||
showReviewAnchorBoxes?: boolean;
|
||||
reviewLabel?: string;
|
||||
timelineEndpointRoot?: string;
|
||||
evidenceLabel?: string;
|
||||
}) {
|
||||
@@ -124,6 +128,7 @@ export function M4ReplayThreatVisual({
|
||||
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
|
||||
const [showLocalSurface, setShowLocalSurface] = useState(true);
|
||||
const [showRollingMap, setShowRollingMap] = useState(true);
|
||||
const [showLowStep, setShowLowStep] = useState(true);
|
||||
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
|
||||
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
|
||||
const [showMediaPoints, setShowMediaPoints] = useState(false);
|
||||
@@ -262,7 +267,7 @@ export function M4ReplayThreatVisual({
|
||||
}, [metadata.timeline, resultId, reviewAnchorIdentity, reviewAnchors, seekPlayback, setPlaybackPlaying]);
|
||||
const reviewAnchorBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||
const timeline = metadata.timeline;
|
||||
if (!frame || !timeline) return [];
|
||||
if (!frame || !timeline || !showReviewAnchorBoxes) return [];
|
||||
return reviewAnchors
|
||||
.filter((anchor) => anchor.sourceSequence === frame.sequence)
|
||||
.map((anchor) => {
|
||||
@@ -281,7 +286,7 @@ export function M4ReplayThreatVisual({
|
||||
dashed: true,
|
||||
};
|
||||
});
|
||||
}, [frame, metadata.timeline, reviewAnchors]);
|
||||
}, [frame, metadata.timeline, reviewAnchors, showReviewAnchorBoxes]);
|
||||
const activeBoxes = useMemo(
|
||||
() => [...boxes(frame?.cameraProposals ?? []), ...reviewAnchorBoxes],
|
||||
[frame, reviewAnchorBoxes],
|
||||
@@ -345,6 +350,7 @@ export function M4ReplayThreatVisual({
|
||||
state: obstacle.state,
|
||||
centroidBodyXyzM: obstacle.centroidBodyXyzM,
|
||||
cellCentersBodyXyzM: obstacle.cellCentersBodyXyzM,
|
||||
occupancySource: obstacle.occupancySource,
|
||||
})) ?? [], [spatialFrame]);
|
||||
const currentIncrementObstacles = spatialFrame?.metricObstacles.filter(
|
||||
(item) => item.state === "current",
|
||||
@@ -352,6 +358,9 @@ export function M4ReplayThreatVisual({
|
||||
const rollingMapObstacles = spatialFrame?.metricObstacles.filter(
|
||||
(item) => item.state === "retained",
|
||||
) ?? [];
|
||||
const lowStepObstacles = spatialFrame?.metricObstacles.filter(
|
||||
(item) => item.occupancySource !== "baseline",
|
||||
) ?? [];
|
||||
const nearest = spatialFrame?.metricObstacles
|
||||
.map((item) => item.assessment.closestApproachM)
|
||||
.filter((value): value is number => value !== null)
|
||||
@@ -513,6 +522,18 @@ export function M4ReplayThreatVisual({
|
||||
>
|
||||
ROLLING
|
||||
</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 ? (
|
||||
<Button
|
||||
size="compact"
|
||||
@@ -567,7 +588,7 @@ export function M4ReplayThreatVisual({
|
||||
<Icon name="chevron-right" size={16} />
|
||||
</IconButton>
|
||||
<Select
|
||||
label="Контрольные примеры M4.8R1"
|
||||
label={reviewLabel}
|
||||
value={String(selectedReviewAnchorIndex)}
|
||||
options={reviewAnchors.map((anchor, index) => ({
|
||||
value: String(index),
|
||||
@@ -618,6 +639,9 @@ export function M4ReplayThreatVisual({
|
||||
<span>Spatial evidence</span>
|
||||
<strong>
|
||||
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
|
||||
{metadata.timeline.occupancyProvenanceDelivery
|
||||
? ` · ${lowStepObstacles.length} low-step`
|
||||
: ""}
|
||||
</strong>
|
||||
<small>
|
||||
{spatialFrame
|
||||
@@ -758,6 +782,7 @@ export function M4ReplayThreatVisual({
|
||||
showCurrentIncrement={showCurrentIncrement}
|
||||
showLocalSurface={showLocalSurface}
|
||||
showRollingMap={showRollingMap}
|
||||
showLowStep={showLowStep}
|
||||
pointSemanticClassIds={alignedSemanticPointIds}
|
||||
semanticClasses={semanticClasses}
|
||||
semanticPalette={semanticPalette}
|
||||
|
||||
@@ -84,6 +84,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
||||
experimentName: "M4.8 · conservative static occupancy qualification",
|
||||
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": {
|
||||
profileId: "rig-ravnoves-perception-gate-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
||||
|
||||
@@ -22,6 +22,7 @@ function mergeResults(
|
||||
m48: next.m48 ?? current.m48,
|
||||
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
||||
m48StaticOccupancy: next.m48StaticOccupancy ?? current.m48StaticOccupancy,
|
||||
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m48t: next.m48t ?? current.m48t,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
@@ -122,6 +123,7 @@ export function useAdvancedLaboratoryCatalog({
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-static-occupancy-qualification",
|
||||
"m48r3-static-occupancy-shadow",
|
||||
].includes(selectedWorkId)
|
||||
&& !indexedResultId
|
||||
) return;
|
||||
|
||||
Reference in New Issue
Block a user