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.MethodDDetector import com.medithings.vesiscan.walldetect.algo.methodd.MethodDParams import com.medithings.vesiscan.walldetect.algo.methodd.MethodDPreprocessing import com.medithings.vesiscan.walldetect.algo.methodd.MethodDTgc import org.junit.Test import java.io.File /** * Python vs Kotlin bit-perfect 정밀 대조용 dump. * * cm=1 세션 첫 10 cycle 평균 (trace 0) 을 pipeline 각 단계에 흘리면서 결과를 JSON 으로 저장. * scratchpad/precision_python.json 과 비교해 divergence 지점 특정. */ class PrecisionDumpTest { private val session = "c:/Projects/medilightv2android/data123/dwjang_HUMAN-kai_VBT26050202_SUPINE_ALIGN_1CM_2026-07-06_164231.json" private val outPath = "C:/Users/장동우/AppData/Local/Temp/claude/c--Projects-medilightv2android/76311e3b-2bb5-4c52-ab8a-8d89cb1052c2/scratchpad/precision_kotlin.json" private fun loadFirstTrace(path: String, win: Int = 10): List { val root = JsonParser.parseReader(File(path).bufferedReader()).asJsonObject val recs = root.getAsJsonArray("records") val cycles = mutableListOf>() for (r in recs) { if (cycles.size >= win) 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]!! }) } // 첫 10 cycle 평균 = trace 0 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 } } } @Test fun `dump pipeline intermediates for cm=1 trace 0`() { PiezoHW.activePreset = PiezoHW.DevicePreset.V1 val trace = loadFirstTrace(session, 10) val params = MethodDParams.DEFAULT val nCh = trace.size val out = HashMap() out["input_signal"] = trace.map { it.toList() } // per-channel: heavy / light / TGC / detect val heavyList = trace.map { MethodDPreprocessing.preprocessHeavy(it, params) } val lightList = trace.map { MethodDPreprocessing.preprocessLight(it) } val heavyTgc = MethodDTgc.applyTgcPipeline(heavyList) val lightTgc = MethodDTgc.applyTgcPipeline(lightList) val perCh = mutableListOf>() for (ch in 0 until nCh) { val angle = PiezoHW.degreeAll[ch] val chRatio = params.otsuRatio * kotlin.math.cos(Math.toRadians(angle)) val r = MethodDDetector.detect( raw = trace[ch], denoisedHeavy = heavyTgc[ch], denoisedLight = lightTgc[ch], otsuRatioOverride = chRatio, params = params, ) val m = LinkedHashMap() m["ch"] = ch m["angle_deg"] = angle m["otsu_ratio_applied"] = chRatio m["heavy"] = heavyList[ch].take(20) m["light"] = lightList[ch].take(20) m["heavy_tgc"] = heavyTgc[ch].take(20) m["light_tgc"] = lightTgc[ch].take(20) m["ant"] = r?.ant m["post"] = r?.post // MethodDResult 의 refined 필드 이름 확인 필요 (Python: ant_refined / post_refined) m["ant_refined"] = r?.let { runCatching { it.javaClass.getDeclaredField("antRefined").also { f -> f.isAccessible = true }.get(it) }.getOrNull() } m["post_refined"] = r?.let { runCatching { it.javaClass.getDeclaredField("postRefined").also { f -> f.isAccessible = true }.get(it) }.getOrNull() } m["low_start"] = r?.lowStart m["low_end"] = r?.lowEnd m["low_amp"] = r?.lowAmp perCh.add(m) } out["per_channel"] = perCh // walls base (apply_cross=false) — Python 매칭 위해 refined 우선 val wallsBase = MethodDRunner.detectMultichannel(trace, params, applyCross = false) out["walls_base"] = wallsBase.map { r -> r?.let { listOf(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) } } // walls cross (apply_cross=true) val wallsCross = MethodDRunner.detectMultichannel(trace, params, applyCross = true) out["walls_cross"] = wallsCross.map { r -> r?.let { listOf(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) } } // BV — Python `extract_walls` 는 refined ant/post 우선 사용 → subsample 정밀도 유지 val allWalls: List?> = wallsCross.map { r -> r?.let { Pair(it.antRefined, it.postRefined) } } val bv = estimateBladderVolume6ch(allWalls) out["bv"] = bv?.let { val m = LinkedHashMap() m["volume_ml"] = it.volumeMl m["volume_mm3"] = it.volumeMm3 m["valid_channels"] = it.validChannels m["lr_ratio"] = it.lrRatio m["d_ant_mm"] = it.dAntMm.toList() m["d_post_mm"] = it.dPostMm.toList() m["D_mm"] = it.dMm.toList() m["bottom_h_mm"] = it.bottomHMm m["top_h_mm"] = it.topHMm m["V_bottom_mm3"] = it.vBottomMm3 m["V_top_mm3"] = it.vTopMm3 m } File(outPath).writeText(Gson().toJson(out)) println("saved: $outPath") println(" bv.volume_ml = ${bv?.volumeMl?.let { "%.4f".format(it) }}") for (i in 0 until nCh) { val r = perCh[i] println(" ch$i: ant=${r["ant"]} post=${r["post"]} angle=${r["angle_deg"]}") } } }