fix: stabilize localization comparison semantics
This commit is contained in:
@@ -410,7 +410,28 @@ test("복수 음원 대응 result map 계약과 shape 관계를 검증한다", a
|
||||
id: "tone-1k",
|
||||
color: "#65f2bf",
|
||||
rawMap: { rows: 50, columns: 50, scale: "power", values: Array(2500).fill(0) },
|
||||
peaks: [{ sourceId: "source-1", position: [0.08, 0.04, 2], value: 1, localizationErrorM: 0 }],
|
||||
peaks: [{
|
||||
sourceId: "source-1",
|
||||
position: [0.08, 0.04, 2],
|
||||
value: 1,
|
||||
localizationErrorM: 0,
|
||||
beamSpread: {
|
||||
calibrated: false,
|
||||
method: "connected-half-power-power-weighted-covariance",
|
||||
halfPowerRatio: 0.5,
|
||||
thresholdValue: 0.5,
|
||||
pixelCount: 1,
|
||||
centroid: [0.08, 0.04, 2],
|
||||
covarianceM2: { xx: 0, xy: 0, yy: 0 },
|
||||
ellipse: {
|
||||
axisScale: "rms",
|
||||
angleReference: "counterclockwise-from-positive-x",
|
||||
majorRmsRadiusM: 0,
|
||||
minorRmsRadiusM: 0,
|
||||
angleDeg: 0,
|
||||
},
|
||||
},
|
||||
}],
|
||||
metrics: { energyInsideTruthRatio: 1, precision: 1, recall: 1 },
|
||||
}],
|
||||
};
|
||||
@@ -419,17 +440,39 @@ test("복수 음원 대응 result map 계약과 shape 관계를 검증한다", a
|
||||
experiment.beamforming.algorithm = "das-phat";
|
||||
result.algorithm = "das-phat";
|
||||
result.bands[0].rawMap.scale = "score";
|
||||
result.bands[0].peaks[0].beamSpread.method =
|
||||
"connected-half-maximum-score-weighted-covariance";
|
||||
assertEqual(
|
||||
(await validateResult(result, { experiment, scene })).length,
|
||||
0,
|
||||
"phase-weighted score result issues",
|
||||
);
|
||||
|
||||
result.algorithm = "das";
|
||||
result.bands[0].rawMap.scale = "power";
|
||||
let issues = await validateResult(result, { experiment, scene });
|
||||
assert(
|
||||
issues.some(({ path }) => path.endsWith("rawMap.scale")),
|
||||
"algorithm and raw map scale must agree",
|
||||
);
|
||||
result.bands[0].rawMap.scale = "score";
|
||||
result.bands[0].peaks[0].beamSpread.method =
|
||||
"connected-half-power-power-weighted-covariance";
|
||||
issues = await validateResult(result, { experiment, scene });
|
||||
assert(
|
||||
issues.some(({ path }) => path.endsWith("beamSpread.method")),
|
||||
"score map and beam spread method must agree",
|
||||
);
|
||||
result.bands[0].peaks[0].beamSpread.method =
|
||||
"connected-half-maximum-score-weighted-covariance";
|
||||
|
||||
result.algorithm = "das";
|
||||
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.scale = "power";
|
||||
result.bands[0].peaks[0].beamSpread.method =
|
||||
"connected-half-power-power-weighted-covariance";
|
||||
result.bands[0].rawMap.values.pop();
|
||||
result.bands[0].rawMap.scale = "normalized";
|
||||
result.bands[0].peaks[0].sourceId = "missing-source";
|
||||
|
||||
@@ -174,6 +174,18 @@ test("GCC-PHAT workload reports reference-pair scan reduction and rejects aliase
|
||||
"maximum geometric lag is exposed and unaliased",
|
||||
);
|
||||
|
||||
let preflightError;
|
||||
try {
|
||||
estimateGccPhatWorkload({ ...fixture, maximumWorkingBytes: 1 });
|
||||
} catch (error) {
|
||||
preflightError = error;
|
||||
}
|
||||
assert(
|
||||
preflightError instanceof RangeError &&
|
||||
preflightError.message.includes("before geometry allocation"),
|
||||
"unplanned workload is rejected before allocating its distance matrix",
|
||||
);
|
||||
|
||||
let memoryError;
|
||||
try {
|
||||
createGccPhatAnalysisPlan({ ...fixture, maximumPlanBytes: 1 });
|
||||
@@ -241,8 +253,8 @@ test("GCC-PHAT corrected lag sign finds a deterministic broadband source and emi
|
||||
assertNear(result.maximumLagSamples, result.workload.maximumLagSamples, 0, "lag metadata");
|
||||
assertEqual(band.rawScale, "score", "raw map scale");
|
||||
assert(
|
||||
band.rawMap.every((value) => Number.isFinite(value) && value >= 0),
|
||||
"shifted and squared score map is finite and nonnegative",
|
||||
band.rawMap.every((value) => Number.isFinite(value) && value >= 0 && value <= 1),
|
||||
"pair- and bandwidth-normalized score map is finite and bounded",
|
||||
);
|
||||
assert(
|
||||
distance3(peakPosition, fixture.target) <= 0.15,
|
||||
@@ -263,6 +275,48 @@ test("GCC-PHAT corrected lag sign finds a deterministic broadband source and emi
|
||||
"X_m * conj(X_ref) correlation uses the physical arrival-lag sign",
|
||||
);
|
||||
assertNear(Math.max(...band.displayMap), 1, 1e-6, "normalized score display");
|
||||
|
||||
const expandedFixture = createBroadbandFixture();
|
||||
expandedFixture.experiment.scan = {
|
||||
...expandedFixture.experiment.scan,
|
||||
xMin: -0.8,
|
||||
xMax: 0.8,
|
||||
yMin: -0.8,
|
||||
yMax: 0.8,
|
||||
columns: 17,
|
||||
rows: 17,
|
||||
};
|
||||
const expanded = await computeGccPhatMap({
|
||||
...expandedFixture,
|
||||
phatFloorRatio: 1e-5,
|
||||
pointBatchSize: 1024,
|
||||
stftTransformBatchSize: 1024,
|
||||
});
|
||||
const expandedTargetIndex = scanIndexFor(
|
||||
expandedFixture.experiment.scan,
|
||||
expandedFixture.target[0],
|
||||
expandedFixture.target[1],
|
||||
);
|
||||
assertNear(
|
||||
expanded.bands[0].rawMap[expandedTargetIndex],
|
||||
band.rawMap[targetIndex],
|
||||
1e-6,
|
||||
"raw score at a fixed hypothesis is independent of scan extent",
|
||||
);
|
||||
});
|
||||
|
||||
test("GCC-PHAT compute rejects an oversized unplanned working set before geometry", async () => {
|
||||
const fixture = createBroadbandFixture();
|
||||
let error;
|
||||
try {
|
||||
await computeGccPhatMap({ ...fixture, maximumWorkingBytes: 1 });
|
||||
} catch (candidate) {
|
||||
error = candidate;
|
||||
}
|
||||
assert(
|
||||
error instanceof RangeError && error.message.includes("before geometry allocation"),
|
||||
"compute preflight error",
|
||||
);
|
||||
});
|
||||
|
||||
test("GCC-PHAT analysis plan reuses geometry and FFT workspaces and releases its busy lock", async () => {
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
createLocalizationAnalysisPlan,
|
||||
estimateLocalizationWorkload,
|
||||
} from "/packages/acoustics-core/localization.js";
|
||||
import {
|
||||
DEFAULT_GCC_PHAT_CROSS_SPECTRUM_BATCH_SIZE,
|
||||
DEFAULT_GCC_PHAT_POINT_BATCH_SIZE,
|
||||
} from "/packages/acoustics-core/gcc-phat.js";
|
||||
import {
|
||||
DEFAULT_LOCALIZATION_ALGORITHM,
|
||||
LOCALIZATION_ALGORITHM_IDS,
|
||||
@@ -167,10 +171,22 @@ test("portable and streaming API accept an algorithm choice without Scene synthe
|
||||
computeOptions: { pointBatchSize: 1024, stftTransformBatchSize: 1024 },
|
||||
};
|
||||
const plan = createLocalizationPlan(config);
|
||||
const defaultBatchPlan = createLocalizationPlan({ ...config, computeOptions: undefined });
|
||||
const localized = await localizeMultichannel({ plan, channels: fixture.channels });
|
||||
assertEqual(plan.algorithm, "gcc-phat", "portable plan algorithm");
|
||||
assertEqual(plan.computeOptions.phatFloorRatio, 1e-5, "portable PHAT floor forwarding");
|
||||
assertEqual(plan.analysisPlan, plan.gccPhatPlan, "portable GCC plan alias");
|
||||
assertEqual(plan.computeOptions.pointBatchSize, 1024, "explicit portable point batch override");
|
||||
assertEqual(
|
||||
defaultBatchPlan.computeOptions.pointBatchSize,
|
||||
DEFAULT_GCC_PHAT_POINT_BATCH_SIZE,
|
||||
"GCC lookup-oriented default batch",
|
||||
);
|
||||
assertEqual(
|
||||
defaultBatchPlan.computeOptions.crossSpectrumBatchSize,
|
||||
DEFAULT_GCC_PHAT_CROSS_SPECTRUM_BATCH_SIZE,
|
||||
"GCC cross-spectrum cancellation batch",
|
||||
);
|
||||
assertEqual(localized.algorithm, "gcc-phat", "portable result algorithm");
|
||||
assertEqual(localized.phatFloorRatio, 1e-5, "portable PHAT floor reaches GCC core");
|
||||
assertEqual(localized.bands[0].rawScale, "score", "portable score map");
|
||||
|
||||
Reference in New Issue
Block a user