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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+28
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user