feat: compare localization algorithms in camera
This commit is contained in:
@@ -76,6 +76,9 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
assert(!documentObject.querySelector("#advanced-settings").open, "advanced settings start collapsed");
|
||||
assert(!documentObject.querySelector("#array-details").open, "array developer detail starts collapsed");
|
||||
assert(!documentObject.querySelector("#camera-help").open, "limitations help starts collapsed");
|
||||
assertEqual(documentObject.querySelector("#algorithm").value, "das", "default DAS algorithm");
|
||||
assertEqual(documentObject.querySelectorAll("#algorithm option").length, 3, "three runnable algorithms");
|
||||
assert(!documentObject.querySelector("#postprocess"), "disabled postprocess advertisement is absent");
|
||||
assertEqual(documentObject.querySelector("#acoustic-profile").value, "free-field", "default free-field profile");
|
||||
assertEqual(
|
||||
documentObject.querySelector("#frequency-response-mode").value,
|
||||
@@ -90,6 +93,13 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
documentObject.querySelector("#acoustic-environment-summary").textContent.includes("≤ 24 dB"),
|
||||
"safe inverse-response boost limit is visible",
|
||||
);
|
||||
assert(documentObject.querySelector("#workload-summary").textContent.includes("DAS"), "algorithm workload label");
|
||||
assert(documentObject.querySelector("#workload-summary").textContent.includes("STFT"), "STFT transform cost");
|
||||
assert(
|
||||
documentObject.querySelector("#workload-summary").textContent.includes("distance cache") &&
|
||||
!documentObject.querySelector("#workload-summary").textContent.includes("steering"),
|
||||
"distance cache is labeled accurately",
|
||||
);
|
||||
|
||||
const canvas = documentObject.querySelector("#scene-canvas");
|
||||
await waitFor(() => canvas.width > 0 && canvas.height > 0, "scene canvas 초기 렌더");
|
||||
@@ -125,6 +135,7 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
30000,
|
||||
);
|
||||
assertEqual(documentObject.documentElement.dataset.runBackend, "cpu", "actual backend");
|
||||
assertEqual(documentObject.documentElement.dataset.runAlgorithm, "das", "actual baseline algorithm");
|
||||
assertEqual(documentObject.documentElement.dataset.resultBandCount, "1", "result band count");
|
||||
assert(documentObject.querySelectorAll("#metric-grid .metric-card").length >= 10, "result metrics rendered");
|
||||
assert(documentObject.querySelector("#metric-grid").textContent.includes("음향 모델"), "acoustic model metric rendered");
|
||||
@@ -169,6 +180,8 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
.map((issue) => `${issue.path}: ${issue.message}`)
|
||||
.join(" · ")}`);
|
||||
assertEqual(resultDocument.backend, "cpu", "exported backend");
|
||||
assertEqual(resultDocument.algorithm, "das", "exported baseline algorithm");
|
||||
assertEqual(resultDocument.bands[0].rawMap.scale, "power", "DAS exports a power map");
|
||||
assertEqual(resultDocument.bands[0].rawMap.rows, 8, "exported map rows");
|
||||
assertEqual(resultDocument.bands[0].rawMap.columns, 8, "exported map columns");
|
||||
assertEqual(resultDocument.bands[0].rawMap.values.length, 64, "exported map value count");
|
||||
@@ -197,6 +210,27 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
"original comparison configuration restore",
|
||||
);
|
||||
|
||||
dispatchValue(windowObject, documentObject.querySelector("#backend"), "webgpu");
|
||||
dispatchValue(windowObject, documentObject.querySelector("#algorithm"), "gcc-phat");
|
||||
await waitFor(
|
||||
() => documentObject.querySelector("#algorithm").value === "gcc-phat" &&
|
||||
documentObject.querySelector("#backend").value === "auto" &&
|
||||
documentObject.querySelector('#backend option[value="webgpu"]').disabled &&
|
||||
documentObject.querySelector("#validation-message").textContent.includes("8개 이상"),
|
||||
"GCC-PHAT CPU capability and narrow-band warning",
|
||||
);
|
||||
assert(documentObject.querySelector("#workload-summary").textContent.includes("ref pairs"), "GCC pair cost");
|
||||
|
||||
dispatchValue(windowObject, documentObject.querySelector("#algorithm"), "das-phat");
|
||||
dispatchValue(windowObject, documentObject.querySelector("#backend"), "cpu");
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.cameraValid === "true" &&
|
||||
documentObject.querySelector("#algorithm").value === "das-phat" &&
|
||||
!documentObject.querySelector('#backend option[value="webgpu"]').disabled &&
|
||||
!documentObject.querySelector("#run-button").disabled,
|
||||
"DAS-PHAT comparison configuration",
|
||||
);
|
||||
|
||||
const comparisonPresets = [
|
||||
["rectangular-2x2-450mm", "4 ch", "0.450 × 0.450 m"],
|
||||
["rectangular-4x3-450mm", "12 ch", "0.450 × 0.450 m"],
|
||||
@@ -245,10 +279,15 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
30000,
|
||||
);
|
||||
assert(!documentObject.querySelector("#result-badge").textContent.includes("이전 설정"), "fresh result badge");
|
||||
assertEqual(documentObject.documentElement.dataset.runAlgorithm, "das-phat", "actual comparison algorithm");
|
||||
assert(documentObject.querySelector("#result-badge").textContent.includes("DAS-PHAT"), "algorithm result badge");
|
||||
assert(documentObject.querySelector("#canvas-badge").textContent.includes("DAS-PHAT"), "algorithm canvas badge");
|
||||
assert(documentObject.querySelector("#metric-grid").textContent.includes("Truth concentration"), "generic concentration metric");
|
||||
assertEqual(documentObject.querySelectorAll("#history-list .history-card").length, 2, "rerun history");
|
||||
const historyText = documentObject.querySelector("#history-list").textContent;
|
||||
assert(historyText.includes("rectangular baseline"), "baseline history retained");
|
||||
assert(historyText.includes("Fermat spiral"), "comparison array history added");
|
||||
assert(historyText.includes("DAS-PHAT") && historyText.includes("DAS"), "both algorithms are compared");
|
||||
|
||||
documentObject.querySelector("#export-history-button").click();
|
||||
const historyDownload = await waitFor(
|
||||
@@ -258,6 +297,12 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
|
||||
const historyDocument = await fetch(historyDownload.url).then((response) => response.json());
|
||||
assertEqual(historyDocument.kind, "micarrsim.comparison-history", "comparison history kind");
|
||||
assertEqual(historyDocument.items.length, 2, "both comparison summaries exported");
|
||||
assertEqual(
|
||||
historyDocument.items.map((item) => item.algorithm).sort().join(","),
|
||||
"das,das-phat",
|
||||
"both comparison algorithms exported",
|
||||
);
|
||||
assert(historyDocument.items.every((item) => Number.isFinite(item.localizationMs)), "localization timing exported");
|
||||
assert(
|
||||
!documentObject.documentElement.dataset.unsavedWork.includes("비교 기록"),
|
||||
"history export clears comparison dirty state",
|
||||
@@ -324,17 +369,17 @@ test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source exam
|
||||
|
||||
dispatchValue(windowObject, documentObject.querySelector("#scan-columns"), 8);
|
||||
dispatchValue(windowObject, documentObject.querySelector("#scan-rows"), 8);
|
||||
dispatchValue(windowObject, documentObject.querySelector("#backend"), "cpu");
|
||||
dispatchValue(windowObject, documentObject.querySelector("#backend"), "auto");
|
||||
dispatchValue(windowObject, documentObject.querySelector("#live-map-rate"), 2);
|
||||
await waitFor(
|
||||
() => documentObject.documentElement.dataset.cameraValid === "true" &&
|
||||
documentObject.documentElement.dataset.liveState === "idle" &&
|
||||
documentObject.querySelector("#scan-columns").value === "8" &&
|
||||
documentObject.querySelector("#scan-rows").value === "8" &&
|
||||
documentObject.querySelector("#backend").value === "cpu" &&
|
||||
documentObject.querySelector("#backend").value === "auto" &&
|
||||
documentObject.querySelector("#live-map-rate").value === "2" &&
|
||||
!documentObject.querySelector("#prepare-live-button").disabled,
|
||||
"8x8 CPU 2 Hz live configuration",
|
||||
"8x8 Auto 2 Hz live configuration",
|
||||
);
|
||||
|
||||
documentObject.querySelector("#prepare-live-button").click();
|
||||
@@ -345,6 +390,11 @@ test("Acoustic Camera prepares, scrubs, plays, and pauses the moving-source exam
|
||||
);
|
||||
const frameCount = Number(documentObject.documentElement.dataset.liveFrameCount);
|
||||
assert(frameCount > 1, `multiple live frames: ${frameCount}`);
|
||||
assertEqual(
|
||||
Number(documentObject.documentElement.dataset.livePlanAnalyses),
|
||||
frameCount - 1,
|
||||
"Auto fallback retries GPU once then reuses one CPU localization plan",
|
||||
);
|
||||
assert(documentObject.querySelector("#live-status").textContent.includes("CPU"), "live backend is CPU");
|
||||
|
||||
dispatchValue(windowObject, documentObject.querySelector("#live-time"), 0.6, "input");
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user