feat: compare localization algorithms in camera

This commit is contained in:
2026-08-28 20:57:38 +09:00
parent e8dac792d9
commit fe0f70ab11
6 changed files with 362 additions and 70 deletions
+62 -2
View File
@@ -14,6 +14,8 @@ import {
import {
alignExperimentToScene,
canonicalStringify,
createComparisonSignature,
createHistorySummary,
createResultDocument,
estimateExperimentWorkload,
hashSimulationInputs,
@@ -247,9 +249,49 @@ test("Camera workload counts unique overlapping FFT bins", () => {
assertEqual(workload.frameCount, 62, "STFT frame count");
assertEqual(JSON.stringify(workload.bandBins), JSON.stringify([[4, 5, 6], [6, 7, 8]]), "bins per band");
assertEqual(workload.selectedBinCount, 5, "overlapping bin is counted once");
assertEqual(workload.stftTransformCount, 186, "STFT transform count");
assertEqual(workload.scanOperationCount, 74_400, "DAS scan operation count");
assertEqual(workload.scanOperationKind, "complex MAC", "DAS operation kind");
assertEqual(workload.complexMacCount, 74_400, "estimated complex MAC count");
assertNear(workload.scalingVs50, 80 / 2500, 1e-15, "50x50 scaling");
assertNear(workload.distanceMiB, (80 * 3 * 4) / 1024 ** 2, 1e-15, "distance table size");
const gccExperiment = structuredClone(experiment);
gccExperiment.beamforming = { algorithm: "gcc-phat" };
const gccWorkload = estimateExperimentWorkload(arrayDefinition, scene, gccExperiment);
assertEqual(gccWorkload.algorithmLabel, "GCC-PHAT", "GCC algorithm label");
assertEqual(gccWorkload.referencePairCount, 2, "GCC reference pairs");
assertEqual(gccWorkload.bandBinSelectionCount, 6, "GCC counts overlapping band bins per band");
assertEqual(gccWorkload.gccCrossSpectrumComplexOps, 1_488, "GCC two-pass cross-spectrum ops");
assertEqual(gccWorkload.inverseFftTransformCount, 4, "GCC inverse FFT count");
assertEqual(gccWorkload.minimumBandBinCount, 3, "GCC narrowest band bin count");
assertEqual(gccWorkload.scanOperationCount, 320, "GCC pair-band scan lookups");
assertEqual(gccWorkload.scanOperationKind, "reference-pair lookup", "GCC operation kind");
assertEqual(gccWorkload.complexMacCount, 0, "GCC is not mislabeled as DAS complex MAC");
});
test("Camera comparison signature groups array, algorithm, and backend variants only", () => {
const scene = { id: "scene", seed: 7, sources: [] };
const baseline = {
id: "experiment",
arrayId: "array-a",
scan: { rows: 8, columns: 8 },
beamforming: { algorithm: "das", backend: "cpu", normalization: "peak" },
};
const variant = structuredClone(baseline);
variant.arrayId = "array-b";
variant.beamforming.algorithm = "das-phat";
variant.beamforming.backend = "webgpu";
assertEqual(
createComparisonSignature(scene, variant),
createComparisonSignature(scene, baseline),
"comparison axes share one history group",
);
variant.scan.columns = 9;
assert(
createComparisonSignature(scene, variant) !== createComparisonSignature(scene, baseline),
"scan changes remain separate",
);
});
test("Camera acoustic presets bound current geometry and expose predictable sparse cost", () => {
@@ -415,11 +457,13 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
arrayId: arrayDefinition.id,
sceneId: scene.id,
scan: { rows: 2, columns: 2, xMin: -1, xMax: 1, yMin: -1, yMax: 1, z: 2 },
beamforming: { backend: "auto" },
beamforming: { algorithm: "das-phat", backend: "auto" },
frequencyBands: [{ id: "band-1", color: "#65f2bf" }],
};
const run = {
algorithm: "das-phat",
backend: "cpu",
referenceMic: "mic-1",
signalStats: {
preNormalizationPeak: 0.75,
appliedScale: 1,
@@ -433,6 +477,7 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
bands: [{
id: "band-1",
color: "#65f2bf",
rawScale: "score",
rawMap: Float32Array.of(0.1, 0.2, 0.3, 1),
displayMap: Float32Array.of(0.1, 0.2, 0.3, 1),
displayScale: "normalized",
@@ -448,7 +493,7 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
localizationErrorPixels: 0.1,
beamSpread: {
calibrated: false,
method: "connected-half-power-power-weighted-covariance",
method: "connected-half-maximum-score-weighted-covariance",
halfPowerRatio: 0.5,
thresholdValue: 0.5,
pixelCount: 3,
@@ -496,6 +541,10 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
});
assert(Array.isArray(result.bands[0].rawMap.values), "typed raw map becomes JSON array");
assertEqual(result.algorithm, "das-phat", "actual algorithm is exported");
assertEqual(result.timingMs.localization, 10, "generic localization timing is exported");
assertEqual(result.bands[0].rawMap.scale, "score", "algorithm raw score scale is exported");
assertEqual(result.environment.referenceMic, "mic-1", "reference microphone metadata is exported");
assert(!("index" in result.bands[0].peaks[0]), "internal peak index is omitted");
assertEqual(
Object.keys(result.bands[0].metrics).join(","),
@@ -512,6 +561,17 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
assertEqual(result.signalStats.acousticPathCount, 38, "acoustic path count is exported");
assertEqual(result.signalStats.acousticPathCounts.diffuse, 12, "path kinds are exported");
const history = createHistorySummary({
arrayDefinition,
scene,
experiment,
run,
evaluations,
inputHash: "a".repeat(64),
});
assertEqual(history.algorithm, "das-phat", "history records the actual algorithm");
assertEqual(history.localizationMs, 10, "history records localization time");
const issues = await validateResult(result, { experiment, scene });
assertEqual(issues.length, 0, `generated result validation: ${JSON.stringify(issues)}`);