diff --git a/app/src/test/java/com/medithings/vesiscan/managers/GoldenDumpTest.kt b/app/src/test/java/com/medithings/vesiscan/managers/GoldenDumpTest.kt new file mode 100644 index 0000000..d94757b --- /dev/null +++ b/app/src/test/java/com/medithings/vesiscan/managers/GoldenDumpTest.kt @@ -0,0 +1,136 @@ +package com.medithings.vesiscan.managers + +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.medithings.vesiscan.walldetect.MethodDRunner +import com.medithings.vesiscan.walldetect.algo.methodd.MethodDParams +import org.junit.Test +import java.io.File + +/** + * Golden test — Kotlin. `estimateBv` pipeline 각 단계 중간값을 dump. + * 3 세션 (cm=0/1/3) × 11 trace = 33 cases 를 Python golden 과 대조. + * + * 실행: + * 1. Python golden 생성: python scratchpad/golden_dump_python.py + * 2. Kotlin dump: ./gradlew testDevDebugUnitTest --tests "*GoldenDumpTest*" + * 3. diff : python scratchpad/golden_diff.py + */ +class GoldenDumpTest { + + private val sessions = mapOf( + "cm0" to "c:/Projects/medilightv2android/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_0CM_2026-07-06_164150.json", + "cm1" to "c:/Projects/medilightv2android/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json", + "cm3" to "c:/Projects/medilightv2android/data123/dwjang_HUMAN-kai_VBT26040302_SUPINE_ALIGN_3CM_2026-07-06_164842.json", + ) + private val outPath = "C:/Users/장동우/AppData/Local/Temp/claude/c--Projects-medilightv2android/76311e3b-2bb5-4c52-ab8a-8d89cb1052c2/scratchpad/golden_kotlin.json" + + private fun loadCycles(path: String, maxN: Int = 20): List> { + val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject + val recs = root.getAsJsonArray("records") + val cycles = mutableListOf>() + for (r in recs) { + if (cycles.size >= maxN) break + val obj = r.asJsonObject + val chs = obj.getAsJsonArray("channels") ?: continue + if (chs.size() != 6) continue + val byCh = HashMap() + for (ce in chs) { + val co = ce.asJsonObject + val ch = co.get("ch").asInt + val data = co.getAsJsonArray("data") ?: continue + if (data.size() != 100) continue + byCh[ch] = DoubleArray(100) { data[it].asDouble } + } + if (byCh.size != 6) continue + cycles.add((0..5).map { byCh[it]!! }) + } + return cycles + } + + private fun meanScan(cycles: List>): List { + val nCh = cycles[0].size + return List(nCh) { ch -> + val len = cycles[0][ch].size + DoubleArray(len) { i -> + var s = 0.0 + for (c in cycles) s += c[ch][i] + s / cycles.size + } + } + } + + private fun dumpCase(sessionId: String, traceIdx: Int, trace: List): Map { + val params = MethodDParams.DEFAULT + val results = MethodDRunner.detectMultichannel(trace, params, applyCross = true) + + // Stage E — walls_cross (refined + span) + val wallsE = results.map { r -> + r?.let { + listOf(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) + } + } + + // Stage M — final BV + val wallsForBv = results.map { r -> + r?.let { WallWithSpan(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) } + } + val bv = estimateBv(wallsForBv) + + val bvMap: Map? = bv?.let { + LinkedHashMap().apply { + put("volume_ml", it.volumeMl) + put("volume_mm3", it.volumeMm3) + put("valid_channels", it.validChannels) + put("d_mm", it.dMm.toList()) + put("d_ant_mm", it.dAntMm.toList()) + put("d_post_mm", it.dPostMm.toList()) + put("sorted_channels", it.sortedChannels) + put("bottom_h_mm", it.bottomHMm) + put("top_h_mm", it.topHMm) + put("V_bottom_mm3", it.vBottomMm3) + put("V_top_mm3", it.vTopMm3) + put("V_core_mm3", it.vCoreMm3) + put("cap_fit_status", it.capFitStatus) + put("cap_fit_points", it.capFitPoints) + put("cap_b_si_mm", it.capBSiMm) + put("cap_c_ap_mm", it.capCApMm) + put("cap_y0_mm", it.capY0Mm) + put("cap_z0_mm", it.capZ0Mm) + put("cap_mean_residual", it.capMeanResidual) + put("lr_ratio", it.lrRatio) + } + } + + val stages = LinkedHashMap() + stages["E_walls_cross"] = wallsE + stages["M_bv"] = bvMap + + return mapOf( + "session_id" to sessionId, + "trace_idx" to traceIdx, + "stages" to stages, + ) + } + + @Test + fun `golden dump all sessions`() { + PiezoHW.activePreset = PiezoHW.DevicePreset.V1 + val cases = mutableListOf>() + val win = 10 + for ((sid, path) in sessions) { + val cycles = loadCycles(path, 20) + if (cycles.size < win) { + println(" skip $sid: only ${cycles.size} cycles") + continue + } + for (i in 0..cycles.size - win) { + val tr = meanScan(cycles.subList(i, i + win)) + cases.add(dumpCase(sid, i, tr)) + println(" dumped $sid trace $i") + } + } + File(outPath).writeText(Gson().toJson(mapOf("cases" to cases))) + println("\nsaved: $outPath (${cases.size} cases)") + } +}