test(align): labdb 원시 파형을 앱 판정에 다시 넣는 진단 덤프 — 시도별 nch·CH3 trace 수

요약은 같은 cm 의 마지막 시도만 남겨(재측정은 최신값) 첫 측정 때 본 CH3 가 몇 trace 였는지
알 수 없다. LABDB_CYCLES_DIR 의 cycle CSV(scratch 스크립트가 labdb 601 JSON 에서 뽑는다)를
AnchorGuide.measure 에 넣어 파일마다 nch · ch3Mean · hit/traces · cap_frac 을 찍는다. 환경변수가
없으면 건너뛴다 — CI 와 무관. 2026-10-14 P9 sitting "1·2cm 에서 ch2·ch3" 질의에 처음 썼다.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 10:14:19 +09:00
parent 0a93aa1ab3
commit 8eee3550f3
@@ -0,0 +1,54 @@
package com.medithings.vesiscan.managers
import org.junit.Assume.assumeTrue
import org.junit.Test
import java.io.File
/**
* 진단용 — labdb 601 원시 파형을 앱과 **똑같은 판정**([AnchorGuide.measure])에 다시 넣어
* 시도별 nch · CH3 · 검출 trace 수를 찍는다. 요약(`positions`)은 같은 cm 의 **마지막 시도**만
* 남기므로(재측정은 최신값), 첫 측정 때 화면에서 본 CH3 가 실제로 몇 trace 였는지는 이걸로만
* 알 수 있다(2026-10-14, P9 sitting "1·2cm 에서 ch2·ch3 보였다" 질의).
*
* 입력: 환경변수 `LABDB_CYCLES_DIR` 의 `*.csv` (cycle,channel,s0.. · 헤더 없음 —
* scratchpad/labdb_export_cycles.py 가 만든다). 파일명 `…__{cm}cm_{phase}_a{attempt}.csv`.
* HW 프리셋은 `LABDB_HW_PRESET`(기본 R300). 환경변수가 없으면 건너뛴다 — CI 와 무관하다.
*/
class LabdbTraceDumpTest {
private fun loadCycles(f: File): List<List<DoubleArray>> {
val byCycle = sortedMapOf<Int, MutableMap<Int, DoubleArray>>()
f.readLines().filter { it.isNotBlank() }.forEach { line ->
val p = line.split(',')
byCycle.getOrPut(p[0].toInt()) { sortedMapOf() }[p[1].toInt()] =
DoubleArray(p.size - 2) { p[it + 2].toDouble() }
}
return byCycle.values.map { chans -> chans.toSortedMap().values.toList() }
}
@Test
fun dumpPerAttemptTraces() {
val dir = System.getenv("LABDB_CYCLES_DIR")?.let(::File)
assumeTrue("LABDB_CYCLES_DIR 없음 — 진단 전용", dir != null && dir.isDirectory)
val presetName = System.getenv("LABDB_HW_PRESET") ?: "R300"
val saved = PiezoHW.activePreset
PiezoHW.activePreset = PiezoHW.DevicePreset.valueOf(presetName)
try {
val re = Regex("""__(-?\d+)cm_([a-z_]+)_a(\d+)\.csv$""")
println("TRACEDUMP preset=$presetName")
println("TRACEDUMP file | cycles | traces | nch | ch3Mean | ch3Hit/traces | ch3Rate | capFrac | nchTraceMean")
dir!!.listFiles { f -> f.name.endsWith(".csv") }!!.sortedBy { it.name }.forEach { f ->
val m = re.find(f.name) ?: return@forEach
val cm = m.groupValues[1].toInt()
val cycles = loadCycles(f)
val rec = AnchorGuide().measure(cycles, cm)
println(
"TRACEDUMP ${f.name} | ${cycles.size} | ${rec.nTrace} | ${rec.nch} | ${if (rec.ch3Mean) "O" else "X"} | " +
"${rec.ch3Hit}/${rec.nTrace} | ${"%.2f".format(rec.ch3Rate)} | ${"%.3f".format(rec.capFrac)} | ${"%.2f".format(rec.nchTrace)}",
)
}
} finally {
PiezoHW.activePreset = saved
}
}
}