# 한계 — 이 프로젝트가 못 하는 것, 검증 안 한 것, 근사한 것

README가 인상적으로 들린다면, 그 별표는 전부 여기 붙어 있습니다.

한 줄 요약: **이 초파리는 게임을 잘 하지 못합니다.** 목표를 향해 가지 못합니다.
그런데도 쓸모가 있는 이유는, QA 퍼저에게 필요한 것이 목표 추적이 아니라
지치지 않는 탐색이기 때문입니다.

---

## 1. 커넥톰은 초파리를 목적지로 데려가지 못합니다

가장 중요한 한계입니다. 측정 결과를 그대로 적습니다.

게임 중에 조향 신호와 "빛이 있는 방향" 사이의 상관계수를 8개 시드로 쟀습니다.

| 디코더 방식 | 평균 상관계수 |
|---|---|
| 좌우 비율 `(r-l)/(r+l)` | 0.046 ± 0.272 |
| 적응형 기준선 (감각 적응) | 0.077 |
| 정류 임계 + tanh (Xenova 방식) | −0.042 ± 0.111 |
| 이산 saccade (200ms 적분) | 0.046 ± 0.272 |

**전부 0입니다.** 방향을 추적하지 못합니다.

정적인 조건에서는 좌우를 구분합니다. 한쪽 눈만 밝게 하고 0.9초 기다리면
조향 신호가 분리도 0.95로 갈립니다. 그러나 닫힌 루프에서는 그 신호가
초파리 머리가 도는 속도를 따라가지 못합니다.

시각→하행뉴런 경로의 지연과 잡음이 원인으로 보이나, 확정하지 못했습니다.

### 우리만 그런 것이 아닙니다

이 분야의 다른 구현을 열어봤습니다.

- **`abgunaydin/webgpu-fly`** — `LIMITATIONS.md`에 *"the connectome does not walk the fly"*
  라고 직접 씁니다. 2,030만 연결이 몸에 닿는 건 틱당 스칼라 하나와 회전 편향 하나이고,
  그 둘이 손으로 짠 `sin(t × 10Hz)` 삼각보행을 스케일합니다.
- **`Xenova/fruit-fly-simulation`** — README에 *"The movements are illustrative,
  not validated predictions of fly behavior"*. 디코더 첫 줄 주석이 "Engineered rate decoder"입니다.
- **`snedea/flybrain`** — README는 *"Behavior is not scripted"* 라고 하지만,
  조향 코드는 뇌 기여를 `Math.max(-0.05, Math.min(0.05, newDir))` 로 클램프하고
  실제 방향은 `atan2` 로 직접 계산합니다. 주석에도
  *"Steering is primarily handled by behavioral biases"* 라고 적혀 있습니다.
  클램프 이유는 *"to prevent worker noise from causing spinning"* 입니다.

- **`nftechie/doomfly`** — 이 분야에서 가장 멀리 간 시도입니다. MaleCNS v1.0(166,700뉴런,
  2,560만 연결)에 도파민 가소성까지 붙였습니다. 손상 시 PPL101 도파민 세포 2개에 200ms
  혐오 자극을 주고 KC→MBON11 연결 4,184개를 가소성 규칙으로 바꿉니다.
  그런데 README 첫 줄이 이렇습니다:

  > **Status: live experimental training, not demonstrated learned survival.**
  > The current v6 candidate failed its visual, conditioning and survival validation gates.

  조향은 `turn = clip((rate(DNp20,R) - rate(DNp20,L)) * .12, -6, 6)` 입니다.
  그들 스스로 *"engineered controller assignments, not established natural motor functions"* 라고
  적어놨습니다.

  **우리 모델에서 DNp20 을 직접 재봤습니다.**

  | 읽기 방식 | 좌우 분리도 |
  |---|---|
  | DNp20 R−L (doomfly 채택) | **0.18 Hz** |
  | DNa02 R−L | 7.45 Hz |
  | 이 프로젝트의 통계 채널 | **0.889** (정규화) |

  DNp20 은 좌우를 거의 구분하지 못합니다. 개체당 좌우 각 1개뿐이라 표본이 너무 작습니다.

**아무도 못 했습니다.** 강화학습을 붙인 쪽도 자기 검증을 통과하지 못했습니다.
우리는 그래서 조향을 보정하지 않기로 했습니다.
초파리가 헤매는 모습이 지금 기술의 실제 모습입니다.

## 1.5 왜 못 하는가 — 배선은 있지만 동역학이 없다

"시각 정보를 더 제대로 주면 되지 않나"를 끝까지 파봤습니다. 결론은 **입력 문제가 아니다** 입니다.

### 실험 1 — 밝기 대신 움직임을 줬다

초파리 시각계는 밝기가 아니라 시간 변화를 읽습니다. 그래서 ON/OFF 시간 대비를 만들어
해부학적으로 올바른 자리에 넣었습니다. L1(ON 경로)과 L2(OFF 경로)입니다.

```
L1 125Hz  →  Mi1 0.08Hz   Tm3 0.00Hz   Mi4 0.00Hz   Mi9 0.00Hz     ON 경로 전멸
L2 125Hz  →  Tm1 26.0Hz   Tm2 15.5Hz   Tm4 21.0Hz                  OFF 경로 정상
```

**OFF 경로는 살고 ON 경로는 죽습니다.** 초파리의 ON 경로는 *억제를 통한 부호 반전*으로
작동하기 때문입니다. L1 은 글루탐산성 억제 시냅스로 Mi1 을 누르고, Mi1 이 다시 반전시켜
ON 신호를 만듭니다. 우리 모델에는 **기저 발화가 없어서**, 조용한 뉴런을 억제해 봐야
아무 일도 일어나지 않습니다. ON 입력을 받는 T4 는 그래서 영영 울지 않습니다.

### 실험 2 — 기저 활동을 넣었다

모든 뉴런에 기저 전류를 넣어 억제가 작동할 바닥을 만들어 줬습니다.
HS(수평 광류 적분 뉴런)가 0 → 6.7Hz 로 깨어났습니다. 그러나 **방향 선택성은 0** 이었습니다.

| 기저 | 오른쪽 흐름 | 왼쪽 흐름 | 방향 분리 |
|---|---|---|---|
| 5.5 | 0.00 | 0.00 | 0.00 Hz |
| 6.0 | 0.00 | 0.00 | 0.00 Hz |
| 6.5 | 0.26 | 0.00 | 0.26 Hz |

### 실험 3 — 가지치기를 풀었다

시각 경로의 연결을 2배(270만 → 590만, 시냅스 62.7% → 78.4%)로 늘렸습니다.
T4 는 여전히 **0.0Hz** 였습니다.

### 그래서 진짜 이유

**T4/T5 의 방향 선택성은 뉴런마다 다른 시간 특성에서 나옵니다.**
Mi9 는 느린 OFF, Mi1/Tm3 는 빠른 ON, Mi4 는 지연된 억제 — 이 시간차를 비교해
움직임의 방향을 계산합니다.

우리 모델은 **모든 뉴런에 똑같이 τ_m = 20ms, τ_syn = 5ms** 를 씁니다.
커넥톰은 **배선을 주지만 동역학은 주지 않습니다.** 배선이 아무리 정확해도
필터가 균일하면 이 계산은 원리적으로 성립하지 않습니다.

이것이 커넥톰만으로 닫힌 루프 항법이 되지 않는 근본 이유입니다.
디코더 문제도, 가지치기 문제도, 렌더링 문제도 아니었습니다.

세포 유형별 시간 상수와 신경전달물질 수용체 동역학을 넣으면 달라질 수 있습니다.
그건 이 프로젝트의 범위를 넘어섭니다.

## 1.8 목표를 못 찾는 것과, 제자리에서 도는 것은 다르다

항법이 안 되는 것은 위에 적은 이유로 어쩔 수 없습니다. 그러나 **제자리에서 빙빙 도는 것**은
별개의 문제였고, 그건 우리 잘못이었습니다.

조향 신호에 남아 있던 고정 편향과 포화 때문에 초파리가 구석에서 계속 같은 방향으로만 돌았습니다.
적응 시간과 이득을 **탐색 범위를 기준으로** 실측해 다시 정했습니다.

| | 이전 | 이후 |
|---|---|---|
| 조향 평균 | 0.273 (오른쪽 치우침) | **0.007** |
| 포화 비율 | 20% | **0%** |
| 총 회전량 | 8.3바퀴 | **1.9바퀴** |
| 탐색 범위 x | 20~179 | **30~319** (맵 전체 폭) |
| 탐색 범위 y | 20~45 | **22~179** |
| 방문한 칸 | 16 | **30** |

### 그리고 두 번째 문제 — 끼면 못 빠져나온다

한 판을 길게 돌려도 탐색이 늘지 않았습니다. 750프레임에 12칸, 6000프레임에 8칸.
**초파리가 일찍 끼여서 그대로 머물기 때문**입니다.

여기서 쓴 해결책은 뇌를 손대지 않습니다. **끼임·진행불가·정지가 감지되면 그 판을 끝내고
새 판을 시작합니다.** 버그는 그대로 기록되고, 실제 퍼저가 하는 일과 같습니다.

같은 계산량(24,000프레임)으로 3회차 평균을 냈습니다.

| | 판수 | 탐색률 | 증상 종류 | 발견 |
|---|---|---|---|---|
| 6000프레임까지 버틴다 | 5 | 9% | 2.7종 | 10건 |
| **끼면 바로 새 판** | **53** | **37%** | **3.0종** | **53건** |

**같은 계산량에서 맵을 4배 넓게 두들깁니다.** 발견 건수도 5배지만, 이 도구의 입장상
건수보다 **탐색률**이 근거입니다.

### 시도했다가 버린 것

판마다 다른 지점에서 시작하게 해봤습니다. 실제 퍼저가 초기 상태를 무작위화하듯이요.
**더 나빠졌습니다.** 작은 막다른 구역에서 시작하는 경우가 생겨서로 보입니다.
측정하고 폐기했습니다. (이 비교는 중복 집계를 고치기 전에 잰 것이라 숫자는 싣지 않습니다.)

QA 퍼저에게 중요한 것은 목표 도달이 아니라 **얼마나 넓게 두들기느냐**입니다.
목표를 못 찾는 것은 모델의 한계지만, 넓게 돌아다니지 못한 것은 고칠 수 있는 문제였습니다.

## 1.9 초파리는 난수에게 진다

이 도구의 본체는 QA 프레임워크입니다. 화면을 읽고, 입력을 넣고, 증상을 찾고,
입력열을 저장해 재현합니다. **무엇이 입력을 만드는지는 갈아 끼울 수 있습니다.**

그래서 같은 조건에서 쟀습니다. 24,000프레임 예산, 연구실 게임, 같은 감지기.

### 처음 낸 숫자는 믿을 게 못 됐습니다

처음에는 정책마다 한 번씩만 돌려 표를 만들었습니다. 그런데 다시 돌리니 평활 난수가
55% 에서 78% 로 튀었습니다. **한 번 측정한 난수 정책의 숫자는 아무 의미가 없습니다.**
심사자가 다시 돌리면 다른 값이 나오는 표를 싣고 있었던 겁니다.

그래서 난수를 `Math.random` 에서 **시드 고정 PRNG** 로 바꾸고, 회차를 나눠
평균과 범위를 냅니다. `node policy_bench.mjs` 로 그대로 재현됩니다.

| 정책 | 회차 | 탐색률(평균) | 범위 | **증상 종류(평균)** | 관측된 종류 |
|---|---|---|---|---|---|
| 초파리 커넥톰 | 3 | **34%** | 28–41% | **2.7종** | STUCK, OOB, DOORLOCK, FREEZE |
| 평활 난수 | 5 | 61% | 57–66% | 3.2종 | 다섯 종 전부 |
| 균등 난수 | 5 | 50% | 46–52% | 3.6종 | STUCK, OOB, DOORLOCK, FREEZE |
| 직진만 | 3 | 5% | 5–5% | 1.0종 | FREEZE |

**초파리가 집니다.** 탐색률에서 평활 난수의 절반을 겨우 넘고, 한 판에 닿는 증상 종류도
두 난수 정책보다 적습니다. 이기는 항목이 하나도 없습니다.

그리고 여기서 QA 지표에 대한 교훈이 하나 나옵니다.
**"버그 수"는 가짜 지표입니다.** 「직진만」이 95건으로 최다인데 전부 같은 정지 버그 하나입니다.
의미 있는 것은 **탐색률과 증상의 종류**입니다.

### 우리도 부풀리고 있었습니다

"버그 개수는 가짜 지표"라고 써놓고, 정작 맵 이탈을 픽셀 버킷으로 나눠 세고 있었습니다.
(321,20)과 (328,20)은 **같은 탈출**인데 두 건으로 올라갔습니다. 넘은 변 기준으로 묶도록
고쳤고, 그 결과 위 표의 건수가 전부 줄었습니다. 결론은 바뀌지 않았습니다.

이 결과를 숨기지 않고 화면 전면에 싣습니다. 제품은 정책이 아니라 프레임워크이고,
프레임워크는 꽂히는 어떤 정책보다 오래 살기 때문입니다.

## 2. 그 대신 확인한 것

| 주장 | 근거 |
|---|---|
| 진짜 커넥톰 138,639뉴런이 브라우저에서 돈다 | 초당 1,321스텝, 283만 스파이크/초 |
| 시각 자극이 하행뉴런까지 전파된다 | 눈 150Hz 자극 시 하행뉴런 247개가 1Hz 이상 발화 |
| 정적 조건에서 좌우를 구분한다 | 분리도 0.95 (왼쪽 −0.41 / 오른쪽 +0.75) |
| **조향은 오직 뇌 출력에서만 나온다** | 조향 채널 40개(전체의 0.03%)를 자르면 분리도 0.95 → 0.00, 복구하면 1.12 |
| 버그를 찾는다 | 24,000프레임에 평균 55건, 심어둔 4종 중 3종 도달 |
| 100% 재현된다 | 12/12 완전 일치 |

**손상 실험이 이 프로젝트의 핵심 증거입니다.** 조향을 스크립트로 보정한 구현은
뉴런을 아무리 잘라도 똑같이 움직입니다. 우리 초파리는 멈춥니다.

## 2.5 남의 게임에 붙습니다 — 되는 것과 안 되는 것

"제품은 정책이 아니라 프레임워크"라는 주장은 우리가 한 번도 본 적 없는 게임에
붙을 수 있어야만 참입니다. 그래서 「Yours」 근무지를 만들었습니다.
HTML 게임을 떨어뜨리면 그대로 붙습니다.

**원리.** `blob:` URL 로 만든 문서는 만든 쪽의 출처를 물려받습니다. 그래서 iframe
안인데도 `contentDocument` 를 읽을 수 있고 캔버스 픽셀도 읽을 수 있습니다.
게임 스크립트보다 먼저 `getContext` 를 가로채 `preserveDrawingBuffer` 를 켜야
하므로, HTML 맨 앞에 심(shim)을 끼워 넣은 뒤 blob 으로 다시 만듭니다.
입력은 iframe 의 window/document/캔버스 세 곳 모두에 합성 `KeyboardEvent` 를 쏩니다.
어디서 듣는지 모르기 때문입니다.

**되는 것**

| | |
|---|---|
| 자체 완결형 HTML 파일 (드래그&드롭) | 됩니다 |
| CORS 를 열어둔 URL | 됩니다 (받아서 다시 띄웁니다) |
| 캔버스에 그리는 게임 | 됩니다 — 2D·WebGL 모두 |
| 정지·반복 감지 | 됩니다 (연구실과 같은 감지기) |
| 표적이 죽으면 새 판 | 됩니다 (연구실의 끼임 재시작과 같은 이유) |

**안 되는 것 — 숨기지 않습니다**

- **CORS 를 막은 URL.** 브라우저가 내용을 주지 않습니다. 우회할 방법이 없고,
  우회하는 척하면 거짓말입니다. 그래서 "파일을 저장해서 떨어뜨려 달라"고 정확히 말합니다.
- **DOM 게임.** 캔버스가 없으면 20초 뒤에 포기하고 그렇게 말합니다.
- **오염된 캔버스.** 게임이 CORS 없이 외부 이미지를 그렸으면 `getImageData` 가
  막힙니다. 이때 초파리는 앞이 안 보입니다. 화면에 `Canvas unreadable` 로 씁니다.
- **정확한 재현.** 외부 게임은 시드를 모르고 내부 상태도 못 봅니다. 입력열은
  남기지만 **12/12 완전 일치는 연구실에서만 보장됩니다.** 외부 표적의 재현은
  게임이 결정적일 때만 됩니다.
- **심어둔 정답이 없습니다.** 외부 게임에서는 "몇 종 중 몇 종을 찾았나"를
  잴 수 없습니다. 증상만 셉니다.

샘플 표적(`docs/samples/rover.html`)에는 프리즈와 화면 반복을 하나씩 심어 놨습니다.
우리 코드를 전혀 모르는 자체 완결형 HTML 이고, 붙이면 프리즈를 250프레임 안에 찾습니다.

## 3. 모델은 균일 LIF입니다

- 누출 적분 발화 뉴런. 호지킨-헉슬리 채널도, 수상돌기 구획도, 가소성도 없습니다.
- 신경전달물질은 흥분/억제 부호로만 반영합니다. 수용체별 동역학은 없습니다.
- 모든 뉴런이 같은 파라미터를 씁니다. 실제로는 세포 유형마다 다릅니다.

## 4. 커넥톰을 쳐냈습니다

- 시냅스 5개 미만 연결을 버려 1,509만 쌍 중 270만 쌍만 남겼습니다(시냅스의 62.7%).
- 남은 시냅스 가중치를 `0.275 / 0.627 ≈ 0.44` 로 보정했습니다. 가지치기 보정이지
  성능을 맞추기 위한 튜닝이 아닙니다.
- 뉴런의 96.8%는 여전히 연결이 남아 있습니다.

## 5. 시각 입력은 광수용체가 아닙니다

FlyWire FAFB는 망막과 라미나가 온전하지 않습니다. 광수용체 R1-6의 출력 연결이
평균 **1.7개**뿐입니다(전체 평균 19.5). 31%는 연결이 아예 없습니다.

그래서 한 단계 안쪽인 **LA>ME(라미나→수질, 8,029뉴런)** 에 넣습니다.
생물학적으로는 L1~L5가 광수용체 신호를 받는 첫 중계소이므로 타당하지만,
광수용체 자체의 처리 단계는 건너뜁니다.

## 6. 시야는 전처리를 거칩니다

날것의 화면을 그대로 넣으면 뇌가 반응하지 않습니다. 실측한 민감도는 이렇습니다.

| 평균 밝기 | 좌우 신호 변화폭 | 잡음 | 판정 |
|---|---|---|---|
| 0.25 | 0.073 | 0.080 | 신호 < 잡음 |
| 0.50 | 0.439 | 0.080 | 신호 5배 |

그래서 중심 밝기를 0.5로 올리고, 좌우 평균 차이를 3.2배 증폭하고,
시그모이드로 부드럽게 포화시킵니다. 라미나의 측면억제와 같은 종류의 처리이지만,
**이득 값은 우리가 정한 것이지 측정에서 나온 것이 아닙니다.**

## 7. 속도 비교는 조건이 다릅니다

`webgpu-fly`가 같은 기계(M2 Pro)에서 잰 값이 NEST 0.67kHz, Rust 0.45kHz,
WebGPU 0.25kHz입니다. 우리는 1.32kHz이지만 **연결 수가 다릅니다** —
저들은 2,030만, 우리는 270만입니다. 연결당 처리량으로는 비슷하거나 저들이 높습니다.
더 빠르다고 주장하지 않습니다.

## 7.5 저작권

- 커넥톰과 주석은 **CC-BY 4.0** 이라 재배포할 수 있습니다.
- 둠은 id Software 가 셰어웨어 판의 자유 재배포를 허용했고, 그 WAD 가 wasm 모듈에 들어 있습니다.
- **GTA1 자산은 이 저장소에 없습니다.** Carnage3D 엔진은 MIT 지만 게임 자산은
  Rockstar/Take-Two 저작물이며, 2004년 무료 배포는 재배포 허가가 아닙니다.
  그래서 원저자의 GitHub Pages 에서 직접 불러옵니다. 사용자가 원저자 데모 페이지를
  여는 것과 동일한 요청입니다. 다만 원저자 호스팅이 사라지면 GTA1 근무지는 동작하지 않습니다.

## 8. 검증하지 않은 것

- 브라우저·GPU별 동작 (개발은 Safari/Chrome + Apple Silicon에서만 했습니다)
- 여러 직원을 동시에 고용했을 때의 실제 병렬 성능
- 둠에서의 버그 검출 능력 (둠에는 심어둔 버그가 없어 정지·반복만 봅니다)
- 우리 테스트 게임 외의 실제 상용 게임에서의 유용성
