feat(lab): visualize M4.8R3 system occupancy
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user