441f249410
레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.
· post_tie_lock 후벽 상위 두 후보의 score 가 구조적으로 붙는 구간(전 데이터
5773 결정 중 격차<10% 가 17.9%)에서 직전 trace 선택을 유지한다.
방향을 강제하지 않는다 — 깊은-peak 고정·인접 골 tie-break 은
전부 계통 편향을 낳았고, tie 밴드에서는 정의상 두 후보의 증거가
같기 때문이다. 상태기를 넘길 때만 동작하며, 레퍼런스도 streaming
strict pass 에서만 건다. streaming.py 이식 전까지 호출부는 없다.
· _sibeam_cap_bounds 검출 채널이 정확히 2개일 때 미검출 이웃 빔까지의 SI 거리를
cap 상한으로. 2개뿐이면 끝 채널이 서로의 이웃이라 기존 top/bottom
clamp 가 성립하지 않아 cap 이 무제한 외삽된다. supine 은 건너뛴다.
· F83 적도-밖 bottom cap 연속 보간(실리콘·supine). 전제인
mirror_bottom_cap 도 함께 옮겼다.
좌표계가 두 개라는 것을 확인했다
레퍼런스 cross_channel._z 는 sample_to_ap_depth 를 인자 없이 불러 교차채널 좌표가
언제나 모듈 기본값(1.936/6.85)으로 떨어진다. BV 는 프리셋 값을 명시로 넘긴다.
abs 는 두 값이 같아 표가 안 나지만 실리콘(1.897/7.651)에서는 갈린다 — 실측
f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 439.88 vs 416.95 mL 이 됐다.
is_deep 판정이 34.209 > 34.042 처럼 아슬아슬한 자리라 작은 좌표 차가 선택을 뒤집는다.
의도인지 누락인지는 알고리즘팀 확인이 필요하다. 확인 전까지 레퍼런스와 같은 값을
내는 쪽을 택했고, 기존 경로는 프리셋 값을 그대로 쓴다.
missing_outside 의 null 과 빈 집합을 구분했다
null = 계산 안 함 → cap clamp 를 언제나 상한으로. 빈 집합 = 계산했고 '방광 밖'
채널이 없다 → 하한 쪽으로. ifEmpty { null } 로 뭉개면 "미검출이지만 span 은 있다"는
가장 흔한 경우에 si_floor 가 통째로 죽는다.
검증
· 레퍼런스 대조 6종 전부 불일치 0 — tie 1568행(lock 24건) · 실데이터 BV 784행
(nch=2 6건) · sibeam 120행(상한 102건) · 합성 BV 640행(mirror 226건) ·
F83 blend 440행 · F83 apply 12행. 실데이터가 미러·nch=2 에 거의 안 닿아
그 경로는 합성 입력으로 레퍼런스 함수를 직접 호출한 기대값으로 덮었다.
· 기존 경로 무변경 — 44 cycle × CCC on/off 덤프가 HEAD 와 88행 전부 일치.
· 전체 36개 통과 · demoDebug APK 빌드 성공.
LegacyEquivalenceDumpTest 가 PiezoHW.activePreset 을 명시로 고정하게 했다. 전역
가변 상태라 다른 테스트가 먼저 돌면 덤프가 통째로 달라진다 — 실제로 한 번 착시를
만들었다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
3.6 KiB
Kotlin
78 lines
3.6 KiB
Kotlin
package com.medithings.vesiscan.managers
|
|
|
|
import com.medithings.vesiscan.walldetect.MethodDRunner
|
|
import org.junit.Test
|
|
import java.io.File
|
|
|
|
/**
|
|
* 기존(팬텀) 경로가 이번 이식으로 **한 글자도 안 바뀌었는지** 실증하는 덤프 하네스.
|
|
*
|
|
* 검출 → BV 전 구간을 `align_cm{0,1}.csv` 의 모든 cycle 에 대해 돌려 결과를 파일로
|
|
* 떨군다. HEAD 코드와 작업트리 코드에서 각각 돌린 덤프가 **바이트 단위로 같으면**
|
|
* 기존 경로 무변경이 증명된다.
|
|
*
|
|
* 이 파일은 [AlgoMode] 를 참조하지 않는다 — HEAD 에는 그 타입이 없기 때문이다.
|
|
* 기본값이 기존 경로이므로 기본 호출만으로 그 경로가 그대로 실행된다.
|
|
*/
|
|
class LegacyEquivalenceDumpTest {
|
|
|
|
private fun res(name: String) =
|
|
javaClass.classLoader!!.getResourceAsStream(name)!!.bufferedReader().use { it.readText() }
|
|
|
|
/** cycle → 6채널 신호 (`cycle,channel,s0..s99`). */
|
|
private fun loadCycles(name: String): Map<Int, List<DoubleArray>> {
|
|
val out = HashMap<Int, MutableList<Pair<Int, DoubleArray>>>()
|
|
for (line in res(name).lineSequence()) {
|
|
val t = line.trim()
|
|
if (t.isEmpty()) continue
|
|
val f = t.split(",")
|
|
if (f.size < 3) continue
|
|
val cyc = f[0].toIntOrNull() ?: continue
|
|
val ch = f[1].toIntOrNull() ?: continue
|
|
val sig = DoubleArray(f.size - 2) { f[it + 2].trim().toDouble() }
|
|
out.getOrPut(cyc) { ArrayList() }.add(ch to sig)
|
|
}
|
|
return out.mapValues { (_, v) ->
|
|
val byCh = v.toMap()
|
|
(0..5).map { byCh[it] ?: DoubleArray(0) }
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun dump() {
|
|
// `PiezoHW.activePreset` 은 전역 가변 상태다. 다른 테스트 클래스가 먼저 돌면서
|
|
// 프리셋을 바꿔 두면 이 덤프가 통째로 달라진다(실제로 한 번 착시를 만들었다).
|
|
// 기본값으로 못 박아 실행 순서와 무관하게 만든다.
|
|
PiezoHW.activePreset = PiezoHW.DevicePreset.V0
|
|
val sb = StringBuilder()
|
|
for (cm in 0..1) {
|
|
val cycles = loadCycles("align_cm$cm.csv")
|
|
for (cyc in cycles.keys.sorted()) {
|
|
val sig = cycles[cyc]!!
|
|
for (cross in listOf(false, true)) {
|
|
val dets = MethodDRunner.detectMultichannel(sig, applyCross = cross)
|
|
val walls = dets.map { r ->
|
|
r?.let { WallWithSpan(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) }
|
|
}
|
|
val bv = estimateBv(walls)
|
|
sb.append("cm$cm cyc$cyc cross=$cross")
|
|
dets.forEachIndexed { i, r ->
|
|
sb.append(if (r == null) " CH$i=-" else
|
|
" CH$i=%d/%d/%.4f/%.4f/%d/%d".format(
|
|
r.ant, r.post, r.antRefined, r.postRefined, r.lowStart, r.lowEnd))
|
|
}
|
|
sb.append(if (bv == null) " BV=-" else
|
|
" BV=%.6f lr=%.6f nch=%d topH=%.4f botH=%.4f vT=%.4f vB=%.4f".format(
|
|
bv.volumeMl, bv.lrRatio, bv.validChannels.size,
|
|
bv.topHMm, bv.bottomHMm, bv.vTopMm3, bv.vBottomMm3))
|
|
sb.append('\n')
|
|
}
|
|
}
|
|
}
|
|
val out = File(System.getProperty("legacyDump") ?: "build/legacy_dump.txt")
|
|
out.parentFile?.mkdirs()
|
|
out.writeText(sb.toString())
|
|
println("[dump] ${out.absolutePath} lines=${sb.lines().size}")
|
|
}
|
|
}
|