feat: add standard peak grouping methods

This commit is contained in:
2026-08-29 09:00:40 +09:00
parent 447f77dfa7
commit e048ccbadc
7 changed files with 299 additions and 4 deletions
+47
View File
@@ -113,6 +113,52 @@ test("peak 검출 감도를 낮추면 약한 두 번째 음원을 선택한다",
assertEqual(recommended.parameters.detectionThresholdRatio, 0.25, "selected ratio is recorded");
});
test("connected component와 watershed가 연결된 복수 lobe를 서로 다르게 군집화한다", () => {
const scan = { xMin: 0, xMax: 6, yMin: 0, yMax: 2, z: 1, columns: 7, rows: 3 };
const rawMap = Float32Array.of(
0, 0, 0, 0, 0, 0, 0,
0, 10, 8, 6, 8, 9, 0,
0, 0, 0, 0, 0, 0, 0,
);
const common = {
rawMap,
scan,
sources: [],
detectionThresholdRatio: 0.5,
minimumPeakDistanceM: 0,
};
const local = evaluateDasBand({ ...common, detectionMethod: "local-max" });
const connected = evaluateDasBand({ ...common, detectionMethod: "connected-components" });
const watershed = evaluateDasBand({ ...common, detectionMethod: "watershed" });
assertEqual(local.detections.length, 2, "local maxima keeps both summits");
assertEqual(connected.detections.length, 1, "connected lobe becomes one component");
assertNear(connected.detections[0].position[0], 121 / 41, 1e-12, "component weighted centroid x");
assertNear(connected.detections[0].position[1], 1, 1e-12, "component weighted centroid y");
assertEqual(connected.detections[0].clusterPixelCount, 5, "component pixel count");
assertEqual(watershed.detections.length, 2, "watershed splits the connected lobe at its saddle");
assertNear(watershed.detections[0].position[0], 44 / 24, 1e-12, "strong basin centroid");
assertNear(watershed.detections[1].position[0], 77 / 17, 1e-12, "weak basin centroid");
assertEqual(connected.parameters.detectionMethod, "connected-components", "component method recorded");
assertEqual(watershed.parameters.detectionMethod, "watershed", "watershed method recorded");
assertEqual(
JSON.stringify(evaluateDasBand({ ...common, detectionMethod: "watershed" })),
JSON.stringify(watershed),
"watershed assignment is deterministic",
);
const suppressed = evaluateDasBand({
...common,
detectionMethod: "watershed",
minimumPeakDistanceM: 10,
});
assertEqual(suppressed.detections.length, 1, "common minimum-distance suppression still applies");
assert(
captureError(() => evaluateDasBand({ ...common, detectionMethod: "unknown" })) instanceof RangeError,
"unsupported detection method is rejected",
);
});
test("zero map 평가가 빈 peak와 유한한 metric을 반환한다", () => {
const scan = { xMin: -1, xMax: 1, yMin: -1, yMax: 1, z: 1, columns: 2, rows: 2 };
const evaluation = evaluateDasBand({
@@ -338,6 +384,7 @@ test("result exporter가 schema 허용 metric만 선택하고 내부 필드를
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");
assertEqual(exported.evaluation.detectionMethod, "local-max", "detection method exported");
assertEqual(exported.evaluation.matchToleranceM, 0, "match tolerance exported");
assertEqual(
Object.keys(selectResultMetrics(evaluation)).join(","),