Files
VesiscanClinicalAndroid/app/src/test/java/com/medithings/vesiscan/managers/LegacyEquivalenceDumpTest.kt
T
dw.jang 441f249410 feat(algo): 미이식 3건 마저 이식 — post_tie_lock · sibeam · F83
레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.

  · 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>
2026-09-03 18:09:26 +09:00

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}")
}
}