feat: visualize beam spread around peaks
This commit is contained in:
@@ -191,6 +191,22 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
|
||||
value: 1,
|
||||
localizationErrorM: 0.05,
|
||||
localizationErrorPixels: 0.1,
|
||||
beamSpread: {
|
||||
calibrated: false,
|
||||
method: "connected-half-power-power-weighted-covariance",
|
||||
halfPowerRatio: 0.5,
|
||||
thresholdValue: 0.5,
|
||||
pixelCount: 3,
|
||||
centroid: [0.9, -0.9, 2],
|
||||
covarianceM2: { xx: 0.04, xy: 0.01, yy: 0.02 },
|
||||
ellipse: {
|
||||
axisScale: "rms",
|
||||
angleReference: "counterclockwise-from-positive-x",
|
||||
majorRmsRadiusM: 0.21,
|
||||
minorRmsRadiusM: 0.12,
|
||||
angleDeg: 22.5,
|
||||
},
|
||||
},
|
||||
}],
|
||||
metrics: {
|
||||
peakToSidelobeDb: 10,
|
||||
@@ -232,6 +248,8 @@ test("Camera result document is schema-safe and linked to its snapshot", async (
|
||||
"only finite schema metrics are exported",
|
||||
);
|
||||
assertEqual(result.bands[0].peaks[0].localizationErrorPixels, 0.1, "pixel error is exported");
|
||||
assertEqual(result.bands[0].peaks[0].beamSpread.pixelCount, 3, "beam spread is exported");
|
||||
assertEqual(result.bands[0].peaks[0].beamSpread.calibrated, false, "beam spread is not probability");
|
||||
assertEqual(result.bands[0].evaluation.matchToleranceM, 0.2, "evaluation settings are exported");
|
||||
assertEqual(result.environment.scanXMin, -1, "scan bounds are recorded");
|
||||
assertEqual(result.environment.fallbackCode, "webgpu-unavailable", "fallback is recorded");
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
evaluateDasBand,
|
||||
exportBandEvaluation,
|
||||
findLocalPeaks,
|
||||
measureBeamSpread,
|
||||
measureMainLobe,
|
||||
selectResultMetrics,
|
||||
} from "/packages/acoustics-core/evaluation.js";
|
||||
@@ -137,6 +138,111 @@ test("flat plateau와 main-lobe width가 deterministic하다", () => {
|
||||
assertNear(lobe.equivalentDiameterM, 6 / Math.sqrt(Math.PI), 1e-12, "equivalent main-lobe diameter");
|
||||
});
|
||||
|
||||
test("beam spread가 회전된 비등방성 half-power lobe의 covariance ellipse를 측정한다", () => {
|
||||
const scan = { xMin: -3, xMax: 3, yMin: -3, yMax: 3, z: 2, columns: 7, rows: 7 };
|
||||
const rawMap = new Float32Array(49);
|
||||
const setPower = (x, y, value) => {
|
||||
rawMap[(3 - y) * scan.columns + x + 3] = value;
|
||||
};
|
||||
setPower(0, 0, 10);
|
||||
setPower(1, 1, 8);
|
||||
setPower(-1, -1, 8);
|
||||
setPower(2, 2, 6);
|
||||
setPower(-2, -2, 6);
|
||||
setPower(-1, 1, 5.5);
|
||||
setPower(1, -1, 5.5);
|
||||
|
||||
const spread = measureBeamSpread(rawMap, scan, 24);
|
||||
assertEqual(spread.calibrated, false, "beam spread is explicitly uncalibrated");
|
||||
assertEqual(
|
||||
spread.method,
|
||||
"connected-half-power-power-weighted-covariance",
|
||||
"beam spread method",
|
||||
);
|
||||
assertEqual(spread.pixelCount, 7, "connected half-power pixels");
|
||||
assertNear(spread.thresholdValue, 5, 1e-12, "half-power threshold");
|
||||
assertNear(spread.centroid[0], 0, 1e-12, "weighted centroid x");
|
||||
assertNear(spread.centroid[1], 0, 1e-12, "weighted centroid y");
|
||||
assertEqual(spread.centroid[2], 2, "weighted centroid z");
|
||||
assertNear(spread.covarianceM2.xx, 75 / 49, 1e-12, "rotated covariance xx");
|
||||
assertNear(spread.covarianceM2.xy, 53 / 49, 1e-12, "rotated covariance xy");
|
||||
assertNear(spread.covarianceM2.yy, 75 / 49, 1e-12, "rotated covariance yy");
|
||||
assertNear(spread.ellipse.majorRmsRadiusM, Math.sqrt(128 / 49), 1e-12, "major RMS radius");
|
||||
assertNear(spread.ellipse.minorRmsRadiusM, Math.sqrt(22 / 49), 1e-12, "minor RMS radius");
|
||||
assertNear(spread.ellipse.angleDeg, 45, 1e-12, "major-axis angle");
|
||||
assertEqual(spread.ellipse.axisScale, "rms", "ellipse scale is explicit");
|
||||
|
||||
const evaluation = evaluateDasBand({
|
||||
rawMap,
|
||||
scan,
|
||||
sources: [],
|
||||
detectionThresholdRatio: 0.5,
|
||||
minimumPeakDistanceM: 0,
|
||||
});
|
||||
assertEqual(evaluation.detections.length, 1, "single selected detection");
|
||||
assertEqual(
|
||||
JSON.stringify(evaluation.detections[0].beamSpread),
|
||||
JSON.stringify(spread),
|
||||
"selected detection exposes its beam spread",
|
||||
);
|
||||
});
|
||||
|
||||
test("beam spread가 1-row, 1-column, single-pixel lobe에서 유한하고 결정적이다", () => {
|
||||
const powers = Float32Array.of(5, 8, 10, 8, 5);
|
||||
const rowSpread = measureBeamSpread(
|
||||
powers,
|
||||
{ xMin: -2, xMax: 2, yMin: 7, yMax: 7, z: 3, columns: 5, rows: 1 },
|
||||
2,
|
||||
);
|
||||
const columnSpread = measureBeamSpread(
|
||||
powers,
|
||||
{ xMin: 4, xMax: 4, yMin: 5, yMax: 9, z: 3, columns: 1, rows: 5 },
|
||||
2,
|
||||
);
|
||||
const pointScan = { xMin: 4, xMax: 4, yMin: 7, yMax: 7, z: 3, columns: 1, rows: 1 };
|
||||
const pointSpread = measureBeamSpread(Float32Array.of(10), pointScan, 0);
|
||||
const silentSpread = measureBeamSpread(Float32Array.of(0), pointScan, 0);
|
||||
|
||||
assertEqual(rowSpread.pixelCount, 5, "one-row lobe size");
|
||||
assertNear(rowSpread.covarianceM2.xx, 14 / 9, 1e-12, "one-row x variance");
|
||||
assertEqual(rowSpread.covarianceM2.yy, 0, "one-row y variance");
|
||||
assertNear(rowSpread.ellipse.majorRmsRadiusM, Math.sqrt(14 / 9), 1e-12, "one-row major radius");
|
||||
assertEqual(rowSpread.ellipse.minorRmsRadiusM, 0, "one-row minor radius");
|
||||
assertEqual(rowSpread.ellipse.angleDeg, 0, "one-row angle");
|
||||
|
||||
assertEqual(columnSpread.covarianceM2.xx, 0, "one-column x variance");
|
||||
assertNear(columnSpread.covarianceM2.yy, 14 / 9, 1e-12, "one-column y variance");
|
||||
assertNear(columnSpread.ellipse.majorRmsRadiusM, Math.sqrt(14 / 9), 1e-12, "one-column major radius");
|
||||
assertEqual(columnSpread.ellipse.minorRmsRadiusM, 0, "one-column minor radius");
|
||||
assertEqual(columnSpread.ellipse.angleDeg, 90, "one-column angle");
|
||||
|
||||
assertEqual(pointSpread.centroid.join(","), "4,7,3", "single-pixel centroid");
|
||||
assertEqual(pointSpread.pixelCount, 1, "single-pixel lobe size");
|
||||
assertEqual(pointSpread.ellipse.majorRmsRadiusM, 0, "single-pixel major radius");
|
||||
assertEqual(pointSpread.ellipse.minorRmsRadiusM, 0, "single-pixel minor radius");
|
||||
assertEqual(silentSpread.pixelCount, 0, "silent lobe has no pixels");
|
||||
|
||||
for (const spread of [rowSpread, columnSpread, pointSpread, silentSpread]) {
|
||||
const numbers = [
|
||||
spread.halfPowerRatio,
|
||||
spread.thresholdValue,
|
||||
spread.pixelCount,
|
||||
...spread.centroid,
|
||||
...Object.values(spread.covarianceM2),
|
||||
spread.ellipse.majorRmsRadiusM,
|
||||
spread.ellipse.minorRmsRadiusM,
|
||||
spread.ellipse.angleDeg,
|
||||
];
|
||||
assert(numbers.every(Number.isFinite), "every degenerate beam-spread number is finite");
|
||||
}
|
||||
|
||||
assertEqual(
|
||||
JSON.stringify(measureBeamSpread(powers, { xMin: -2, xMax: 2, yMin: 7, yMax: 7, z: 3, columns: 5, rows: 1 }, 2)),
|
||||
JSON.stringify(rowSpread),
|
||||
"degenerate measurement is repeatable",
|
||||
);
|
||||
});
|
||||
|
||||
test("result exporter가 schema 허용 metric만 선택하고 내부 필드를 제거한다", () => {
|
||||
const scan = { xMin: 0, xMax: 1, yMin: 0, yMax: 1, z: 1, columns: 2, rows: 2 };
|
||||
const evaluation = evaluateDasBand({
|
||||
@@ -154,6 +260,16 @@ test("result exporter가 schema 허용 metric만 선택하고 내부 필드를
|
||||
assertEqual(exported.peaks[0].sourceId, "truth", "schema peak source match");
|
||||
assertEqual(exported.peaks[0].localizationErrorM, 0, "schema peak localization error");
|
||||
assertEqual(exported.peaks[0].localizationErrorPixels, 0, "schema peak pixel error");
|
||||
assertEqual(
|
||||
JSON.stringify(exported.peaks[0].beamSpread),
|
||||
JSON.stringify(evaluation.detections[0].beamSpread),
|
||||
"schema peak retains beam-spread descriptor",
|
||||
);
|
||||
assert(exported.peaks[0].beamSpread !== evaluation.detections[0].beamSpread, "beam spread is copied");
|
||||
assert(
|
||||
exported.peaks[0].beamSpread.covarianceM2 !== evaluation.detections[0].beamSpread.covarianceM2,
|
||||
"beam-spread covariance is copied",
|
||||
);
|
||||
assert(!("index" in exported.peaks[0]), "internal peak index omitted");
|
||||
assert(!("mainLobeWidthM" in exported.metrics), "explicit metric selection is respected");
|
||||
assertEqual(exported.evaluation.version, "1.0.0", "evaluation version exported");
|
||||
|
||||
Reference in New Issue
Block a user