fix(align): CH3 전무 폴백으로 고른 자리에서는 좌우 정렬이 CH3 를 요구하지 않는다
2026-10-13 임상(e.yu): supine 에서 CH3 가 끝내 안 잡혀 폴백(nch 최대)으로 자리를 골랐는데, 좌우 단계가 "↑ 머리방향으로 이동 (ch3 미검출)" 만 내서 끝나지 않았다. 좌우 advisor 는 Python `alignment_advice` 와 1:1 로 CH3 없음 → 무조건 MOVE_UP 이다. 폴백 자리는 CH3 가 없는 걸 알고 고른 자리라 그 조건이 모순이다. - AlignmentAdvice.advise(..., requireCh3 = true): false 면 CH3 없이 CH4·CH5 균형만 본다. STOP 문구에 "ch3 없음(폴백 위치)"를 붙여 화면·기록에 남긴다. 기본값은 종전 그대로(Python 1:1). - LateralGuide.requireCh3 — 좌우를 시작할 때 정렬 화면이 전 위치 판정의 select_rule 을 보고 `no_ch3_max_nch` 면 끈다. - 테스트 LateralNoCh3FallbackTest 5건. 기존 LateralAlignParityTest 는 그대로(기본값 경로). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -96,13 +96,24 @@ object AlignmentConstants {
|
|||||||
const val VERTICAL_SOFT_HINT_AFTER = 12
|
const val VERTICAL_SOFT_HINT_AFTER = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
/** stateless 단일 스냅샷 advisor — alignment.py:alignment_advice(). */
|
/**
|
||||||
|
* stateless 단일 스냅샷 advisor — alignment.py:alignment_advice().
|
||||||
|
*
|
||||||
|
* [requireCh3] (2026-10-13): 상하 단계가 **CH3 전무 폴백**(`no_ch3_max_nch`)으로 자리를 골랐으면
|
||||||
|
* 좌우 단계에서 CH3 를 요구하면 안 된다 — 그 자리는 CH3 가 없는 걸 알고 고른 자리라
|
||||||
|
* "↑ 머리방향(ch3 미검출)"이 영영 끝나지 않는다(2026-10-13 임상, supine 에서 실제로 그랬다).
|
||||||
|
* 그때는 CH4·CH5 균형만 본다. 기본값 true 는 Python 과 1:1 인 종전 동작이다.
|
||||||
|
*/
|
||||||
object AlignmentAdvice {
|
object AlignmentAdvice {
|
||||||
fun advise(ch3: Boolean, u4: Int?, u5: Int?, latTol: Int = AlignmentConstants.LAT_TOL): Pair<AlignAction, String> {
|
fun advise(
|
||||||
if (!ch3) return Pair(AlignAction.MOVE_UP, "↑ 머리방향으로 이동 (ch3 미검출)")
|
ch3: Boolean, u4: Int?, u5: Int?, latTol: Int = AlignmentConstants.LAT_TOL,
|
||||||
|
requireCh3: Boolean = true,
|
||||||
|
): Pair<AlignAction, String> {
|
||||||
|
if (!ch3 && requireCh3) return Pair(AlignAction.MOVE_UP, "↑ 머리방향으로 이동 (ch3 미검출)")
|
||||||
|
val note = if (!ch3) " · ch3 없음(폴백 위치)" else ""
|
||||||
if (u4 != null && u5 != null) {
|
if (u4 != null && u5 != null) {
|
||||||
val d = abs(u4 - u5)
|
val d = abs(u4 - u5)
|
||||||
if (d <= latTol) return Pair(AlignAction.STOP, "■ 정렬 완료 — 정지 (|Δ|=$d)")
|
if (d <= latTol) return Pair(AlignAction.STOP, "■ 정렬 완료 — 정지 (|Δ|=$d)$note")
|
||||||
return if (u4 < u5)
|
return if (u4 < u5)
|
||||||
Pair(AlignAction.MOVE_RIGHT, "→ 우측으로 이동 (ch4 $u4 < ch5 $u5, |Δ|=$d)")
|
Pair(AlignAction.MOVE_RIGHT, "→ 우측으로 이동 (ch4 $u4 < ch5 $u5, |Δ|=$d)")
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ class LateralGuide(
|
|||||||
|
|
||||||
private val buf = ArrayDeque<List<DoubleArray>>()
|
private val buf = ArrayDeque<List<DoubleArray>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 좌우 판정에서 CH3 를 요구할지(2026-10-13). 상하 단계가 CH3 전무 폴백으로 자리를 골랐으면
|
||||||
|
* false — 그 자리엔 CH3 가 없는 게 전제라 "머리방향으로 올리라"가 끝나지 않는다. 호출부가
|
||||||
|
* 좌우를 시작하기 전에 상하 판정의 `select_rule` 을 보고 정한다.
|
||||||
|
*/
|
||||||
|
var requireCh3: Boolean = true
|
||||||
|
|
||||||
/** 마지막 판정. 아직 한 번도 못 채웠으면 null. */
|
/** 마지막 판정. 아직 한 번도 못 채웠으면 null. */
|
||||||
var committed: Commit? = null
|
var committed: Commit? = null
|
||||||
private set
|
private set
|
||||||
@@ -96,7 +103,7 @@ class LateralGuide(
|
|||||||
val ch3 = walls.size > CH3_INDEX && walls[CH3_INDEX] != null
|
val ch3 = walls.size > CH3_INDEX && walls[CH3_INDEX] != null
|
||||||
val u4 = walls.getOrNull(CH4_INDEX)?.urineLen
|
val u4 = walls.getOrNull(CH4_INDEX)?.urineLen
|
||||||
val u5 = walls.getOrNull(CH5_INDEX)?.urineLen
|
val u5 = walls.getOrNull(CH5_INDEX)?.urineLen
|
||||||
val (action, msg) = AlignmentAdvice.advise(ch3, u4, u5, latTol)
|
val (action, msg) = AlignmentAdvice.advise(ch3, u4, u5, latTol, requireCh3)
|
||||||
|
|
||||||
return Commit(
|
return Commit(
|
||||||
action = action,
|
action = action,
|
||||||
|
|||||||
@@ -975,6 +975,11 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
// 평균에 섞이면 첫 판정이 엉뚱한 방향을 가리킨다.
|
// 평균에 섞이면 첫 판정이 엉뚱한 방향을 가리킨다.
|
||||||
lateral.reset(); lrCommit = null; lrAccum = 0; lrConfirmed = false
|
lateral.reset(); lrCommit = null; lrAccum = 0; lrConfirmed = false
|
||||||
lrRecent.clear()
|
lrRecent.clear()
|
||||||
|
// 상하가 CH3 전무 폴백으로 정해졌으면 좌우에서 CH3 를 요구하지 않는다
|
||||||
|
// (2026-10-13 임상: 폴백 자리에서 "↑ 머리방향(ch3 미검출)"이 끝나지 않았다).
|
||||||
|
lateral.requireCh3 = AnchorSelection
|
||||||
|
.selectBest(guide.records.filter { it.capFrac.isFinite() })
|
||||||
|
.rule != AnchorSelectRule.NO_CH3_MAX_NCH
|
||||||
lrRunning = true
|
lrRunning = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.medithings.vesiscan.managers
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 좌우 정렬의 CH3 조건 끄기 (2026-10-13).
|
||||||
|
*
|
||||||
|
* 상하 단계가 CH3 전무 폴백(`no_ch3_max_nch`)으로 자리를 골랐으면 그 자리에 CH3 가 없는 게
|
||||||
|
* 전제다. 그런데 좌우 advisor 는 CH3 가 없으면 무조건 "↑ 머리방향(ch3 미검출)"을 내서 끝나지
|
||||||
|
* 않았다(2026-10-13 임상 supine). `requireCh3=false` 면 CH4·CH5 균형만 본다. 기본값은 종전 그대로.
|
||||||
|
*/
|
||||||
|
class LateralNoCh3FallbackTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `default still demands CH3 - Python parity untouched`() {
|
||||||
|
assertEquals(AlignAction.MOVE_UP, AlignmentAdvice.advise(ch3 = false, u4 = 50, u5 = 50).first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `without the CH3 requirement a balanced pair stops`() {
|
||||||
|
val (a, msg) = AlignmentAdvice.advise(ch3 = false, u4 = 50, u5 = 50, requireCh3 = false)
|
||||||
|
assertEquals(AlignAction.STOP, a)
|
||||||
|
assertTrue(msg.contains("ch3 없음")) // 폴백 자리라는 걸 화면에 남긴다
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `without the CH3 requirement imbalance still steers left or right`() {
|
||||||
|
assertEquals(AlignAction.MOVE_RIGHT, AlignmentAdvice.advise(false, 10, 90, requireCh3 = false).first)
|
||||||
|
assertEquals(AlignAction.MOVE_LEFT, AlignmentAdvice.advise(false, 90, 10, requireCh3 = false).first)
|
||||||
|
assertEquals(AlignAction.PROBE_LR, AlignmentAdvice.advise(false, null, null, requireCh3 = false).first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `with CH3 present the flag changes nothing`() {
|
||||||
|
assertEquals(AlignAction.STOP, AlignmentAdvice.advise(true, 50, 50, requireCh3 = false).first)
|
||||||
|
assertEquals(AlignAction.STOP, AlignmentAdvice.advise(true, 50, 50, requireCh3 = true).first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `LateralGuide forwards the flag`() {
|
||||||
|
val g = LateralGuide()
|
||||||
|
assertTrue(g.requireCh3)
|
||||||
|
g.requireCh3 = false
|
||||||
|
assertEquals(false, g.requireCh3)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user