feat(clinical): 병원 임상만 최종 알고리즘으로 · 전역은 건드리지 않는다
병원 임상 화면은 최종 보고본(레퍼런스)이 기본이어야 하는데, AlgoMode.reference 는
프로세스 전역이라 그냥 켜면 **일반 측정 화면의 BV 까지** 바뀐다. 그쪽은 현행 임상에서
쓰이고 있어 임의로 건드리면 안 된다.
AlgoMode.withReference(ref) { } 범위 실행을 두고 ClinicalBv 가 그 안에서만 돈다.
계산이 끝나면 전역값은 원래대로다 — 임상 화면을 다녀왔다는 이유로 다른 화면의 값이
달라지지 않는다. 되돌리는지도 테스트로 고정했다. 임상 측정은 코루틴에서 돌아 두 계산이
겹칠 수 있으므로 동기화한다: 겹치면 한쪽의 복원이 다른 쪽의 설정을 지워 엉뚱한 경로로
계산된 값이 나오는데, 그 한 건이 검증 기록에 섞이면 나중에 찾아낼 방법이 없다.
ClinicalBv.compute(reference = true) 가 기본. BV 측정 화면의 스위치는 이제 전역을
쓰지 않고 그 화면의 계산에만 적용된다(두 경로 비교용).
**앞 커밋(21b0fca)의 숫자를 정정한다.** "align_cm1 기존 125.52 vs 레퍼런스 122.25 mL"
는 잘못된 측정이었다 — PiezoHW.activePreset 을 안 잡고 돌려 dps·delay 가 다른 프리셋
값으로 계산됐다(실측 자원은 VBT26050202=V1). 프리셋을 고정하면 이 데이터에서는 두
경로가 **같은 값**을 낸다(cm0 410.65 / cm1 490.63, 양쪽 동일).
두 경로가 같다는 뜻은 아니다. AlgoModeSwitchTest 실측으로 **cycle 44개 중 38개**의
검출·BV 가 갈린다. 여러 cycle 을 평균한 mean-scan 에서 차이가 묻힌 것이다. 즉 입력에
따라 같기도 다르기도 하므로, 값에 경로 표시를 붙이는 이유는 그대로 유효하다.
같은 함정을 테스트에도 반영했다 — ClinicalBvTest·AlgoPathReportTest 가 프리셋을 V1 로
고정한다. 다른 테스트가 남긴 전역 프리셋에 끌려가면 값이 통째로 흔들린다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,5 +47,32 @@ object AlgoMode {
|
|||||||
var reference: Boolean = false
|
var reference: Boolean = false
|
||||||
|
|
||||||
/** 화면에 표시할 이름. */
|
/** 화면에 표시할 이름. */
|
||||||
val label: String get() = if (reference) "레퍼런스 (Python 1:1)" else "기존 (현행 임상)"
|
val label: String get() = labelOf(reference)
|
||||||
|
|
||||||
|
/** 전역 상태가 아니라 **그 계산에 실제로 쓰인** 값의 이름. */
|
||||||
|
fun labelOf(ref: Boolean): String =
|
||||||
|
if (ref) "레퍼런스 (Python 1:1)" else "기존 (현행 임상)"
|
||||||
|
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [ref] 경로로 [block] 을 실행하고 **원래 값으로 되돌린다.**
|
||||||
|
*
|
||||||
|
* ## 왜 전역 토글을 그냥 켜지 않나
|
||||||
|
* [reference] 는 프로세스 전역이라, 병원 임상 화면이 켜면 **일반 측정 화면의 BV 까지**
|
||||||
|
* 바뀐다. 그쪽은 현행 임상에서 쓰이고 있어 임의로 바꾸면 안 된다.
|
||||||
|
*
|
||||||
|
* 그래서 임상 경로([ClinicalBv])만 이 범위 안에서 돌린다. 계산이 끝나면 전역값은
|
||||||
|
* 원래대로다 — 임상 화면을 다녀왔다는 이유로 다른 화면의 값이 달라지지 않는다.
|
||||||
|
*
|
||||||
|
* ## 동기화하는 이유
|
||||||
|
* 임상 화면의 측정은 코루틴에서 돈다. 두 계산이 겹치면 한쪽의 복원이 다른 쪽의
|
||||||
|
* 설정을 지워 **엉뚱한 경로로 계산된 값**이 나온다. 겹칠 일이 드물어도, 그때 나온
|
||||||
|
* 한 건이 검증 기록에 섞이면 나중에 찾아낼 방법이 없다.
|
||||||
|
*/
|
||||||
|
fun <T> withReference(ref: Boolean, block: () -> T): T = synchronized(lock) {
|
||||||
|
val prev = reference
|
||||||
|
reference = ref
|
||||||
|
try { block() } finally { reference = prev }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,13 @@ object ClinicalBv {
|
|||||||
/**
|
/**
|
||||||
* 이 값을 낸 알고리즘 경로. **숫자와 함께 반드시 표시한다.**
|
* 이 값을 낸 알고리즘 경로. **숫자와 함께 반드시 표시한다.**
|
||||||
*
|
*
|
||||||
* 두 경로가 같은 입력에 다른 값을 낸다(실측 align_cm1: 기존 125.52 mL vs
|
* 두 경로는 같은 신호에 다른 값을 낼 수 있다 — `AlgoModeSwitchTest` 실측에서
|
||||||
* 레퍼런스 122.25 mL). 어느 쪽에서 나온 값인지 모르면 "정렬 위치의 BV 가
|
* **cycle 44개 중 38개**의 검출·BV 가 갈렸다. 다만 여러 cycle 을 평균한
|
||||||
* 타당한가"를 판단할 수 없다 — 검증 자체가 성립하지 않는다.
|
* mean-scan 에서는 차이가 묻히기도 한다(align_cm0·cm1 은 두 경로가 동일).
|
||||||
|
*
|
||||||
|
* 즉 **입력에 따라 같기도 다르기도 하다.** 그래서 값만 남기면 나중에 해석할 수
|
||||||
|
* 없다 — 어느 경로에서 나온 BV 인지 모르면 "정렬 위치가 타당한가"의 근거가
|
||||||
|
* 되지 못한다.
|
||||||
*/
|
*/
|
||||||
val algoLabel: String,
|
val algoLabel: String,
|
||||||
/** 파형 마커용 (전벽, 후벽) 인덱스. 미검출 채널은 null. */
|
/** 파형 마커용 (전벽, 후벽) 인덱스. 미검출 채널은 null. */
|
||||||
@@ -76,8 +80,26 @@ object ClinicalBv {
|
|||||||
signals: List<DoubleArray>,
|
signals: List<DoubleArray>,
|
||||||
params: MethodDParams = MethodDParams.DEFAULT,
|
params: MethodDParams = MethodDParams.DEFAULT,
|
||||||
supine: Boolean = false,
|
supine: Boolean = false,
|
||||||
|
/**
|
||||||
|
* 병원 임상은 **최종 보고본(레퍼런스)** 이 기본이다.
|
||||||
|
*
|
||||||
|
* 전역 [AlgoMode.reference] 를 켜는 대신 [AlgoMode.withReference] 범위 안에서만
|
||||||
|
* 돌린다 — 전역을 켜면 일반 측정 화면의 BV 까지 바뀌는데, 그쪽은 현행 임상에서
|
||||||
|
* 쓰이고 있어 임의로 건드리면 안 된다.
|
||||||
|
*
|
||||||
|
* false 는 비교용이다(두 경로가 얼마나 다른지 그 자리에서 보기 위함).
|
||||||
|
*/
|
||||||
|
reference: Boolean = true,
|
||||||
|
): Outcome = AlgoMode.withReference(reference) {
|
||||||
|
computeInScope(signals, params, supine, AlgoMode.labelOf(reference))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeInScope(
|
||||||
|
signals: List<DoubleArray>,
|
||||||
|
params: MethodDParams,
|
||||||
|
supine: Boolean,
|
||||||
|
algo: String,
|
||||||
): Outcome {
|
): Outcome {
|
||||||
val algo = AlgoMode.label
|
|
||||||
if (signals.size < PiezoHW.centerCh.size) {
|
if (signals.size < PiezoHW.centerCh.size) {
|
||||||
return Outcome(null, emptyList(), 0, "채널 부족 (${signals.size}/6)", algo, emptyList(), null)
|
return Outcome(null, emptyList(), 0, "채널 부족 (${signals.size}/6)", algo, emptyList(), null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.medithings.vesiscan.ble.BleManager
|
import com.medithings.vesiscan.ble.BleManager
|
||||||
import com.medithings.vesiscan.ble.PiezoChannelData
|
import com.medithings.vesiscan.ble.PiezoChannelData
|
||||||
import com.medithings.vesiscan.managers.AlgoMode
|
|
||||||
import com.medithings.vesiscan.managers.ClinicalBv
|
import com.medithings.vesiscan.managers.ClinicalBv
|
||||||
import com.medithings.vesiscan.models.HospitalFixedParams
|
import com.medithings.vesiscan.models.HospitalFixedParams
|
||||||
import com.medithings.vesiscan.models.ProbeCycle
|
import com.medithings.vesiscan.models.ProbeCycle
|
||||||
@@ -79,13 +78,10 @@ fun BvMeasureSection(
|
|||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var history by remember { mutableStateOf<List<Double>>(emptyList()) }
|
var history by remember { mutableStateOf<List<Double>>(emptyList()) }
|
||||||
|
|
||||||
// 검증 목적이 **최종 보고된 알고리즘**이라 이 화면에서는 레퍼런스 경로를 기본으로
|
// 병원 임상은 **최종 보고본이 기본**이다. 전역 AlgoMode 를 켜지 않는다 —
|
||||||
// 켠다. AlgoMode 는 전역이라 일반 측정 화면에도 영향을 주므로, 켠 사실을 화면에
|
// ClinicalBv 가 계산 동안만 그 경로로 돌리고 되돌리므로, 일반 측정 화면의 BV 는
|
||||||
// 드러내고 되돌릴 수 있게 둔다.
|
// 영향을 받지 않는다. 끄는 것은 두 경로를 그 자리에서 비교하기 위한 용도다.
|
||||||
var useReference by remember { mutableStateOf(AlgoMode.reference) }
|
var useReference by remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (!AlgoMode.reference) { AlgoMode.reference = true; useReference = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(mode) {
|
LaunchedEffect(mode) {
|
||||||
val m = mode ?: return@LaunchedEffect
|
val m = mode ?: return@LaunchedEffect
|
||||||
@@ -142,7 +138,8 @@ fun BvMeasureSection(
|
|||||||
}
|
}
|
||||||
if (!aborted && cycles.size == AVG_CYCLES) {
|
if (!aborted && cycles.size == AVG_CYCLES) {
|
||||||
error = null
|
error = null
|
||||||
val r = ClinicalBv.compute(ClinicalBv.meanScan(cycles), supine = supine)
|
val r = ClinicalBv.compute(
|
||||||
|
ClinicalBv.meanScan(cycles), supine = supine, reference = useReference)
|
||||||
outcome = r
|
outcome = r
|
||||||
r.volumeMl?.let { v ->
|
r.volumeMl?.let { v ->
|
||||||
history = (history + v).takeLast(HISTORY_MAX)
|
history = (history + v).takeLast(HISTORY_MAX)
|
||||||
@@ -173,7 +170,7 @@ fun BvMeasureSection(
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Switch(
|
Switch(
|
||||||
checked = useReference,
|
checked = useReference,
|
||||||
onCheckedChange = { useReference = it; AlgoMode.reference = it },
|
onCheckedChange = { useReference = it },
|
||||||
enabled = !running,
|
enabled = !running,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
@@ -183,7 +180,8 @@ fun BvMeasureSection(
|
|||||||
fontSize = 13.sp, fontWeight = FontWeight.SemiBold,
|
fontSize = 13.sp, fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"끄면 기존 경로로 계산합니다. 두 경로는 같은 신호에 다른 값을 낼 수 있습니다.",
|
"이 화면에만 적용됩니다. 끄면 기존 경로로 계산합니다 — " +
|
||||||
|
"두 경로는 같은 신호에 다른 값을 낼 수 있습니다.",
|
||||||
fontSize = 10.sp, color = MlSecondaryText,
|
fontSize = 10.sp, color = MlSecondaryText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.medithings.vesiscan.managers
|
package com.medithings.vesiscan.managers
|
||||||
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -8,20 +9,31 @@ import java.io.File
|
|||||||
* 두 알고리즘 경로가 같은 입력에 어떤 값을 내는지 **숫자로 남긴다**.
|
* 두 알고리즘 경로가 같은 입력에 어떤 값을 내는지 **숫자로 남긴다**.
|
||||||
*
|
*
|
||||||
* 검증 목적이 "정렬이 고른 위치의 BV 가 타당한가"인데, 그 BV 가 어느 경로에서 나온
|
* 검증 목적이 "정렬이 고른 위치의 BV 가 타당한가"인데, 그 BV 가 어느 경로에서 나온
|
||||||
* 값인지 모르면 판단이 성립하지 않는다. [AlgoMode.reference] 는 기본이 **false**라
|
* 값인지 모르면 판단이 성립하지 않는다. 병원 임상 경로([ClinicalBv])는 최종 보고본
|
||||||
* 앱은 기존 경로로 돈다 — 이 시험은 그 차이를 실측 데이터로 찍어 보고서에 쓴다.
|
* (레퍼런스)이 기본이고, 일반 측정 화면은 기존 경로 그대로다 — 이 시험은 그 차이를
|
||||||
|
* 실측 데이터로 찍어 보고서에 쓴다.
|
||||||
*
|
*
|
||||||
* 통과·실패를 가리는 시험이 아니다(단정문이 없다). 값을 출력하는 것이 목적이라
|
* 통과·실패를 가리는 시험이 아니다(단정문이 없다). 값을 출력하는 것이 목적이라
|
||||||
* 리포트에 그대로 옮길 수 있다.
|
* 리포트에 그대로 옮길 수 있다.
|
||||||
*/
|
*/
|
||||||
class AlgoPathReportTest {
|
class AlgoPathReportTest {
|
||||||
|
|
||||||
@After fun restore() { AlgoMode.reference = false }
|
// 실측 자원은 VBT26050202(=V1) 로 받은 것이다. 프리셋이 다르면 dps·delay 가 달라져
|
||||||
|
// 같은 sample index 가 다른 깊이로 매핑된다 — 프리셋을 안 잡으면 Python 과 어긋난
|
||||||
|
// 값이 나오고, 그걸 "불일치"로 오해하게 된다(AnchorMeasureParityTest 와 같은 이유).
|
||||||
|
private val savedPreset = PiezoHW.activePreset
|
||||||
|
|
||||||
private fun scan(path: String): List<DoubleArray>? {
|
@Before fun setUp() { PiezoHW.activePreset = PiezoHW.DevicePreset.V1 }
|
||||||
|
|
||||||
|
@After fun restore() {
|
||||||
|
AlgoMode.reference = false
|
||||||
|
PiezoHW.activePreset = savedPreset
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `cycle,ch,samples…` 를 cycle 목록으로. */
|
||||||
|
private fun cyclesOf(path: String): List<List<DoubleArray>> {
|
||||||
val f = File(path)
|
val f = File(path)
|
||||||
if (!f.exists()) return null
|
if (!f.exists()) return emptyList()
|
||||||
// cycle,ch,samples… — 전 cycle 을 채널별 평균(mean-scan)해 정렬과 같은 입력으로.
|
|
||||||
val byCycle = linkedMapOf<Int, MutableMap<Int, DoubleArray>>()
|
val byCycle = linkedMapOf<Int, MutableMap<Int, DoubleArray>>()
|
||||||
f.forEachLine { line ->
|
f.forEachLine { line ->
|
||||||
val p = line.trim().split(',')
|
val p = line.trim().split(',')
|
||||||
@@ -31,32 +43,61 @@ class AlgoPathReportTest {
|
|||||||
byCycle.getOrPut(ci) { linkedMapOf() }[ch] =
|
byCycle.getOrPut(ci) { linkedMapOf() }[ch] =
|
||||||
DoubleArray(p.size - 2) { p[it + 2].toDoubleOrNull() ?: 0.0 }
|
DoubleArray(p.size - 2) { p[it + 2].toDoubleOrNull() ?: 0.0 }
|
||||||
}
|
}
|
||||||
val cycles = byCycle.values.filter { it.size >= 6 }.map { m -> (0..5).map { m.getValue(it) } }
|
return byCycle.values.filter { it.size >= 6 }.map { m -> (0..5).map { m.getValue(it) } }
|
||||||
return if (cycles.isEmpty()) null else ClinicalBv.meanScan(cycles)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun report(label: String, sig: List<DoubleArray>) {
|
|
||||||
val out = StringBuilder("\n=== $label ===\n")
|
|
||||||
for (ref in listOf(false, true)) {
|
|
||||||
AlgoMode.reference = ref
|
|
||||||
val r = ClinicalBv.compute(sig)
|
|
||||||
out.append(" %-22s BV=%s 검출=%d/4 %s\n".format(
|
|
||||||
if (ref) "레퍼런스(Python 1:1)" else "기존(현행 임상)",
|
|
||||||
r.volumeMl?.let { "%.2f mL".format(it) } ?: "계산불가",
|
|
||||||
r.detectedCenter,
|
|
||||||
r.walls.filterNotNull().joinToString(" ") { "CH${it.channel}(${it.ant}~${it.post})" },
|
|
||||||
))
|
|
||||||
}
|
|
||||||
println(out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `두 경로의 BV 를 실측 데이터로 비교`() {
|
@Test fun `두 경로의 BV 를 실측 데이터로 비교`() {
|
||||||
var any = false
|
|
||||||
for (name in listOf("align_cm0.csv", "align_cm1.csv")) {
|
for (name in listOf("align_cm0.csv", "align_cm1.csv")) {
|
||||||
val sig = scan("src/test/resources/$name") ?: continue
|
val cycles = cyclesOf("src/test/resources/$name")
|
||||||
any = true
|
if (cycles.isEmpty()) continue
|
||||||
report(name, sig)
|
val sig = ClinicalBv.meanScan(cycles)
|
||||||
|
val sb = StringBuilder("\n=== BV · $name ===\n")
|
||||||
|
for (ref in listOf(false, true)) {
|
||||||
|
// ClinicalBv 가 범위 안에서 경로를 잡으므로 전역을 건드리지 않는다.
|
||||||
|
val r = ClinicalBv.compute(sig, reference = ref)
|
||||||
|
sb.append(
|
||||||
|
" %-22s BV=%-12s 검출=%d/4 %s\n".format(
|
||||||
|
if (ref) "레퍼런스(최종)" else "기존(현행)",
|
||||||
|
r.volumeMl?.let { "%.2f mL".format(it) } ?: "계산불가",
|
||||||
|
r.detectedCenter,
|
||||||
|
r.walls.filterNotNull()
|
||||||
|
.joinToString(" ") { "CH${it.channel}(${it.ant}~${it.post})" },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
println(sb)
|
||||||
|
}
|
||||||
|
// 범위 실행이 전역을 되돌렸는지 — 되돌리지 않으면 일반 측정 화면이 오염된다.
|
||||||
|
println("\n전역 AlgoMode.reference = ${AlgoMode.reference} (false 여야 한다)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `정렬 지표를 두 경로로 비교`() {
|
||||||
|
val expect = File("src/test/resources/align_expect.tsv")
|
||||||
|
if (!expect.exists()) { println("\n(기대값 파일 없음)"); return }
|
||||||
|
val want = expect.readLines()
|
||||||
|
.filter { it.isNotBlank() && !it.startsWith("#") }
|
||||||
|
.associate { l -> l.split("\t").let { it[0].trim().toInt() to it } }
|
||||||
|
|
||||||
|
for (ref in listOf(false, true)) {
|
||||||
|
AlgoMode.reference = ref
|
||||||
|
val sb = StringBuilder(
|
||||||
|
"\n=== AnchorGuide · ${if (ref) "레퍼런스(최종)" else "기존(현행)"} ===\n")
|
||||||
|
for (cm in 0..1) {
|
||||||
|
val cycles = cyclesOf("src/test/resources/align_cm$cm.csv")
|
||||||
|
if (cycles.isEmpty()) continue
|
||||||
|
val rec = AnchorGuide().measure(cycles, cm)
|
||||||
|
val w = want[cm]
|
||||||
|
sb.append(
|
||||||
|
" cm=%d nch=%d(py %s) ch3=%s(py %s) rate=%.10f(py %s) cap=%.10f(py %s)\n"
|
||||||
|
.format(
|
||||||
|
cm, rec.nch, w?.getOrNull(2),
|
||||||
|
if (rec.ch3Mean) "O" else "X", w?.getOrNull(3),
|
||||||
|
rec.ch3Rate, w?.getOrNull(4),
|
||||||
|
rec.capFrac, w?.getOrNull(5),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
println(sb)
|
||||||
}
|
}
|
||||||
if (!any) println("\n(실측 자원 없음 — 비교 생략)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ import java.io.File
|
|||||||
*/
|
*/
|
||||||
class ClinicalBvTest {
|
class ClinicalBvTest {
|
||||||
|
|
||||||
|
// 실측 자원은 VBT26050202(=V1) 로 받은 것이다. 프리셋이 다르면 dps·delay 가 달라져
|
||||||
|
// 같은 sample index 가 다른 깊이로 매핑된다 — 다른 테스트가 남긴 전역 프리셋에
|
||||||
|
// 끌려가면 여기 값이 통째로 흔들린다.
|
||||||
|
private val savedPreset = PiezoHW.activePreset
|
||||||
|
|
||||||
|
@org.junit.Before fun pinPreset() { PiezoHW.activePreset = PiezoHW.DevicePreset.V1 }
|
||||||
|
|
||||||
|
@org.junit.After fun restorePreset() { PiezoHW.activePreset = savedPreset }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 정렬 검증용 실측 trace (`cycle,ch,samples…`). 파일이 없으면 **실패시킨다** —
|
* 정렬 검증용 실측 trace (`cycle,ch,samples…`). 파일이 없으면 **실패시킨다** —
|
||||||
* 조용히 건너뛰면 이 클래스에서 제일 중요한 parity 검사가 통과한 것처럼 보인다.
|
* 조용히 건너뛰면 이 클래스에서 제일 중요한 parity 검사가 통과한 것처럼 보인다.
|
||||||
@@ -46,34 +55,52 @@ class ClinicalBvTest {
|
|||||||
|
|
||||||
private fun flat(n: Int = 120) = List(6) { DoubleArray(n) { 1000.0 } }
|
private fun flat(n: Int = 120) = List(6) { DoubleArray(n) { 1000.0 } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 경로 모두에서 대조한다.
|
||||||
|
*
|
||||||
|
* [ClinicalBv.compute] 는 병원 임상용이라 **레퍼런스가 기본**이다. 기대값을 기존
|
||||||
|
* 경로로 만들어 비교하면 갈린다 — 실제로 갈린다(AlgoModeSwitchTest: cycle 44개 중
|
||||||
|
* 38개). 그건 버그가 아니라 두 경로가 다르다는 사실이므로, 시험은 **같은 경로끼리**
|
||||||
|
* 맞춰 비교해야 의미가 있다.
|
||||||
|
*/
|
||||||
@Test fun `AnchorGuide 의 cap_frac 과 같은 경로로 계산한다`() {
|
@Test fun `AnchorGuide 의 cap_frac 과 같은 경로로 계산한다`() {
|
||||||
val scan = realScan()
|
val scan = realScan()
|
||||||
|
for (ref in listOf(false, true)) {
|
||||||
// AnchorGuide.measure 가 하는 것과 **같은 호출**을 손으로 재현.
|
// AnchorGuide.measure 가 하는 것과 **같은 호출**을 손으로 재현.
|
||||||
val outcome = MethodDRunner.detectMultichannelWithEvidence(
|
val expected = AlgoMode.withReference(ref) {
|
||||||
scan, MethodDParams.DEFAULT, applyTgc = true, applyCross = true)
|
val outcome = MethodDRunner.detectMultichannelWithEvidence(
|
||||||
val spans = outcome.walls.map { r ->
|
scan, MethodDParams.DEFAULT, applyTgc = true, applyCross = true)
|
||||||
r?.let { WallWithSpan(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) }
|
val spans = outcome.walls.map { r ->
|
||||||
|
r?.let { WallWithSpan(it.antRefined, it.postRefined, it.lowStart, it.lowEnd) }
|
||||||
|
}
|
||||||
|
estimateBv(spans, missingOutside = outcome.missingOutside)
|
||||||
|
}
|
||||||
|
val got = ClinicalBv.compute(scan, reference = ref)
|
||||||
|
if (expected == null) {
|
||||||
|
assertNull("ref=$ref 레퍼런스 경로가 null 인데 값을 냈다", got.volumeMl)
|
||||||
|
} else {
|
||||||
|
assertNotNull("ref=$ref 레퍼런스 경로는 값을 냈는데 null", got.volumeMl)
|
||||||
|
assertEquals("ref=$ref volumeMl", expected.volumeMl, got.volumeMl!!, 1e-9)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val expected = estimateBv(spans, missingOutside = outcome.missingOutside)
|
}
|
||||||
|
|
||||||
val got = ClinicalBv.compute(scan)
|
@Test fun `범위 실행이 전역 상태를 되돌린다`() {
|
||||||
|
// 되돌리지 않으면 임상 화면을 다녀왔다는 이유로 **일반 측정 화면의 BV** 가 바뀐다.
|
||||||
if (expected == null) {
|
val before = AlgoMode.reference
|
||||||
assertNull("레퍼런스 경로가 null 인데 ClinicalBv 가 값을 냈다", got.volumeMl)
|
ClinicalBv.compute(realScan(), reference = true)
|
||||||
} else {
|
assertEquals("compute 뒤 전역값이 바뀌었다", before, AlgoMode.reference)
|
||||||
assertNotNull("레퍼런스 경로는 값을 냈는데 ClinicalBv 가 null", got.volumeMl)
|
|
||||||
assertEquals(expected.volumeMl, got.volumeMl!!, 1e-9)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `마커는 검출 인덱스를 그대로 준다`() {
|
@Test fun `마커는 검출 인덱스를 그대로 준다`() {
|
||||||
// 파형 위 세로선이 실제 검출 위치와 어긋나면, 눈으로 보는 것과 계산이 달라져
|
// 파형 위 세로선이 실제 검출 위치와 어긋나면, 눈으로 보는 것과 계산이 달라져
|
||||||
// 원인 분석이 성립하지 않는다.
|
// 원인 분석이 성립하지 않는다.
|
||||||
val scan = realScan()
|
val scan = realScan()
|
||||||
val outcome = MethodDRunner.detectMultichannelWithEvidence(
|
val outcome = AlgoMode.withReference(true) {
|
||||||
scan, MethodDParams.DEFAULT, applyTgc = true, applyCross = true)
|
MethodDRunner.detectMultichannelWithEvidence(
|
||||||
val got = ClinicalBv.compute(scan)
|
scan, MethodDParams.DEFAULT, applyTgc = true, applyCross = true)
|
||||||
|
}
|
||||||
|
val got = ClinicalBv.compute(scan) // 기본 = 레퍼런스
|
||||||
|
|
||||||
assertEquals(outcome.walls.size, got.markers.size)
|
assertEquals(outcome.walls.size, got.markers.size)
|
||||||
outcome.walls.forEachIndexed { i, r ->
|
outcome.walls.forEachIndexed { i, r ->
|
||||||
|
|||||||
Reference in New Issue
Block a user