feat: add comparable localization algorithms

This commit is contained in:
2026-08-28 20:20:57 +09:00
parent 7fe846fbf9
commit d58f673402
20 changed files with 2104 additions and 68 deletions
+62 -1
View File
@@ -157,6 +157,51 @@ test("단일 tone 장면과 실험이 관계 검증을 통과한다", async () =
assertEqual((await validateExperiment(experiment, { arrayDefinition, scene })).length, 0, "experiment issues");
});
test("세 위치 추정 알고리즘과 backend 제약을 계약이 검증한다", async () => {
const [arrayDefinition, scene, baseline] = await Promise.all([
loadJson("/assets/array-geometries/rectangular-16x12.array.json"),
loadJson("/assets/sample-scenes/single-tone.scene.json"),
loadJson("/assets/sample-scenes/single-tone.experiment.json"),
]);
for (const algorithm of ["das", "das-phat", "gcc-phat"]) {
const experiment = structuredClone(baseline);
experiment.beamforming.algorithm = algorithm;
experiment.beamforming.backend = algorithm === "gcc-phat" ? "cpu" : "auto";
assertEqual(
(await validateExperiment(experiment, { arrayDefinition, scene })).length,
0,
`${algorithm} experiment issues`,
);
}
const incompatible = structuredClone(baseline);
incompatible.beamforming.algorithm = "gcc-phat";
incompatible.beamforming.backend = "webgpu";
let issues = await validateExperiment(incompatible, { arrayDefinition, scene });
assert(
issues.some(({ path }) => path === "$.beamforming.backend"),
"GCC-PHAT rejects the WebGPU backend",
);
incompatible.beamforming.backend = "cpu";
issues = await validateExperiment(incompatible, {
arrayDefinition: { ...arrayDefinition, microphones: arrayDefinition.microphones.slice(0, 1) },
scene,
});
assert(
issues.some(({ path }) => path === "$.beamforming.algorithm"),
"GCC-PHAT requires two microphones",
);
incompatible.beamforming.algorithm = "music";
issues = await validateExperiment(incompatible, { arrayDefinition, scene });
assert(
issues.some(({ path }) => path === "$.beamforming.algorithm"),
"unsupported algorithms are rejected",
);
});
test("shoebox 음향 환경의 room과 occluder 경계를 검증한다", async () => {
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
scene.acoustics = {
@@ -357,6 +402,7 @@ test("복수 음원 대응 result map 계약과 shape 관계를 검증한다", a
sceneId: experiment.sceneId,
createdAt: "2026-08-28T00:00:00.000Z",
backend: "cpu",
algorithm: "das",
inputHash: "a".repeat(64),
signalStats: { preNormalizationPeak: 0.5, appliedScale: 1, clippedSampleCount: 0 },
timingMs: { total: 10, signalGeneration: 3, beamforming: 7 },
@@ -369,10 +415,25 @@ test("복수 음원 대응 result map 계약과 shape 관계를 검증한다", a
}],
};
assertEqual((await validateResult(result, { experiment, scene })).length, 0, "valid result issues");
experiment.beamforming.algorithm = "das-phat";
result.algorithm = "das-phat";
result.bands[0].rawMap.scale = "score";
assertEqual(
(await validateResult(result, { experiment, scene })).length,
0,
"phase-weighted score result issues",
);
result.algorithm = "das";
let issues = await validateResult(result, { experiment, scene });
assert(issues.some(({ path }) => path === "$.algorithm"), "result algorithm must match its experiment");
experiment.beamforming.algorithm = "das";
result.bands[0].rawMap.values.pop();
result.bands[0].rawMap.scale = "normalized";
result.bands[0].peaks[0].sourceId = "missing-source";
const issues = await validateResult(result, { experiment, scene });
issues = await validateResult(result, { experiment, scene });
assert(issues.some(({ message }) => message.includes("rows × columns")));
assert(issues.some(({ path }) => path.endsWith("rawMap.scale")));
assert(issues.some(({ path }) => path.endsWith("sourceId")));