feat: harden end-to-end simulation workflow

This commit is contained in:
Codex
2026-08-28 12:34:11 +09:00
parent 2ce96064cf
commit 3317a03d1a
17 changed files with 577 additions and 24 deletions
+66 -3
View File
@@ -1,4 +1,5 @@
import {
alignExperimentToScene,
canonicalStringify,
createResultDocument,
estimateExperimentWorkload,
@@ -108,6 +109,46 @@ test("Camera distance shift keeps the scene and scan on one z plane", () => {
assertEqual(scene.image.plane.z, 3.25, "background image plane z shift");
});
test("Camera aligns an experiment to an imported scene without changing grid resolution", () => {
const experiment = {
scan: { z: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, columns: 32, rows: 24 },
};
const imageScene = {
sources: [{ id: "one", position: [0.2, -0.1, 4.5] }],
image: { plane: { z: 4.5, xMin: -2, xMax: 3, yMin: -1.5, yMax: 2.5 } },
};
assertEqual(alignExperimentToScene(experiment, imageScene), "image-plane", "alignment source");
assertEqual(
JSON.stringify(experiment.scan),
JSON.stringify({
z: 4.5,
xMin: -2,
xMax: 3,
yMin: -1.5,
yMax: 2.5,
columns: 32,
rows: 24,
}),
"image plane becomes scan plane while rows and columns stay unchanged",
);
const sourceScene = {
sources: [
{ id: "one", position: [-0.5, 0.1, 3] },
{ id: "two", position: [0.5, 0.2, 5] },
],
};
assertEqual(alignExperimentToScene(experiment, sourceScene), "source-bounds", "source fallback");
assertEqual(experiment.scan.z, 4, "median source depth");
assertNear(experiment.scan.xMin, -0.75, 1e-12, "source x minimum with padding");
assertNear(experiment.scan.xMax, 0.75, 1e-12, "source x maximum with padding");
assertNear(experiment.scan.yMin, -0.15, 1e-12, "source y minimum with padding");
assertNear(experiment.scan.yMax, 0.45, 1e-12, "source y maximum with padding");
assertEqual(experiment.scan.columns, 32, "columns retained after source fallback");
assertEqual(experiment.scan.rows, 24, "rows retained after source fallback");
});
test("Camera result document is schema-safe and linked to its snapshot", async () => {
const arrayDefinition = {
id: "test-array",
@@ -125,7 +166,7 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
id: "test-experiment",
arrayId: arrayDefinition.id,
sceneId: scene.id,
scan: { rows: 2, columns: 2 },
scan: { rows: 2, columns: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, z: 2 },
beamforming: { backend: "auto" },
frequencyBands: [{ id: "band-1", color: "#65f2bf" }],
};
@@ -143,7 +184,14 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
}],
};
const evaluations = new Map([["band-1", {
peaks: [{ index: 3, sourceId: "source-1", position: [1, -1, 2], value: 1, localizationErrorM: 0.05 }],
peaks: [{
index: 3,
sourceId: "source-1",
position: [1, -1, 2],
value: 1,
localizationErrorM: 0.05,
localizationErrorPixels: 0.1,
}],
metrics: {
peakToSidelobeDb: 10,
energyInsideTruthRatio: 0.75,
@@ -152,6 +200,18 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
mainLobeWidthM: 0.2,
ignoredNonFinite: Number.NaN,
},
evaluation: {
version: "1.0.0",
truthRadiusM: 0.1,
matchToleranceM: 0.2,
detectionThresholdRatio: 0.5,
detectionThresholdValue: 0.25,
minimumPeakDistanceM: 0.1,
sidelobeExclusionRadiusM: 0.1,
halfPowerRatio: 0.5,
dynamicRangeDb: 30,
maximumDetections: 128,
},
}]]);
const result = createResultDocument({
@@ -168,9 +228,12 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
assert(!("index" in result.bands[0].peaks[0]), "internal peak index is omitted");
assertEqual(
Object.keys(result.bands[0].metrics).join(","),
"peakToSidelobeDb,energyInsideTruthRatio,precision,recall",
"peakToSidelobeDb,energyInsideTruthRatio,precision,recall,mainLobeWidthM",
"only finite schema metrics are exported",
);
assertEqual(result.bands[0].peaks[0].localizationErrorPixels, 0.1, "pixel error is exported");
assertEqual(result.bands[0].evaluation.matchToleranceM, 0.2, "evaluation settings are exported");
assertEqual(result.environment.scanXMin, -1, "scan bounds are recorded");
assertEqual(result.environment.fallbackCode, "webgpu-unavailable", "fallback is recorded");
const issues = await validateResult(result, { experiment, scene });
+5 -2
View File
@@ -153,15 +153,18 @@ test("result exporter가 schema 허용 metric만 선택하고 내부 필드를
assertEqual(Object.keys(exported.metrics).join(","), "precision,recall", "selected metric keys");
assertEqual(exported.peaks[0].sourceId, "truth", "schema peak source match");
assertEqual(exported.peaks[0].localizationErrorM, 0, "schema peak localization error");
assertEqual(exported.peaks[0].localizationErrorPixels, 0, "schema peak pixel error");
assert(!("index" in exported.peaks[0]), "internal peak index omitted");
assert(!("mainLobeWidthM" in exported.metrics), "diagnostic omitted from schema metrics");
assert(!("mainLobeWidthM" in exported.metrics), "explicit metric selection is respected");
assertEqual(exported.evaluation.version, "1.0.0", "evaluation version exported");
assertEqual(exported.evaluation.matchToleranceM, 0, "match tolerance exported");
assertEqual(
Object.keys(selectResultMetrics(evaluation)).join(","),
RESULT_METRIC_KEYS.join(","),
"default schema metric selection",
);
const unsupported = captureError(() => selectResultMetrics(evaluation, ["mainLobeWidthM"]));
const unsupported = captureError(() => selectResultMetrics(evaluation, ["gridResolutionM"]));
assert(unsupported instanceof RangeError, "unsupported result metric is rejected");
});