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
+53 -3
View File
@@ -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");