feat(align): nch=4 위치가 3곳 이상이면 cap_frac 최소 대신 가운데를 고른다 — 방광 끝 위치 선택으로 BV 과소 추정

알고리즘 팀(2026-10-07): nch=4 인 위치가 여럿이라는 것은 방광이 빔 커버 범위보다 크다는
뜻이다. 그때 cap_frac 최소 규칙은 외삽을 최소화하는 자리를 찾으려 하지만, 방광 끝에 가까운
위치일수록 cap_frac 이 오히려 작게 계산된다 — 한쪽 cap 은 끝 채널이 방광을 스쳐 작아지고,
반대쪽 cap 은 높이 상한에 걸려 그만큼 커지지 못한다. 그 결과 끝 위치가 뽑히고 BV 가 과소
추정됐다.

규칙 ①′: 후보(ch3 게이트 통과) 중 nch=4 가 3곳 이상이면 cm 순 가운데, 짝수면 가운데 둘 중
아래쪽(치골 쪽, 작은 cm). 2곳까지는 종전 규칙(nch 최대 → cap_frac 최소 → 낮은 cm) 그대로.

이 가지는 Python select_supine_anchor 에 아직 없다. 레퍼런스 케이스 200개에는 이 가지를 타는
케이스가 없어(확인) 대조가 그대로 유효하고, 패리티 시험이 그 가정을 검사한다. 새 가지는
AnchorSelectionCenterRuleTest 7건이 잠근다. 어느 가지로 골랐는지 AnchorDecision.rule 로
돌려주고, 요약·labdb 601 params 에 select_rule 로 남긴다 — nch4_center 세션은
selection_verified_against_reference=false. 정렬 완료 안내 문구에도 "채널 4/4 위치 N곳 —
가운데" 를 덧붙인다(화면은 종전대로 "치골 위 N cm" 만 보여 준다).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 16:49:02 +09:00
parent a6b19b3dcc
commit 1592a1454e
6 changed files with 160 additions and 8 deletions
+18 -1
View File
@@ -341,7 +341,7 @@ eligible = ch3Mean && ch3Rate >= 0.80
> ②는 "앞에 eligible 이 있을 때만" 걸립니다. 한 번도 CH3 가 안 잡혔으면 ③(상한)까지 갑니다.
### 3.4 최적 선택 — 3단 규칙
### 3.4 최적 선택 — 3단 규칙 (+ nch=4 다수 예외)
`AnchorSelection.selectBest()` (`AnchorGuide.kt:174`)
@@ -352,6 +352,9 @@ eligible = ch3Mean && ch3Rate >= 0.80
① 후보 자격 ch3 == "O" AND ch3_rate ≥ 0.8
→ 통과가 0개면 → REATTACH (재부착)
①′ nch=4 다수 nch=4 인 후보가 **3곳 이상**이면 그중 **가운데** (cm 순 · 짝수면 아래쪽=치골 쪽)
→ 여기서 끝. 2곳까지는 아래로. (2026-10-07~)
② 규칙 A 남은 후보 중 nch 최대
③ 동률이면 cap_frac 최소 → 그래도 같으면 낮은 cm
@@ -359,6 +362,18 @@ eligible = ch3Mean && ch3Rate >= 0.80
입력은 **`cap_frac` 이 유한한 위치만**입니다 (`selectableRecords()`).
**①′ 는 2026-10-07 에 추가됐습니다(알고리즘 팀).** nch=4 인 위치가 여럿이라는 것은 방광이
빔 커버 범위보다 크다는 뜻입니다. 이때 ③의 cap_frac 최소 규칙은 외삽 영역을 최소화하는
위치를 찾으려 하지만, **방광 끝에 가까운 위치일수록 cap_frac 이 오히려 작게** 계산됩니다 —
한쪽 cap 은 끝 채널이 방광을 스쳐 작아지고, 반대쪽 cap 은 높이 상한에 걸려 그만큼 커지지
못합니다. 그 결과 방광 끝 위치가 선택되고 BV 가 과소 추정됐습니다. 그래서 nch=4 위치가
3곳 이상이면 가운데를 고릅니다. 짝수면 가운데 둘 중 아래쪽(작은 cm)입니다.
이 가지는 Python `select_supine_anchor` 에 **아직 없습니다.** 레퍼런스 케이스 200개에는 이
가지를 타는 케이스가 없어 대조는 그대로 유효하고(`AnchorSelectionParityTest` 가 확인), 새
가지는 `AnchorSelectionCenterRuleTest` 가 잠급니다. 이 가지로 고른 세션은 요약에
`select_rule: "nch4_center"` · `selection_verified_against_reference: false` 로 남습니다.
### 3.5 부착 위치 = 최적 (오프셋 0 · 2026-10-01~)
```kotlin
@@ -433,6 +448,7 @@ val anchorCm: Int? get() = bestCm?.let { it + offsetCm } // offsetCm = 0 (2026
**레퍼런스는 어떻게 되나.** 선택 규칙 `selectBest` 는 Python `select_supine_anchor` 와
1:1 이고 200 케이스로 대조돼 있습니다(`AnchorSelectionParityTest`) — **손대지 않았습니다.**
(2026-10-07 에 ①′ nch=4 다수 예외가 더해졌습니다 — [3.4](#34-최적-선택--3단-규칙--nch4-다수-예외).)
바뀐 것은 후보 집합뿐이고, **Python 의 회고 분석 경로가 이미 전 위치로 판정**하므로 같은
방식으로 재현·대조할 수 있습니다. 다만 최종 보고서의 "단일 pass" 기술은 갱신이 필요합니다.
@@ -440,6 +456,7 @@ val anchorCm: Int? get() = bestCm?.let { it + offsetCm } // offsetCm = 0 (2026
```json
"selection_scope": "full_sweep",
"select_rule": "max_nch_min_cap", // 2026-10-07~ · "nch4_center" 면 Python 미대조 가지
"best_cm": 3, // 앱이 쓴 값 = 전 위치 판정
"anchor_cm": 4, // best + 1
"best_cm_single_pass": 0, // 옛 단일 pass 가 냈을 답
+2 -1
View File
@@ -190,12 +190,13 @@ IMU 는 2026-09-21 이후 업로드분부터 `sensor.imu` 로 들어옵니다(
| `ch3_hit_min` | number | CH3 검출률 하한 (0.8) |
| `final_offset_cm` | int | best 에 더하는 오프셋 — **0** (2026-10-01~), 그 전 세션은 1 |
| `best_cm` | int / null | 알고리즘이 고른 최적 위치 |
| `select_rule` | string | 어느 가지로 골랐나 (2026-10-07~): `max_nch_min_cap` 종전 3단 규칙 · `nch4_center` nch=4 후보 3곳 이상 → 가운데 · `none` 후보 없음 |
| `anchor_cm` | int / null | 실제 부착 위치 = `best_cm + final_offset_cm` (2026-10-01~ 는 best 그 자리) |
| `action` | string | `STOP` / `REATTACH` / … |
| `stop_reason` | string / null | 탐색 종료 사유 |
| `max_nch` | int | 관측된 최대 검출 채널 수 |
| `patient_unnamed` | bool | 환자명 없이 잰 데이터인가(폴더가 `unnamed_*`) |
| `selection_verified_against_reference` | bool | 앱 판정이 Python 레퍼런스와 대조 완료인가 |
| `selection_verified_against_reference` | bool | 앱 판정이 Python 레퍼런스와 대조 완료인가 — `nch4_center` 가지는 Python 에 아직 없어 **false** |
| `positions` | array | 위치별 지표 — 아래 |
| `lateral` | object | 좌우 정렬 결과 — 아래 |
| `anchor_basis` | string / null | 이 위치로 임상을 진행한 근거. 600 의 같은 필드와 같은 값입니다. **null = 정렬만 하고 임상으로 넘어가지 않음** |