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
|
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 summary = summarize(dets)
|
||||||
val out = guide4.step(summary)
|
val out = guide4.step(summary)
|
||||||
lastSeenPhase = guide4.phase
|
lastSeenPhase = guide4.phase
|
||||||
|
|||||||
@@ -54,12 +54,19 @@ object MethodDRunner {
|
|||||||
/**
|
/**
|
||||||
* 6채널 (또는 N채널) raw 신호 → 채널별 MethodDResult? 리스트.
|
* 6채널 (또는 N채널) raw 신호 → 채널별 MethodDResult? 리스트.
|
||||||
* raw[ch] 길이가 다르면 그대로 처리 (각 채널 독립).
|
* raw[ch] 길이가 다르면 그대로 처리 (각 채널 독립).
|
||||||
|
*
|
||||||
|
* @param applyCross false 면 교차채널 보정을 건너뛴 **base 검출**만 반환.
|
||||||
|
* 2026-07-06 이식 (piezophantomtest ea3f15c):
|
||||||
|
* 정렬 위치 선택 (`AlignGuide` / `select_alignment_position`) 은 base 검출로 수행해야 함
|
||||||
|
* — 교차채널 보정이 nch 를 바꿔 위치 선택이 흔들리는 것을 차단.
|
||||||
|
* BV 산출은 applyCross=true 로 별도 수행.
|
||||||
*/
|
*/
|
||||||
fun detectMultichannel(
|
fun detectMultichannel(
|
||||||
signals: List<DoubleArray>,
|
signals: List<DoubleArray>,
|
||||||
params: MethodDParams = MethodDParams.DEFAULT,
|
params: MethodDParams = MethodDParams.DEFAULT,
|
||||||
beamAnglesDeg: DoubleArray? = null,
|
beamAnglesDeg: DoubleArray? = null,
|
||||||
applyTgc: Boolean = true,
|
applyTgc: Boolean = true,
|
||||||
|
applyCross: Boolean = true,
|
||||||
): List<MethodDResult?> {
|
): List<MethodDResult?> {
|
||||||
val angles = beamAnglesDeg ?: PiezoHW.degreeAll
|
val angles = beamAnglesDeg ?: PiezoHW.degreeAll
|
||||||
// 1) per-channel SG denoise (heavy + light)
|
// 1) per-channel SG denoise (heavy + light)
|
||||||
@@ -81,7 +88,9 @@ object MethodDRunner {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 5) cross-channel 후처리 (Python `apply_cross_channel` 순서 고정)
|
// 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
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-6
@@ -22,6 +22,16 @@ import kotlin.math.pow
|
|||||||
|
|
||||||
object MethodDWallSelect {
|
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 Side { ANT, POST }
|
||||||
enum class CType { PEAK, SHOULDER }
|
enum class CType { PEAK, SHOULDER }
|
||||||
|
|
||||||
@@ -96,20 +106,32 @@ object MethodDWallSelect {
|
|||||||
}
|
}
|
||||||
if (hi <= lo) return WallResult(null, emptyList())
|
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 relPeaks = PeakDetection.findPeaks1D(sub, 0, sub.size)
|
||||||
val relShoulders = findShouldersLocal(sub)
|
val relShoulders = findShouldersLocal(sub)
|
||||||
|
|
||||||
val candIdxType = mutableListOf<Pair<Int, CType>>()
|
val candIdxType = mutableListOf<Pair<Int, CType>>()
|
||||||
for (p in relPeaks) candIdxType += Pair(lo + p, CType.PEAK)
|
for (p in relPeaks) candIdxType += Pair(vLo + p, CType.PEAK)
|
||||||
for (s in relShoulders) {
|
// 2026-07-06 (piezophantomtest b252de0): post 는 peak-only.
|
||||||
val idx = lo + s
|
// shoulder(d2 변곡) 는 후벽을 깊은쪽으로 과확장 → BV 과대 (특히 30° 채널).
|
||||||
if (candIdxType.any { it.second == CType.PEAK && abs(it.first - idx) <= 1 }) continue
|
// ant 는 shoulder 유지 (near-field 전벽은 shoulder 로 잡히는 게 정상).
|
||||||
candIdxType += Pair(idx, CType.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>()
|
val scored = mutableListOf<Candidate>()
|
||||||
for ((p, ctype) in candIdxType) {
|
for ((p, ctype) in candIdxType) {
|
||||||
|
// view 확장은 경계 peak 확정 전용 — 수용은 [lo, hi] 로 제한.
|
||||||
|
if (p !in lo..hi) continue
|
||||||
val vIdx = adjacentValley(sig, p, edge)
|
val vIdx = adjacentValley(sig, p, edge)
|
||||||
val prom = sig[p] - sig[vIdx]
|
val prom = sig[p] - sig[vIdx]
|
||||||
val gate = if (ctype == CType.PEAK) params.minPeakProm else params.minShoulderProm
|
val gate = if (ctype == CType.PEAK) params.minPeakProm else params.minShoulderProm
|
||||||
|
|||||||
Reference in New Issue
Block a user