feat(algo): 데모 piezophantomtest ea3f15c/b252de0 이식 — post peak-only + apply_cross flag

## 이식 대상
알고리즘팀 main : 6229825 → ea3f15c (fast-forward)
- b252de0 : post wall 후보는 peak-only 로 탐색 (shoulder 제외)
- 4afda38 : method_d(apply_cross) 옵션 추가

## MethodDWallSelect 변경
1. BOUNDARY_PEAK_MARGIN = 2 (post 전용)
   - post 탐색창 hi 를 2 샘플 밖으로 넓혀 view 확보 → 경계에 걸린 peak 이
     shoulder 로 오분류되던 케이스 해결. 수용 범위 [lo, hi] 유지 → farther
     peak 새로 안 받음 (d_max 전역 확대 부작용 회피).
2. side=POST 는 peak-only
   - shoulder(d2 변곡) 는 후벽을 깊은쪽으로 과확장 → BV 과대 (특히 30° 채널).
   - ant 는 shoulder 유지 (near-field 전벽은 shoulder 로 잡히는 게 정상).

## MethodDRunner 변경
- detectMultichannel(applyCross: Boolean = true) 파라미터 추가.
  false 면 교차채널 3종 (ant_tiebreak / neighbor_top_validate / inward_post) 건너뜀.

## AlignmentAdvisorV2 (RollingAligner) 변경
- 정렬 위치 선택은 applyCross=false 로 base 검출 사용.
  이유: 교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단.
  BV 산출은 PiezoMonitoringView 경로에서 별도로 applyCross=true 로 진행.

## 컴파일
:app:compileDevDebugKotlin / :compileDemoDebugKotlin / UnitTest 모두 통과.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 10:22:56 +09:00
parent 097d086bce
commit 15db5ec8bf
3 changed files with 42 additions and 8 deletions
@@ -568,7 +568,10 @@ class RollingAligner(
}
}
lastAvg = avg
val dets = MethodDRunner.detectMultichannel(avg, methodDParams)
// 2026-07-06 (piezophantomtest ea3f15c): 정렬 위치 선택은 base 검출로 수행
// (교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단).
// BV 산출은 별도 경로 (PiezoMonitoringView) 에서 applyCross=true 로 진행.
val dets = MethodDRunner.detectMultichannel(avg, methodDParams, applyCross = false)
val summary = summarize(dets)
val out = guide4.step(summary)
lastSeenPhase = guide4.phase
@@ -54,12 +54,19 @@ object MethodDRunner {
/**
* 6채널 (또는 N채널) raw 신호 → 채널별 MethodDResult? 리스트.
* raw[ch] 길이가 다르면 그대로 처리 (각 채널 독립).
*
* @param applyCross false 면 교차채널 보정을 건너뛴 **base 검출**만 반환.
* 2026-07-06 이식 (piezophantomtest ea3f15c):
* 정렬 위치 선택 (`AlignGuide` / `select_alignment_position`) 은 base 검출로 수행해야 함
* — 교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단.
* BV 산출은 applyCross=true 로 별도 수행.
*/
fun detectMultichannel(
signals: List<DoubleArray>,
params: MethodDParams = MethodDParams.DEFAULT,
beamAnglesDeg: DoubleArray? = null,
applyTgc: Boolean = true,
applyCross: Boolean = true,
): List<MethodDResult?> {
val angles = beamAnglesDeg ?: PiezoHW.degreeAll
// 1) per-channel SG denoise (heavy + light)
@@ -81,7 +88,9 @@ object MethodDRunner {
)
}
// 5) cross-channel 후처리 (Python `apply_cross_channel` 순서 고정)
applyCrossChannel(results, angles, lightTgc, heavyTgc, signals, params)
if (applyCross) {
applyCrossChannel(results, angles, lightTgc, heavyTgc, signals, params)
}
return results
}
@@ -22,6 +22,16 @@ import kotlin.math.pow
object MethodDWallSelect {
// 경계 peak 구제 margin — 2026-07-06 이식 (piezophantomtest b252de0):
// 진짜 peak 이 탐색창 끝(post=hi) 에 걸리면 find_peaks_1d 가 바깥 샘플을 못 봐
// local max 확정 실패 → shoulder 로 오분류(p1_1/v2 CH3). peak 확정용 view 만
// 이 margin 만큼 바깥으로 넓히고 수용 범위는 [lo,hi] 유지 → farther peak 을
// 새로 받지 않아 d_max 전역 확대 부작용 회피.
// post 전용 — ant 는 경계 확장이 벽을 surface 쪽으로 흔들어 채널 탈락시키는
// 민감성이 있어 적용 X.
private const val BOUNDARY_PEAK_MARGIN: Int = 2
enum class Side { ANT, POST }
enum class CType { PEAK, SHOULDER }
@@ -96,20 +106,32 @@ object MethodDWallSelect {
}
if (hi <= lo) return WallResult(null, emptyList())
val sub = DoubleArray(hi - lo + 1) { sig[lo + it] }
// 2026-07-06 (piezophantomtest b252de0): peak 확정용 view 를 경계 밖으로 margin 만큼
// 확장 (post 전용). 수용 범위 [lo, hi] 는 아래서 유지.
val margin = if (side == Side.POST) BOUNDARY_PEAK_MARGIN else 0
val vLo = lo
val vHi = minOf(n - 1, hi + margin)
val sub = DoubleArray(vHi - vLo + 1) { sig[vLo + it] }
val relPeaks = PeakDetection.findPeaks1D(sub, 0, sub.size)
val relShoulders = findShouldersLocal(sub)
val candIdxType = mutableListOf<Pair<Int, CType>>()
for (p in relPeaks) candIdxType += Pair(lo + p, CType.PEAK)
for (s in relShoulders) {
val idx = lo + s
if (candIdxType.any { it.second == CType.PEAK && abs(it.first - idx) <= 1 }) continue
candIdxType += Pair(idx, CType.SHOULDER)
for (p in relPeaks) candIdxType += Pair(vLo + p, CType.PEAK)
// 2026-07-06 (piezophantomtest b252de0): post 는 peak-only.
// shoulder(d2 변곡) 는 후벽을 깊은쪽으로 과확장 → BV 과대 (특히 30° 채널).
// ant 는 shoulder 유지 (near-field 전벽은 shoulder 로 잡히는 게 정상).
if (side != Side.POST) {
for (s in relShoulders) {
val idx = vLo + s
if (candIdxType.any { it.second == CType.PEAK && abs(it.first - idx) <= 1 }) continue
candIdxType += Pair(idx, CType.SHOULDER)
}
}
val scored = mutableListOf<Candidate>()
for ((p, ctype) in candIdxType) {
// view 확장은 경계 peak 확정 전용 — 수용은 [lo, hi] 로 제한.
if (p !in lo..hi) continue
val vIdx = adjacentValley(sig, p, edge)
val prom = sig[p] - sig[vIdx]
val gate = if (ctype == CType.PEAK) params.minPeakProm else params.minShoulderProm