fix: expose physical aliasing and delayed arrivals

This commit is contained in:
2026-08-29 10:21:33 +09:00
parent e048ccbadc
commit de0053eae3
38 changed files with 3086 additions and 214 deletions
+103 -19
View File
@@ -171,6 +171,7 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
"Result JSON download",
);
const resultDocument = await fetch(resultDownload.url).then((response) => response.json());
assertEqual(resultDocument.schemaVersion, "1.1.0", "exported sampling-aware Result version");
assert(
!documentObject.documentElement.dataset.unsavedWork.includes("Result"),
"Result export clears Result dirty state",
@@ -399,11 +400,24 @@ test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Arra
scene.name = "Two-tone band preservation";
const secondSource = structuredClone(scene.sources[0]);
secondSource.id = "source-2";
secondSource.name = "1.2 kHz tone";
secondSource.name = "48 kHz DC-edge tone";
secondSource.position = [-0.12, 0.09, 2];
secondSource.signal.frequency = 1200;
secondSource.signal.frequency = 48000;
secondSource.signal.phaseDeg = 45;
scene.sources.push(secondSource);
const nyquistEdgeSource = structuredClone(scene.sources[0]);
nyquistEdgeSource.id = "source-3";
nyquistEdgeSource.name = "72 kHz Nyquist-edge tone";
nyquistEdgeSource.position = [0.18, -0.12, 2];
nyquistEdgeSource.signal.frequency = 72000;
nyquistEdgeSource.signal.phaseDeg = 90;
scene.sources[0].name = "47 kHz aliased tone";
scene.sources[0].signal.frequency = 47000;
scene.sources.push(secondSource, nyquistEdgeSource);
scene.ambient.push({
id: "ambient-1",
gainDb: -30,
signal: { type: "tone", frequency: 46800, phaseDeg: 20 },
});
const openArrayDefinition = await loadJson(
"/assets/array-geometries/rectangular-16x12.array.json",
@@ -486,38 +500,62 @@ test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Arra
);
downloads.length = 0;
const contextInvalidExperiment = structuredClone(experiment);
contextInvalidExperiment.id = "nyquist-invalid-after-rebind";
contextInvalidExperiment.frequencyBands[0].maxHz = openScene.sampleRate / 2;
const aliasedExperiment = structuredClone(experiment);
aliasedExperiment.id = "above-nyquist-after-rebind";
aliasedExperiment.sceneId = openScene.id;
aliasedExperiment.arrayId = openArrayDefinition.id;
aliasedExperiment.frequencyBands[0].minHz = openScene.sampleRate / 2 + 800;
aliasedExperiment.frequencyBands[0].maxHz = openScene.sampleRate / 2 + 1_000;
aliasedExperiment.frequencyBands[1].minHz = openScene.sampleRate / 2 - 1_000;
aliasedExperiment.frequencyBands[1].maxHz = openScene.sampleRate / 2 - 800;
assertEqual(
(await validateExperiment(contextInvalidExperiment)).length,
(await validateExperiment(aliasedExperiment, {
scene: openScene,
arrayDefinition: openArrayDefinition,
})).length,
0,
"Nyquist fixture is valid without a Scene context",
"above-Nyquist observation band remains valid with a Scene context",
);
const contextInvalidFile = new windowObject.File(
[JSON.stringify(contextInvalidExperiment)],
"nyquist-invalid-after-rebind.experiment.json",
const aliasedFile = new windowObject.File(
[JSON.stringify(aliasedExperiment)],
"above-nyquist-after-rebind.experiment.json",
{ type: "application/json" },
);
dispatchFiles(
windowObject,
documentObject.querySelector("#experiment-input"),
[contextInvalidFile],
[aliasedFile],
);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.documentElement.dataset.cameraValid === "true",
"relationship-invalid Experiment rejection",
documentObject.documentElement.dataset.cameraValid === "true" &&
documentObject.querySelector("#experiment-id").value === aliasedExperiment.id,
"above-Nyquist Experiment import",
);
assertEqual(
documentObject.querySelector("#experiment-id").value,
experiment.id,
"context relationship validation rejects the imported Experiment before assignment",
aliasedExperiment.id,
"above-Nyquist Experiment is assigned instead of rejected",
);
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"failed relationship validation leaves the current bands unchanged",
JSON.stringify(aliasedExperiment.frequencyBands),
"above-Nyquist requested bands remain explicit in the editor",
);
const samplingWarning = documentObject.querySelector("#sampling-warning");
assert(!samplingWarning.hidden, "above-Nyquist steering warning remains visible");
assert(
samplingWarning.textContent.includes("ADC FFT bin") &&
samplingWarning.textContent.includes("원래 analog carrier를 사용하지 않습니다") &&
samplingWarning.textContent.includes("같은 ADC bin 집합") &&
samplingWarning.textContent.includes("order 1/inverted"),
"warning states that localization does not use the original carrier",
);
dispatchFiles(windowObject, documentObject.querySelector("#experiment-input"), [experimentFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.querySelector("#experiment-id").value === experiment.id,
"restore original multi-band Experiment",
);
let replacementConfirmCount = 0;
@@ -546,7 +584,7 @@ test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Arra
dispatchFiles(windowObject, documentObject.querySelector("#scene-input"), [sceneFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.querySelectorAll("#source-summary .source-line").length === 2 &&
documentObject.querySelectorAll("#source-summary .source-line").length === 3 &&
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
documentObject.documentElement.dataset.cameraValid === "true",
"source-added Scene reimport",
@@ -560,6 +598,26 @@ test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Arra
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개"),
"Camera status keeps the preserved band count visible",
);
assert(
documentObject.querySelector("#source-summary").textContent.includes(
"48000 Hz tone → ADC 0 Hz alias (signed +0 Hz) · DC phase-degenerate edge",
),
"Camera summary keeps the aliased exact-DC phase-degenerate edge visible",
);
assert(
documentObject.querySelector("#source-summary").textContent.includes(
"72000 Hz tone → ADC 24000 Hz alias (signed +24000 Hz) · Nyquist phase-degenerate edge",
),
"Camera summary keeps the aliased exact-Nyquist phase-degenerate edge visible",
);
const sceneAliasWarning = documentObject.querySelector("#sampling-warning");
assert(!sceneAliasWarning.hidden, "aliased Scene tone keeps the sampling warning visible");
assert(
sceneAliasWarning.textContent.includes("선택 ADC bin으로 접힌 Scene tone") &&
sceneAliasWarning.textContent.includes("tone-1k ← source source-1 47000→ADC 1000 Hz") &&
sceneAliasWarning.textContent.includes("tone-1.2k ← ambient ambient-1 46800→ADC 1200 Hz"),
"baseband bands disclose contributing source and ambient aliases",
);
const arrayFile = new windowObject.File(
[JSON.stringify(arrayDefinition)],
@@ -583,6 +641,32 @@ test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Arra
"ready status keeps the connected Experiment band count visible",
);
dispatchValue(windowObject, documentObject.querySelector("#scan-columns"), 8);
dispatchValue(windowObject, documentObject.querySelector("#scan-rows"), 8);
dispatchValue(windowObject, documentObject.querySelector("#backend"), "cpu");
await waitFor(
() => documentObject.documentElement.dataset.cameraValid === "true" &&
!documentObject.querySelector("#run-button").disabled,
"aliased Scene tone snapshot setup",
);
documentObject.querySelector("#run-button").click();
await waitFor(
() => documentObject.documentElement.dataset.runState === "done",
"aliased Scene tone snapshot",
30000,
);
const metricText = documentObject.querySelector("#metric-grid").textContent;
assert(
metricText.includes("tone-1k · ADC steering") &&
metricText.includes("source source-1 47000→ADC 1000 Hz"),
"baseband source-alias steering metric is rendered",
);
assert(
metricText.includes("tone-1.2k · ADC steering") &&
metricText.includes("ambient ambient-1 46800→ADC 1200 Hz"),
"baseband ambient-alias steering metric is rendered",
);
documentObject.querySelector("#export-experiment-button").click();
const experimentDownload = await waitFor(
() => downloads.find((download) => download.fileName.endsWith(".experiment.json")),