diff --git a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt index 36600eb..72413c7 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt @@ -584,6 +584,29 @@ const val ELLIPSE_CAP_R_MAX = 0.92 /** Python `volume/config_vol.TOP_CAP_EDGE_K` — 최상단 채널이 최광폭일 때 top cap 제한. */ const val TOP_CAP_EDGE_K = 0.6 +/** + * Python `runners.CAP_D_ADAPTIVE_SILICON` = ("aspect", 80.0, 2.0). + * + * **실리콘(r*) 기기에서만** 켜지는 큰 방광 cap 억제. 레퍼런스가 `cap_d_adaptive="auto"` + * 로 hw 버전을 보고 자동 결정한다 — abs(v*)에는 적용하지 않는다. + */ +const val CAP_D_ADAPTIVE_D0 = 80.0 +const val CAP_D_ADAPTIVE_BETA = 2.0 + +/** + * cap 높이를 aspect-ratio 상한으로 억제 — `h · min(1, (D0/D)^β)`, `D = 2h`. + * + * cap 이 지름 80mm(= 높이 40mm)를 넘어서면 그때부터 눌러 준다. 큰 방광에서 끝단 + * 외삽이 과대해지는 것을 막는 장치다. [enabled] 가 false 면 아무 것도 하지 않는다. + */ +internal fun applyCapDAdaptive(h: Double, enabled: Boolean): Double { + if (!enabled || h <= 0.0) return h + val d = 2.0 * h + return if (d > CAP_D_ADAPTIVE_D0) + h * minOf(1.0, Math.pow(CAP_D_ADAPTIVE_D0 / d, CAP_D_ADAPTIVE_BETA)) + else h +} + /** Python `runners.CAP_AXIS_RATIO_MAX / CAP_AXIS_DMAX_MAX` — cap 축 붕괴 자기진단. */ const val CAP_AXIS_RATIO_MAX = 0.85 const val CAP_AXIS_DMAX_MAX = 100.0 @@ -1162,6 +1185,15 @@ fun estimateBladderVolume( topKind = "shrink cone" } else { // fallback (ellipse fit 실패): 기존 hemisphere 공식. + // + // 이 분기에서만 D-adaptive cap 억제가 걸린다(Python `_bv_core` 의 else 분기와 + // 같은 자리). shrink 경로는 이미 R_eff 로 높이가 구속되므로 적용하지 않는다. + val capDAdaptive = PiezoHW.activePreset.let { + it == PiezoHW.DevicePreset.R100 || it == PiezoHW.DevicePreset.R200 || + it == PiezoHW.DevicePreset.R300 + } + hCapBot = applyCapDAdaptive(hCapBot, capDAdaptive) + hCapTop = applyCapDAdaptive(hCapTop, capDAdaptive) vBottom = sS[0] * hCapBot / 2.0 + Math.PI * hCapBot * hCapBot * hCapBot / 6.0 vTop = sS[n - 1] * hCapTop / 3.0 bottomKind = "$capKind sphere" diff --git a/app/src/test/java/com/medithings/vesiscan/managers/CapDAdaptiveTest.kt b/app/src/test/java/com/medithings/vesiscan/managers/CapDAdaptiveTest.kt new file mode 100644 index 0000000..fe1598d --- /dev/null +++ b/app/src/test/java/com/medithings/vesiscan/managers/CapDAdaptiveTest.kt @@ -0,0 +1,34 @@ +package com.medithings.vesiscan.managers + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 실리콘 전용 cap 억제 `_apply_cap_d_adaptive` 대조. + * + * 기대값은 Python `_apply_cap_d_adaptive(h, CAP_D_ADAPTIVE_SILICON)` 을 그대로 돌려 뽑았다. + * 규칙: `D = 2h` 가 D0(80) 을 넘을 때만 `h · (D0/D)^2` 로 누른다. + * + * 이 함수는 **실리콘(r*) 기기에서만** 켜진다. abs(v*) 에 켜면 BV 가 근거 없이 줄어든다. + */ +class CapDAdaptiveTest { + + @Test + fun matchesPythonReference() { + val cases = listOf( + 0.0 to 0.0, + 10.0 to 10.0, + 39.9 to 39.9, + 40.0 to 40.0, // D=80 = D0 — 경계는 억제하지 않는다(D > D0 만) + 40.1 to 39.9002493766, + 50.0 to 32.0, + 60.0 to 26.6666666667, + 80.0 to 20.0, + ) + for ((h, want) in cases) { + assertEquals("h=$h (실리콘)", want, applyCapDAdaptive(h, true), 1e-9) + // 꺼져 있으면 항등이어야 한다 — abs 기기에서 값이 변하면 안 된다. + assertEquals("h=$h (abs)", h, applyCapDAdaptive(h, false), 1e-12) + } + } +}