Files
VesiscanClinicalAndroid/app/src/main/java/com/medithings/vesiscan/managers/AlgoMode.kt
T
dw.jang e17391ca9f 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>
2026-09-09 10:14:52 +09:00

79 lines
4.3 KiB
Kotlin

/*
* 알고리즘 경로 전환 — 기존(demo-final 현행) vs 레퍼런스(piezo-phantom-test 1:1).
*/
package com.medithings.vesiscan.managers
/**
* 검출·BV 알고리즘을 어느 쪽으로 돌릴지.
*
* ## 왜 두 경로를 유지하나
* demo-final 은 팬텀 시연과 임상 계측이 함께 도는 브랜치다. 여기서 "기존"이란
* **오늘 임상이 실제로 돌아간 코드**(커밋 `ebddc52`·`20a48ad` 시점의 검출·BV)를 말한다.
* 그 위에 신 저장소가 추가로 맞춘 레퍼런스 단계들은 값을 눈에 띄게 바꾸므로, 시연·계측이
* 모르는 사이 다른 알고리즘으로 도는 일이 없도록 **기본은 기존 경로**로 두고 레퍼런스
* 경로는 dev 패널에서 명시적으로 켤 때만 쓴다.
*
* ## 무엇이 갈리나
* 켜면 아래가 전부 레퍼런스 동작으로 바뀐다. 부분 적용은 없다 — 단계들이 서로를
* 전제하므로 섞으면 어느 쪽도 아닌 값이 나온다.
*
* · 교차채널 보정 3단계 → **7단계**
* (①.9 envelope · ① ant_tiebreak · ①.7 Λ-dip · ①.8 edge ·
* ①.95 lumen_return · ② NTV[Rule A 만] · ③ fp_gate,
* 기존의 `inward_post` 는 빠진다)
* · 파이프라인 `scale_recovery`(2) · `missing_outside` 증거 산출(4) ·
* `channel_confidence`→untrusted(5b) · `CONF_DEMOTE`(6)
* · 좌표계 교차채널 z 를 프리셋(실리콘 1.897/7.651) 대신 **전역 캘리브레이션**
* (1.936/6.85)으로 잰다 — 레퍼런스 `cross_channel._z` 가 그렇다.
* abs 는 값이 같아 표가 안 나고 실리콘에서만 갈린다.
* · nch=2 clamp `_sibeam_cap_bounds` — 채널이 2개뿐일 때 빔 기하로 cap 상한
* · 미러 · F83 `mirror_bottom_cap` + 적도-밖 bottom cap 연속 보간(실리콘·supine)
* · BV 극 cap 의 상한/하한을 관측 증거로 가르는 `si_floor`/`missing_outside`,
* bottom cap 제한 신설, 중앙 채널 보간 은행가 반올림,
* `adaptive_large_bladder_relax` 제거(cap 축 폴백을 가로막고 있었다)
*
* 검출 파라미터(`d_max` 15 · `inward_walk_win` 4)와 prominence 기준(`ANT_FLOOR_REF`),
* cap 축 폴백 · `ELLIPSE_CAP_R_MAX` · `TOP_CAP_EDGE_K` · `cap_d_adaptive`, 프리셋 유도
* dps/delay 는 **양쪽 공통**이다 — `ebddc52`/`20a48ad` 에서 이미 기존 경로에 들어갔고
* 오늘 임상이 그 위에서 돌았으므로 여기서 되돌리지 않는다.
*
* ## 상태 보관
* 프로세스 수명 동안만 유지한다(런타임 토글). 앱을 다시 켜면 기존 경로로 돌아간다.
*/
object AlgoMode {
/** true = 레퍼런스 1:1 경로. 기본은 false(기존 경로). */
@Volatile
var reference: Boolean = false
/** 화면에 표시할 이름. */
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 }
}
}