feat(algo): 미이식 3건 마저 이식 — post_tie_lock · sibeam · F83

레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.

  · post_tie_lock  후벽 상위 두 후보의 score 가 구조적으로 붙는 구간(전 데이터
                   5773 결정 중 격차<10% 가 17.9%)에서 직전 trace 선택을 유지한다.
                   방향을 강제하지 않는다 — 깊은-peak 고정·인접 골 tie-break 은
                   전부 계통 편향을 낳았고, tie 밴드에서는 정의상 두 후보의 증거가
                   같기 때문이다. 상태기를 넘길 때만 동작하며, 레퍼런스도 streaming
                   strict pass 에서만 건다. streaming.py 이식 전까지 호출부는 없다.
  · _sibeam_cap_bounds  검출 채널이 정확히 2개일 때 미검출 이웃 빔까지의 SI 거리를
                   cap 상한으로. 2개뿐이면 끝 채널이 서로의 이웃이라 기존 top/bottom
                   clamp 가 성립하지 않아 cap 이 무제한 외삽된다. supine 은 건너뛴다.
  · F83            적도-밖 bottom cap 연속 보간(실리콘·supine). 전제인
                   mirror_bottom_cap 도 함께 옮겼다.

좌표계가 두 개라는 것을 확인했다

레퍼런스 cross_channel._z 는 sample_to_ap_depth 를 인자 없이 불러 교차채널 좌표가
언제나 모듈 기본값(1.936/6.85)으로 떨어진다. BV 는 프리셋 값을 명시로 넘긴다.
abs 는 두 값이 같아 표가 안 나지만 실리콘(1.897/7.651)에서는 갈린다 — 실측
f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 439.88 vs 416.95 mL 이 됐다.
is_deep 판정이 34.209 > 34.042 처럼 아슬아슬한 자리라 작은 좌표 차가 선택을 뒤집는다.
의도인지 누락인지는 알고리즘팀 확인이 필요하다. 확인 전까지 레퍼런스와 같은 값을
내는 쪽을 택했고, 기존 경로는 프리셋 값을 그대로 쓴다.

missing_outside 의 null 과 빈 집합을 구분했다

null = 계산 안 함 → cap clamp 를 언제나 상한으로. 빈 집합 = 계산했고 '방광 밖'
채널이 없다 → 하한 쪽으로. ifEmpty { null } 로 뭉개면 "미검출이지만 span 은 있다"는
가장 흔한 경우에 si_floor 가 통째로 죽는다.

검증

  · 레퍼런스 대조 6종 전부 불일치 0 — tie 1568행(lock 24건) · 실데이터 BV 784행
    (nch=2 6건) · sibeam 120행(상한 102건) · 합성 BV 640행(mirror 226건) ·
    F83 blend 440행 · F83 apply 12행. 실데이터가 미러·nch=2 에 거의 안 닿아
    그 경로는 합성 입력으로 레퍼런스 함수를 직접 호출한 기대값으로 덮었다.
  · 기존 경로 무변경 — 44 cycle × CCC on/off 덤프가 HEAD 와 88행 전부 일치.
  · 전체 36개 통과 · demoDebug APK 빌드 성공.

LegacyEquivalenceDumpTest 가 PiezoHW.activePreset 을 명시로 고정하게 했다. 전역
가변 상태라 다른 테스트가 먼저 돌면 덤프가 통째로 달라진다 — 실제로 한 번 착시를
만들었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:09:26 +09:00
parent ccb44f0ea6
commit 441f249410
19 changed files with 4415 additions and 29 deletions
+47 -5
View File
@@ -100,13 +100,55 @@ TGC 로 재면 원거리 이득이 약한 채널을 정상 채널에 붙여 놓
bit-exact" 라던 종전 주석은 근거가 없어져 걷어내고, 성격을 **회귀 고정**으로 바꿔
적었다.
## 나중에 추가된 3건 (2026-09-03)
`post_tie_lock` · `_sibeam_cap_bounds` · F83 을 마저 옮겼다. 셋 다 레퍼런스 경로에서만
의미가 있다.
| 항목 | 어디 | 기존 경로에 미치는 영향 |
|---|---|---|
| `post_tie_lock` | `walldetect/PostTieLock.kt` | 없음 — 상태기를 **넘길 때만** 동작하고, 넘기는 호출부가 없다 |
| `_sibeam_cap_bounds` | `PiezoBVEstimator.kt` | 없음 — `AlgoMode.reference` 로 막았다 |
| 미러 + F83 | `PiezoBVF83.kt` | 없음 — `mirrorBottomCap` 을 켜는 호출부가 F83 뿐이다 |
`post_tie_lock` 은 후벽 상위 두 후보의 score 가 구조적으로 붙는 구간에서 **직전 trace 의
선택을 유지**한다(방향을 강제하지 않는다 — 그렇게 한 앞선 시도들은 전부 계통 편향을
낳았다). 레퍼런스도 streaming 경로의 strict pass 에서만 걸고 정렬에서는 쓰지 않으므로,
`streaming.py` 를 옮기기 전까지는 클래스와 배선만 자리를 잡아 둔 상태다.
### 좌표계가 두 개다 — 레퍼런스를 그대로 따랐다
레퍼런스 `cross_channel._z` 는 `sample_to_ap_depth(idx, angle)` 를 **인자 없이** 불러
교차채널 좌표가 언제나 모듈 기본값(1.936 / 6.85)으로 떨어진다. 반면 BV 는 프리셋 값을
명시로 넘긴다. abs(vr\*)는 두 값이 같아 표가 안 나지만 **실리콘(r\*: 1.897/7.651)에서는
갈린다** — 실측 f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 부피가 439.88 vs
416.95 mL 이 됐다. `is_deep` 판정이 34.209 > 34.042 처럼 아슬아슬하게 걸리는 자리라
작은 좌표 차이가 그대로 선택을 뒤집는다.
의도인지 누락인지 **알고리즘팀 확인이 필요하다.** 확인 전까지는 레퍼런스와 같은 값을
내는 쪽을 택했고, **기존 경로는 프리셋 값을 그대로 쓴다** — 오늘 임상이 그 위에서 돌았다.
### 대조 결과
`ThreeFeatureParityTest` 가 모드 ON 상태에서 레퍼런스 산출 기대값과 대조한다.
실데이터는 abs 인체 0~4cm 라 미러·nch=2 에 거의 안 닿아, 그 경로는 합성 입력으로
**레퍼런스 함수를 직접 호출한** 기대값으로 덮었다.
| 대조 | 행 | 실제로 밟은 경로 |
|---|---|---|
| tie lock | 1568 | lock 발동 24 건 |
| 실데이터 BV | 784 | nch=2 6 건 |
| sibeam | 120 | 상한 산출 102 건 |
| 합성 BV | 640 | `mirror sphere` 226 건 |
| F83 blend | 440 | weight 0→1 |
| F83 apply | 12 | sub-window 재검출 |
전부 불일치 0.
## 아직 안 옮긴 것
| 항목 | 왜 |
|---|---|
| `post_tie_lock` | 상태기 — 스트리밍 경로와 함께 설계해야 한다 |
| `_sibeam_cap_bounds` | nch=2 전용 |
| F83 cap 보간 | 실리콘 + supine 조건 |
`streaming.py`(연속 측정 상태기). `post_tie_lock` 의 자연스러운 소비처라 함께 설계해야
한다.
레퍼런스 저장소의 `library/analysis/abdomen.py` 가 커밋되어 있지 않아
`pipeline_dump.py` · `compare.py` 가 그대로는 돌지 않는다. 이번 대조는 스크래치 사본에