# fly — ハエの脳(コネクトーム)を触る環境
ショウジョウバエの神経系を電子顕微鏡で丸ごと輪切りにして、全ニューロンと全シナプスを
再構築した「コネクトーム」を手元で触るための環境。配線図を眺めるだけでなく、
**その脳が入った体を物理エンジンの中で飛ばして、脳・体・視界を並べて見る**ところまで。
## まず動かす — 脳・体・視界を並べる
```powershell
.\.venv\Scripts\python.exe run_app.py
```
```
┌─ 脳 ──────────┬─ 体 ──────────┬─ 視界 ─────────┐
│ 点群 (OpenGL) │ MuJoCo の描画 │ 複眼の像 │
│ 領域ごとの明るさ│ 位置・姿勢・速さ│ 報酬 (花) の操作 │
└───────────────┴───────────────┴────────────────┘
```
3枚とも **同じ1本のシミュレーションの同じ時刻** から出ている。場面は「飛ぶ (匂いの
する花に寄る)」と「逃げる (ハエ叩きが迫って跳ぶ)」の2つ。
- **脳が光るのは、その場所に実際に入力が届いているときだけ。** 視葉は複眼に映った像、
触角葉は匂いの濃度、神経核は関節の動き。入力を作っていない領域は暗いままで、
それが正しい。キノコ体は何も学習していないので光らせない。
- **報酬は花。** 置くとプルームの源ごと動くので、本当に振る舞いが変わる — 触角葉が
光り、機体が向きを変え、複眼の中で花が大きくなる。
- **必ずスロー再生になる** (実時間の 1/25 〜 1/100)。何倍かは固定で書かず実測して出す。
詳しくは [アプリ — ハエ脳ビューア](#アプリ--ハエ脳ビューア)。セットアップは下の
[使い方](#使い方) から。
## 入っているデータ
| | **Male CNS v1.0** | **hemibrain** | **FAFB-FlyWire** |
|---|---|---|---|
| 範囲 | オスの**中枢神経系まるごと**
(脳+視葉+腹部神経索) | メス脳の中心部のみ
(視葉なし) | メスの脳まるごと
(神経索なし) |
| ボディ数 | 211,577 | 21,739 | 139,248 |
| 接続 | **1億5185万** (3億1183万シナプス) | 355万 | — (注釈のみ) |
| 出典 | Janelia FlyEM 2025 | Scheffer et al. 2020 | Dorkenwald/Schlegel et al. 2024 |
| ログイン | 不要 | 不要 | 注釈は不要 / 生データは要アカウント |
**Male CNS が主力**。脳と神経索が繋がっているので「見る → 判断する → 脚を動かす」を
1本の配線図の上で端から端まで辿れる。hemibrain は軽くてツールが全部対応しているので練習用、
FlyWire はオスとメスを比べるための対照。
## セットアップ済みのもの
- Python 3.12 (uv 管理) + 仮想環境 `.venv/`
- `navis` (形態解析・3D可視化), `fafbseg` (FlyWire), `neuprint-python`, `flybrains`,
pandas / pyarrow / networkx / plotly / JupyterLab
- `flygym` + `mujoco` (NeuroMechFly v2 / flybody = 物理エンジン上のハエの体)
- `PySide6` + `pyqtgraph` (デスクトップアプリ)
## 使い方
```powershell
git clone https://github.com/seeton/fly.git
cd fly
uv venv --python 3.12 .venv
uv pip install --python .venv\Scripts\python.exe -r requirements.txt
python scripts\00_fetch_data.py # 公開データを取得 (78 MB, --all で 7.7 GB)
.\.venv\Scripts\Activate.ps1
```
`data/` (生データ) と `out/` (動画・3D HTML) は大きいのでリポジトリには入れていない。
どちらも上の取得スクリプトと `scripts/` の実行で作り直せる。
(有効化せずに `.\.venv\Scripts\python.exe` を直接叩いてもよい)
### まず 3D で見る
```powershell
python scripts\02_neuron_3d.py
```
hemibrain の実物のニューロン(嗅覚投射ニューロン DA1 uPN)5本と脳メッシュがブラウザで開いて回せる。
### 逃避回路を辿る — Male CNS
```powershell
python scripts\05_explore_malecns.py --descending DNp01
```
ハエに手を伸ばすと飛んで逃げる、あの回路。出力:
```
10001 DNp01 [descending_neuron]
-> 800146 TTMn [vnc_motor]
```
DNp01 = Giant Fiber(脳で一番太い軸索を持ち、1本で首を貫いて神経索まで降りる)。
TTMn = 中脚を蹴り出して体を空中に打ち上げる筋肉の運動ニューロン。**単シナプス**で直結
=最速で逃げるための配線で、数十年の電気生理の結論がそのままデータから出てくる。
`--type DNp01` で上流を見ると1位・2位が **LC4 / LPLC2** = 迫ってくる影の検出器。
つまり 視覚 → 巨大繊維 → 脚の筋肉 が一続きで見える。
```powershell
python scripts\05_explore_malecns.py # 全体サマリ
python scripts\05_explore_malecns.py --search pC1 # 細胞型を検索
python scripts\05_explore_malecns.py --type DNp01 # 上流/下流
python scripts\05_explore_malecns.py --dimorphic # 性的二型のニューロン
```
### hemibrain / FlyWire
```powershell
python scripts\01_explore_hemibrain.py --type MBON01 # 学習出力ニューロンの上流/下流
python scripts\03_flywire_annotations.py --nt # メス脳の神経伝達物質の内訳
```
### ノートブック
```powershell
jupyter lab
```
- `notebooks\01_first_touch.ipynb` — hemibrain。3D表示 → 配線表 → MBONの上流 → 最短経路。
「フェロモン入力 DA1_lPN から学習出力 MBON01 まで」を引くと `DA1_lPN → KCg-m → MBON01` と
教科書通りキノコ体を経由する経路が出る
- `notebooks\02_malecns.ipynb` — Male CNS。逃避回路、求愛回路(pC1)、性的二型、
FlyWire(メス)との細胞数比較。**実行済みの出力付き**
---
## ビジュアル (out/ に出力)
| スクリプト | 出力 | 中身 |
|---|---|---|
| `02_neuron_3d.py` | neurons_3d.html | hemibrain のニューロン + 脳メッシュ |
| `06_escape_circuit_3d.py` | escape_circuit_3d.html | 逃避回路の骨格。青=LC4/水色=LPLC2 → 赤=DNp01 → 緑=TTMn |
| `07_synapses_3d.py` | synapses_3d.html | hemibrain の実測シナプス点群 (水色=入力/赤=出力) |
| `08_malecns_synapses_3d.py` | malecns_synapses_3d.html | MaleCNS の実測シナプス。`--partner TTMn` で接触点を強調 |
| `09_escape_animation.py` | escape_animation.html | **▶再生**: 実際の軸索に沿って信号が脳→神経索へ降りる |
| `10_fly_walk.py` | fly_walk.mp4 | 物理エンジンの中でハエが歩く (三脚歩行) |
| `11_escape_takeoff.py` | escape_takeoff.mp4 | **回路が指した動作を体にやらせる**: DNp01発火→中脚伸展→跳躍 |
| `12_flight.py` | flight.mp4 / flight_plain.mp4 | **飛ぶ**: 羽ばたきで体重ぶんの揚力を出す。`--aero plain` は比較 |
| `14_render_flight.py` | flight_learned.mp4 | 学習した飛び方 (out/flight_policy.json) をそのまま飛ばす |
| `18_fly_scene.py` | fly_meadow.mp4 / fly_scene_*.mp4 | 草むらを飛ぶ。ヨー角速度を複眼 / 真値 / 無しで比べる |
| `19_compare_vision.py` | vision_compare.mp4 | 外乱に対する視覚フィードバックの有無を3条件で並べる |
| `20_forage.py` | forage.mp4 | 遠くは匂い、近くは視覚で 60cm 先の花へ |
| `21_choose.py` | choose_smell.mp4 / choose_nosmell.mp4 | 見た目が同じ2つの花を匂いで選ばせる試み (旧実装。結果は再現しない — 下記参照) |
| `23_synapse_movie.py` | synapse_fire.mp4 | 逃避回路の実測シナプスが1個ずつ光る (脳だけ) |
| `24_escape_full.py` | escape_full.mp4 | 逃避を3つ並べて — 脳のシナプス / 体の動き / ハエの視界 |
| `25_forage_brain.py` | forage_brain.mp4 | 花に近づく脳 — 匂い (触角葉) と好み (外側角)。キノコ体は暗いまま |
動画は **作った時点の姿で固まる**。どれが今のコードの姿かはアプリの動画タブが
スクリプトの更新時刻と突き合わせて出す (`python run_app.py`)。そのときの実測値は
`out/logs/<動画名>.txt` に残る。作り直す前の版を残したいときは `out/archive/` へ
退避してから回すとよい。
```powershell
python scripts\09_escape_animation.py # 信号が降りるアニメ
python scripts\08_malecns_synapses_3d.py --partner TTMn # GF→TTMn の90シナプス
python scripts\11_escape_takeoff.py # 跳躍 (体長の73%浮く)
python scripts\10_fly_walk.py --seconds 2 # 歩行
```
### 回路から動作まで一続きで分かること
`--partner TTMn` で出る **90シナプス**(全部 LTct 内)が Giant Fiber と中脚運動
ニューロンの接触点。TTM は中脚の筋肉なので、`11_escape_takeoff.py` で中脚だけを
蹴ると体長の 73% 浮くが、`--with-hind` で後脚も一緒に蹴ると **かえって低くなる**
(+1.83 → +1.67 mm)。解剖の通りになる。
## ハエの体 (NeuroMechFly v2)
`flygym` = マイクロCTから作られた実物大のハエの3Dモデルを MuJoCo で動かすもの。
6本脚・66関節。初回実行時にメッシュ (約200MB) を自動ダウンロードする。
**つまづきやすい点**: 位置アクチュエータの `kp` を上げないと (既定値では)
関節が指令に全く追従せず、その場で震えるだけになる。ここでは `kp=45` を使っている。
いま世界でやられているのは、この体に**コネクトームをそのまま繋ぐ**こと:
- **NeuroMechFly v2 / flygym** — 体の側。ここに入れている
https://neuromechfly.org
- **Flyvis** (TuragaLab) — FlyWire の視覚系の結線でネットワークを作り、
実際に記録された神経活動を予測できた (Nature 2024)
https://github.com/TuragaLab/flyvis
- **全脳埋め込みシミュレーション** — FlyWire 13.8万ニューロンのスパイキングモデルを
NeuroMechFly の体に入れて、見る・歩く・毛づくろい・逃げるを出させる試み
https://github.com/cobanov/awesome-fly (まとめ)
ここにある `11_escape_takeoff.py` は、その最小版
(回路の追跡結果を手で体の指令に翻訳したもの) にあたる。
---
## アプリ — ハエ脳ビューア
デスクトップアプリ (PySide6)。ブラウザは要らない。
```powershell
.\.venv\Scripts\python.exe run_app.py
```
タブは4つ。**「飛ばす」が主役**で、脳・体・視界を横に並べて同じ1本の
シミュレーションを走らせる。「回路」「動画」「五目並べ」はその次。
読み込みは3段に分けてあり、**2秒ほどで検索と 3D が使える**。残りは裏で追いつく。
| | 何を読む | かかる時間 | それが済むと |
|---|---|---|---|
| 1 | 注釈 21万ボディ | 0.3 秒 | 検索・骨格の 3D |
| 2 | 全結合 1億5185万 (1GB) | 1.4 秒 | 上流/下流の表・経路探索 |
| 3 | シナプス座標の行数 | 初回 1.8 秒 / 2回目以降は控えから | 見出しの数字 |
接続は **読みながら int32 に落とす** (bodyId は最大15.7億、weight は最大2591 で
どちらも収まる)。そのまま int64 で持つと 3.6GB、読み込みの峰が 7.4GB になる。
常駐はおよそ 2.2GB。
### 飛ばすタブ — 脳・体・視界を並べて走らせる
```
┌─ 脳 ──────────┬─ 体 ──────────┬─ 視界 ─────────┐
│ 点群 (OpenGL) │ MuJoCo の描画 │ 複眼の像 │
│ 領域ごとの明るさ│ 位置・姿勢・速さ│ 報酬 (花) の操作 │
└───────────────┴───────────────┴────────────────┘
```
3つのパネルは全部おなじ時刻の同じ走りから出ている。**脳が光るのは、その場所に
実際に入力が届いているときだけ** — 視葉は複眼に映った像、触角葉は匂いの濃度、
神経核は関節の動き。入力を作っていない領域は暗いままで、それが正しい。
キノコ体は何も学習していないので光らせない ([報酬はどこにあるのか](#報酬はどこにあるのか--キノコ体を光らせない理由))。
場面は2つ:
| 場面 | 何が起きるか |
|---|---|
| 飛ぶ | 草むらを飛び、匂いのする花に寄る。花はその場で置き直せる |
| 逃げる | ハエ叩きが迫り、視角がしきい値を超えたら跳ぶ。発火の前後は自動でさらに遅くなる |
**報酬は花**。「ここに花を置く」で風上 N cm・左右 M cm に置くと、プルームの源も
一緒に動く。だから置くと本当に振る舞いが変わる — 触角葉が光り、機体が向きを変え、
複眼の中で花が大きくなる。右下に出る内訳は `flight_env.reward_terms`、つまり
**学習 (`13_train_flight.py`) が使ったのと同じ式**。ただし **この場で学習はしない**。
飛び方は `out/flight_policy.json` の固定パラメータで、報酬を見て変わるものではない。
**必ずスロー再生になる**。物理の刻みは 2e-5 s、複眼の更新は 200 Hz で、実測は
実時間の 1/25 〜 1/100。何倍スローかは固定で書かず、**走らせながら実測して右上に出す**。
画質 (複眼の描画解像度 256 / 512 / 1028 px) で速さが変わる。
複眼の描画は既定で **影を切ってある**。この場面では影の計算が描画時間の9割を占め
(体の絵 320x240 で 418 ms → 47 ms)、影ありだと複眼1回が 855 ms かかって live に
ならない。動画 (`out/`) と同じ入力で見たいときは画質を「精細 + 影」にする。
この機械での実測 (飛ぶ場面):
| 画質 | 物理 | 複眼 200 Hz | 合計 |
|---|---|---|---|
| 軽い (256px) | x43 | x21 | 約 x65 スロー |
| 標準 (512px) | x40 | x41 | 約 x81 スロー |
| 精細 + 影 | — | — | 約 x500 スロー |
mujoco / flygym を入れていない環境 (固めた exe) では、このタブは理由を出すだけで
他のタブは普通に使える。
**回路タブ** — 左で細胞型を検索 → 選ぶと上流/下流がシナプス数順に出る。相手を
クリックすればそのまま辿っていける。右は 3D ビュー (OpenGL)。
| ボタン | すること |
|---|---|
| 骨格 | 選んだ細胞型の 3D 形状 |
| シナプス | 6.8GB を走査して実測シナプスを点群表示 |
| 接触点 | 「接触の相手」に入れた細胞型との接触シナプスだけ黄色で表示 |
| 運動ニューロンまで何段? | weight≥10 の接続で何ステップで筋肉に届くか |
| 経路探索 | 2つの細胞型の間の最短経路を求めて 3D に色分け表示 |
**動画タブ** — `out/` の動画を説明つきで再生する (ループ・こま送り・0.25〜2倍速)。
各動画には **生成スクリプトの更新時刻と突き合わせた「最新 / 古い」** が付く。
動画は作った時点の姿で固まるので、これが無いと今のコードの姿かどうか分からない。
「今のコードで作り直す」でその場から再生成でき、そのときのスクリプトの出力
(揚力が体重の何倍だったか、方位がどれだけ揺れたか) を `out/logs/` に残して
動画の下に出す。**数字を説明文に書かないのは、動画より先に古くなるから**。
**五目並べタブ** — 15x15 の自由形で蝿と打つ。相手は
[自分と打って勝ち負けだけから覚えた網](#五目並べ--勝ち負けだけで覚えた蝿と打つ)
(`out/gomoku_net.pt`)。学習と同じ読み (`gomoku_net.Player`) で打ち、読みは別スレッドで
回すので画面は止まらない。盤に重なっている色は**網が読む前に目だけで見た手の
確からしさ**そのもので、打ったあとに「どこを何回読んだか」と「その局面の見込み」を出す。
強さは学習のときに外のエンジンで測った値をそのまま出す。torch が無い環境 (固めた
exe など) では、前の打ち手 (`out/gomoku_policy.json`、numpy だけ) で打つ。
### 実行ファイルに固める
```powershell
.\.venv\Scripts\python.exe build_app.py # dist/ハエ脳ビューア/ に出る
```
`data/` (7.7GB) と `out/` は同梱しない。exe は自分の置き場から上へ辿って
それらを探す。別の場所に置くなら `FLY_ROOT` 環境変数で教える。
試すと面白い例:
- `DNp01` を選び、接触相手に `TTMn` → 接触90シナプスが全部 LTct にあるのが見える
- `DNp01` で「運動ニューロンまで何段?」→ 815個中774個に4段以内で届く。
1段が TTMn (跳躍)、2段に **DLMn / DVMn = 飛翔筋の運動ニューロン**
- `LC4` → `DNp01` → `TTMn` と辿ると、目から筋肉までが一本道でつながる
## 飛ばす
```powershell
python scripts\12_flight.py # 飛ぶ
python scripts\12_flight.py --aero plain # 比較: 落ちる
```
Janelia flybody (翅つき全身モデル) を 200 Hz で羽ばたかせる。翅の付け根の3自由度は
- **roll** = ストローク (前→後ろに扇ぐ) ← 主動力
- **pitch** = 迎角 (打ち返しで反転)
- **yaw** = 展開 (畳む↔広げる)
結果: 高度 1.00 → 1.63 cm、上昇速度 39.6 cm/s、正味空力は体重の 1.16 倍。
### そのままでは飛ばない — 埋めた穴が2つ
1. **メッシュ名の不一致**: `fruitfly.xml` は `wing_left_brown.obj` を参照するが、
配布メッシュは `l_wing_brown.obj`。rigging.yaml の `flybody_name` から
対応表を作って解決した (85個すべて対応)。[scripts/flybody_model.py](scripts/flybody_model.py)
2. **空力が無効**: 翅に楕円体の「流体ジオム」はあるのに `fluidshape="ellipsoid"`
が設定されておらず、羽ばたいても力が出ない。`geom_fluid` を立てて有効化した。
さらに翅アクチュエータは既定の設定では指令にまったく追従しない
(翅の armature が実効慣性を支配している)。位置サーボ化し `kp=50`、
タイムステップ 2e-5 で追従するようにした。
### 空力係数について (重要)
MuJoCo の楕円体流体モデルは、昆虫が揚力を稼ぐ **前縁渦 (LEV)** を表現しない。
- `--aero plain` (MuJoCo 既定): 体重の **0.79 倍** → 落ちる
- `--aero real` (Kutta揚力係数 3, blunt抗力 1.5): 体重の **1.43 倍** → 飛ぶ
後者は「昆虫の実測揚力係数は定常翼理論の2〜3倍」という事実に合わせた
粗い代表であって、前縁渦を解いているわけではない。両方入れてあるので比較できる。
羽ばたきパラメータ (周波数・ストローク振幅・迎角・位相の8個) は
正味空力の大きさを目的関数にしたランダム探索で決めた。見つかった最適値は
**200 Hz・ストローク振幅ほぼ最大** で、実際のショウジョウバエとほぼ同じだった。
## 飛び方を学習させる (報酬の的を少しずつ前に置くカリキュラム)
```powershell
python scripts\13_train_flight.py # 学習 (30コア並列, 約30分)
python scripts\14_render_flight.py # 学習した飛び方を録画
python scripts\14_render_flight.py --no-control # 制御を切ると宙返りする
```
手でゲインを合わせるかわりに CMA-ES で制御器16パラメータを探索する。
制御器は本物のハエの平均棍(haltere)反射と同じ構造:
ピッチ←ストローク中心、ロール←左右の振幅差、ヨー←左右の迎角差、
高度←振幅の増減。的を「その場→1cm前→3cm前→8cm前」と段階的に遠ざける。
### 到達した状態 (1.5秒飛行)
| 項目 | 値 |
|---|---|
| 高度保持 | 9.83 cm ± **0.03 cm** |
| ロールの振れ | **±0°** |
| ヨーの総回転 | **0°** (1.5秒) |
| 横方向のずれ | **-0.1 cm** (ほぼ直線) |
| 前進 | 24 cm / 1.5 s ≒ 16 cm/s |
| 羽ばたき | **210 Hz** (実物は約200 Hz) |
### まだ直っていない: 機首下げ約50度
本物のショウジョウバエは低速飛行で**機首上げ30〜45度**。この方策は
**機首下げ50度**で飛ぶ。ピッチ目標を振っても到達できる範囲は -32〜-75度で、
機首上げ側には入れない (それ以上振ると墜落する)。
原因はおそらく制御器の構造にある。いまは翅の展開角 (yaw) を**一定に固定**して
ストローク(roll)と迎角(pitch)だけを振っているので、翅先は単純な往復軌道を描く。
本物のハエは展開角も羽ばたき周期内で振って**8の字軌道**を描き、それで
ストローク面の向きを体の姿勢と独立に選べる。次にやるならそこ。
### 報酬設計で踏んだ失敗 (全部ログに残してある)
| 書き忘れたもの | 起きたこと |
|---|---|
| 目標高度 | 姿勢は完璧(傾き2°)だが揚力を捨てて落下。係留揚力 1.08→0.44 |
| 減点を有界にする | 「早く死ぬほど高得点」になり、飛べる解(報酬-4.4)を捨てて即墜落(+0.003)を選んだ |
| 機首方向 | 水平は保つがコマのように回転 (1秒で-242度) |
| 傾きの**向き** | 機首下げ50度で安定してしまう |
「報酬に書かなかったものは最適化されない」という同じ失敗を4回繰り返した。
## 体のモデルは正しいのか — 検証の記録
「飛ばない/不自然な飛び方をする」原因を、報酬をいじる前に体の側で突き止めた記録。
### 正しかったもの
| 検査項目 | 結果 |
|---|---|
| 全身質量 | 0.985 mg (実物 約1.0 mg) |
| メッシュ85件の割り当て | 語幹不一致 0件 (貪欲マッチは壊していなかった) |
| 重心の位置 | 翅の付け根の 281 um 後方・254 um 下 — 解剖学的に妥当 |
| 翅の長さ | 約 2.3 mm (実物どおり) |
### 間違っていたもの 1 — ストローク面が固定されていた
翅の付け根は3自由度 (ストローク・迎角・**展開角**) あるのに、展開角を
一定に固定してストロークと迎角だけ振っていた。すると翅先が単純な往復軌道
しか描かず、掃く面の向きが体に固定される。
機体を水平に係留して正味の空力を測ると **体軸の真上から34度ずれて後ろ向き**。
この力を鉛直にするには機体を傾けるしかない = 機首下げ50度の正体。
展開角も周期内で振ると向きを選べるようになる:
| 展開角の振幅 | 体軸zからの力の傾き |
|---|---|
| 0.0 (従来) | -34度 |
| 0.8 | -12度 |
| 1.2 | **+1度** (真上) |
### 間違っていたもの 2 — 空力係数を非対称に水増ししていた
Kutta揚力だけ3倍・blunt抗力だけ3倍という改変をしていた。これは力の
大きさだけでなく **向きまで歪める**。一様スケールに改めた (既定の2倍)。
掃く面を水平にすれば一様2倍でも最大 **2.35 体重ぶん** 出るので、
非対称な3倍は不要だった。
### 間違っていたもの 3 — 翅の慣性が armature で水増しされていた
元の XML は翅の関節に `armature=1e-6` を入れているが、これは翅自身の
ヒンジまわりの慣性 `2.3e-7` の **4.3倍**。数値安定化の項が実際の慣性を
上回っていた。実慣性に合わせると羽ばたきのパワーが半減する
(32,000 -> 14,700 erg/s)。`load_model(wing_armature=...)` で既定修正済み。
ただし 5e-8 まで下げると dt=1e-5 でも発散する。
### 残る本質的な限界 — 空力モデルが昆虫飛行のものではない
消費パワーを係留状態で実測した:
| 条件 | パワー | 揚力 |
|---|---|---|
| 学習後の羽ばたき (260Hz, 振幅1.25) | 14,700 erg/s | 1.04 体重 |
| 実物寄り (200Hz, 振幅1.0) | 3,200 erg/s | 0.58 体重 |
| **実物のショウジョウバエ** | **300-800 erg/s** | **1.0 体重** |
**実物の約4倍のパワーを使って6割の揚力しか出ない** = 効率が約7倍悪い。
MuJoCo の楕円体流体モデルは、昆虫が揚力を稼ぐ **前縁渦 (LEV)** や
**回転揚力** を表現しない。だから係数を水増ししないと浮かないし、
水増ししても力の **向き** は正しくならない。機首下げ姿勢はその帰結で、
報酬をどういじっても直らない (実際、体軸角ボーナスを入れても -57度のまま
動かなかった)。
**次にやるなら**: MuJoCo の汎用流体モデルを使うのをやめ、
Sane & Dickinson の準定常翼素理論 (並進・回転・付加質量の3項) を
自分で実装して、毎ステップ翅に外力として加える。体のモデルはそのまま使える。
## 空力を昆虫のものに入れ替えた
MuJoCo の楕円体流体モデルをやめ、**準定常翼素理論** (Sane & Dickinson 2002) を
実装した。翅をスパン方向に分割し、翼素ごとに3項を毎ステップ計算して外力で与える。
```powershell
python scripts\16_validate_aero.py # 検証
python scripts\16_validate_aero.py --sweep # 迎角と振幅を振る
```
- **並進力** — 力係数は Dickinson et al. (1999) がショウジョウバエの翅の
力学的スケールモデルで実測した値。前縁渦 (LEV) の効果込みで
`C_L` は迎角45度付近で **1.8** に達する (定常翼理論なら約0.9)。
- **回転揚力** (Kramer効果) — 打ち返しで翅がひねられるときに出る力。
- **付加質量** — 翅が押しのける空気の慣性。
### 都合の悪い物理も残す
翅だけを内蔵流体から外し、**胴体・脚・頭・腹部の空気抵抗は MuJoCo に残している**。
翅の流体ジオムを「楕円体モデル・係数ゼロ」にすると、そのジオムには力が出ず、
その body は内蔵の inertia-box モデルからも外れる。`opt.density` はそのままなので
胴体の抗力は生きる (実測: 100 cm/s 前進時の減速度は処理の前後で
-505.7 → -502.8 cm/s^2 とほぼ不変)。
翅にも抗力はかかる。迎角90度では `C_D = 3.46` で、揚力より大きい。
### XML の数値安定化パラメータが物理を壊していた
`fruitfly.xml` の翅関節の値は、この寸法にはどれも大きすぎた。
| 項目 | XML の値 | 実際の物理 | 影響 |
|---|---|---|---|
| `armature` | 1e-6 | 翅の慣性は 2.3e-7 | 実効慣性が4.3倍 → パワー倍増 |
| `damping` | 5e-4 | ほぼ無視できるはず | 1400 rad/s で 0.7 dyn cm の抵抗 → **パワーの大半をここで捨てていた** |
ダンピングを 5e-4 → 1e-5 にすると正味パワーが **2381 → 683 erg/s**。
揚力は変わらない。`load_model(wing_armature=..., wing_damping=...)` で既定修正済み。
### パワーの測り方
`sum |F v|` (絶対値) で測ると共振で戻ってくるぶんまで費用に数えてしまう。
本物の昆虫は胸部がバネとして働き慣性エネルギーを回収するので、
**正味の `sum F v`** で測るのが正しい。絶対値だと 9080、正味だと 2381 erg/s と
4倍近く違った。
### 検証結果
240 Hz・ストローク振幅 1.25 rad・迎角 50 度・機首上げ 50 度:
| | MuJoCo 内蔵の流体 | **翼素理論** | 実物のショウジョウバエ |
|---|---|---|---|
| 揚力 | 0.58 体重 | **0.951 体重** | 1.0 |
| 正味パワー | 3,200〜14,700 erg/s | **1,001 erg/s** | 300〜800 |
| 釣り合う姿勢 | 機首**下げ** 50度 | **機首上げ 50度** | 機首上げ 30〜45度 |
| 翅先の速度 | — | 230〜280 cm/s | 200〜300 |
| 迎角 | — | 46〜50度 | 約45度 |
**姿勢の符号が反転した**のが最大の変化。内蔵流体では正味の力が体軸から
後ろ向きにずれていて、機体を機首下げにしないと鉛直にならなかった。
翼素理論では前向きにずれるので、実物と同じ**機首上げ**姿勢で釣り合う。
報酬をいじっても直らなかったのは当然で、空力モデルの問題だった。
揚力 0.951 はホバリングまであと一歩 (羽ばたきの最適化余地)。
パワーはまだ実物の1.3〜3倍。
## 最適化はエネルギーの抜け穴を必ず見つける
消費パワーを報酬に入れた (`1/(1+(P/800)^2)` の省エネボーナス) ところ、
CMA-ES が **パワー -31,126 erg/s** の解を見つけてきた。静止空気から
エネルギーを取り出している値で、物理的にあり得ない。掘ると問題が2つあった。
### 1. パワーの測り方が間違っていた
`アクチュエータの力 x 速度` で測っていたが、これにはサーボの速度
フィードバック (ダンパー) の散逸が負の値として混ざる。翅は 1400 rad/s で
振れるので `kv * qd^2` だけで -27,600 erg/s になる。報酬が `max(P, 0)` だった
ため **「ダンパーを強く回すほど省エネ扱い」** という穴になっていた。
→ **翅が空気に渡したパワー** (`-sum(dF . v)`) を直接測るように変更。
定常飛行では必ず正になる量で、実物の 300-800 erg/s と直接比較できる。
### 2. 付加質量の項が本当にエネルギーを生んでいた
正しい指標で測り直しても -2,262 erg/s。項ごとに切り分けると:
| 方策 | 並進のみ | +回転 | **+付加質量** |
|---|---|---|---|
| 抜け穴を突いた解 | +424 | +518 | **-1,999** |
付加質量を「前ステップとの差分で法線加速度を出す**明示的な力**」として
与えていたため、迎角の急反転時に力が速度と同相になり、エネルギーを
注入できた。
→ 付加質量は本来ただの慣性なので、翼素ごとの `rho pi/4 c^2` を
ヒンジからの距離の2乗で積分して **armature に足す** 形に変更
(`WingAero.added_mass_inertia`)。値は 4.92e-8 = 翅の実慣性の 21%。
これならエネルギーを保存する。
### 確認
| | 空気へのパワー | 飛行 |
|---|---|---|
| 省エネ報酬なしの解 | +214 erg/s | 1.5秒飛行 |
| 抜け穴を突いた解 | +280 erg/s | **0.07秒で墜落** |
抜け穴を突いた解はエネルギー漏れに完全に依存していた。塞いだら飛べない。
**教訓**: 最適化に物理を渡すときは、まずエネルギー収支が閉じているかを
確かめる。「都合の良い物理」があれば必ず突かれる。
### 最終結果
| | 体軸角 | 空気へのパワー | 備考 |
|---|---|---|---|
| MuJoCo 内蔵の流体 | -53度 | (桁違い) | 宙返り→機首下げ |
| 翼素理論に交換 | -27度 | 207 erg/s | |
| + 省エネ報酬 | -2度 | **-2,262** | **物理違反** |
| + 抜け穴を塞ぐ | **-12度** | **+292 erg/s** | 3秒飛行 |
| 実物のショウジョウバエ | **+30〜45度** | 300-800 erg/s | |
3秒間、高度 9.42 cm を保って 55 cm 前進 (約18 cm/s)。
パワーは実物の下限あたり。姿勢はほぼ水平まで来たが、実物の機首上げには
届いていない。
## 脚をたたむ (実物の観察から)
実物のハエは飛ぶとき脚をたたむ。ところが `fruitfly.xml` の脚は位置サーボで
駆動されていて、**指令を 0 のままにすると脚を広げた着地姿勢で飛ぶ**。
長いことそれに気づかずに飛ばしていた。
しかも flybody には飛行姿勢が同梱されている
(`assets/model/flybody/pose/flight/yaw_roll_pitch.yaml`)。翅の角度が
neutral と同じだったので「中身が無い」と早合点していたが、実際は
**脚6本と口吻の角度が入っていた**。
`scripts/flight_posture.py` が、関節名の形式の違い
(`c_thorax-lf_coxa-pitch` と `coxa_T1_left`) を関節軸から対応づけて適用する。
### 効果
22関節に飛行姿勢を適用したときの変化:
- **重心が 134 um 上がる** (翅の付け根からの下方オフセットが 254 -> 120 um)
- **揚力が約 10% 増える** (脚の空気抵抗が減る)
| 機首上げ | 脚を広げたまま | 脚をたたむ |
|---|---|---|
| 47度 | Fz 0.874 | **Fz 0.959** |
| 55度 | Fz 0.876 | **Fz 0.966** (前後力が釣り合う) |
0.87 ではホバリングに届かなかったのが 0.96 でほぼ届く。脚の抗力は
無視できる大きさではなかった。
### ホバリングの姿勢は実物と一致した
検証済みの羽ばたき (240 Hz, 対称な迎角 +-50度) を脚をたたんだ状態で測ると、
**機首上げ 55度で前後力が釣り合い、揚力 0.966 体重ぶん**。
実物のショウジョウバエのホバリング姿勢 (機首上げ 30-45度) と同じ側・近い値。
内蔵の流体モデルでは機首**下げ** 50度にしかならなかったので、
- 空力を翼素理論に 入れ替える
- 脚をたたむ
の2つで、姿勢が実物と同じ側に来たことになる。
### ただし学習は別の解を選ぶ
カリキュラム学習 (8 cm 前の的へ飛ぶ) をかけると、迎角が非対称な羽ばたきに
移り、巡航姿勢は機首下げ 29度になる。前進を報酬にしている以上、
前に進みやすい解が選ばれる。ホバリングの釣り合いは機首上げなので、
物理は正しく、目的関数が別の飛び方を選んでいる状態。
## 風景を作って、複眼で飛ぶ
```powershell
python scripts\18_fly_scene.py # 複眼で針路維持
python scripts\18_fly_scene.py --yaw-source true # 真値を使う (比較)
python scripts\18_fly_scene.py --yaw-source none --disturb 6 # 何も使わない
python scripts\18_fly_scene.py --no-head-stab # 頭の安定化を切る
```
### 風景 (`scripts/world_scene.py`)
ハエの体長は 0.25 cm、飛行速度は 15-50 cm/s。数秒で 50-150 cm 進むので、
それに合う数十cm〜数mのスケールで市松模様の地面・遠くの壁・空を置いた。
最初は「色の違う柱40本」だったが、半径 1.2-3.5 cm ある柱はハエから見れば
木の幹で、**間を縫って飛ぶ**話にならなかった。草むらに作り直した:
| | 寸法 | ハエとの比 |
|---|---|---|
| 草の茎 260本 | 半径 0.03-0.09 cm、高さ 3-18 cm | 体(0.25cm)より細い |
| 葉 45枚 | 長半径 0.6-1.6 cm | 体の 3-6 倍 |
| 隙間 | 1-4 cm | 翼開長(0.5cm)の 2-8 倍 |
| 地面の市松 | 1 マス 2.5 cm | — |
| 花 | 直径 2.5 cm | — |
草は当たり判定を持たせてある (`collide=True`) ので、ぶつかれば落ちる。
つまずいた点: `texuniform=True` のとき `texrepeat` は「単位長あたりの
繰り返し数」。最初 60 にしていて 1cm に60マスとなり、遠目には無地に見えていた。
### 複眼 (`scripts/fly_vision.py`)
flybody の頭部には `eye_left` / `eye_right` がある (同梱の視野は140度だが、
読み込み時に170度へ広げる。下記)。
**最初の実装は3か所とも実物と違っていた**。どれも後から実測で分かった。
**1. 光学**
ショウジョウバエの個眼間角は約 5 度、受容角 (1個の個眼が見込む角、FWHM) も
約 5 度、片眼 約750個。最初の実装にはこのボケが無かった。
さらに「視野140度 / 96画素 = 1.46 度/画素」という見積り自体が誤りだった。
**透視投影では画素あたりの角度が一様ではない**。
| 描画96画素・視野140度 | 実際 |
|---|---|
| 視野の中央 | **3.28 度/画素** |
| 視野の端 | 0.40 度/画素 |
中央は個眼間角 5 度より粗く、周辺だけが 12 倍細かい。エイリアシングは
視野の周辺で起きていた。
**視野も狭すぎた**。実物のショウジョウバエは片眼で約180度を見ていて、左右で
ほぼ全周をおおう。同梱カメラの140度では横も後ろも見えていない。
透視投影では 180 度は作れない (tan が発散する) ので **170度 = ±85度** に広げた。
描画コストは解像度にほぼ依存しない (実測 252px 157ms / 1024px 167ms、
シーン構築が支配的) ので、広角カメラ1枚を高解像度で描いて畳み込めば済む。
| | 前 | いま |
|---|---|---|
| 視野 | 140度 | **170度** (±82.7度を見込む) |
| 描画 | 252x252 | 1028x1028 |
| 個眼 | 28x28 = 784 /眼 | **34x34 = 1,156 /眼** |
| 中央の角度刻み | 1.25 度/画素 | 1.27 度/画素 |
実物は片眼 約750個なので少し多い。正方格子で視野の四隅まで取っているぶん。
いまは各個眼のガウス型受容野を **角度の上で** 作り、受け取り行列 `M` にまとめて
個眼の像 = M @ 描画像 @ M.T
で一度に畳み込む。画素の見込み角を tan で求めるので透視投影の歪みも吸収される。
描画コストは解像度にほぼ依存しない (96px 155ms / 256px 174ms、シーン構築が
支配的) ので、中央でも受容角を表現できる 252 画素で描いてから畳み込んでいる。
結果は **28x28 = 784 個/眼**、受容野 FWHM 中央 5.11 度、個眼間隔 4.42-5.06 度。
**2. 動き検出**
「画像全体をずらして一番合う位置を探す」は実物のアルゴリズムではない。
正しい光学を入れると S/N が 0.50 → 0.03 まで落ちて使い物にならなかった。
実物は相関型検出器 (Hassenstein-Reichardt) で、隣り合う個眼の
**遅らせた信号と生の信号を掛け合わせる**。T4 (明) / T5 (暗) の演算がこれで、
LPTC (HS/VS) が視野全体で足し合わせている。入れ替えると戻った。
コントラスト利得制御も入れてある。相関型検出器の出力はそのままだと
**コントラストの2乗に比例**し、暗い茂みの前と明るい空の前で同じ回転が
桁違いの信号になる。実物の LPTC はほぼコントラスト非依存。
**3. 単位を持たせない**
以前は `ヨー角速度 [rad/s] ≒ -8.6 x 流れ` と、**物理エンジンの真値に対して
較正した係数**を使っていた。実物のハエは自分の真の角速度を知らない。
いまは出力を任意単位のまま出し、運動へのゲインは学習に任せる。
そもそも相関型検出器は角速度計ではない。角速度を4倍にしても出力は2.5倍にしか
ならない (圧縮特性)。実物も同じなので、換算係数を置くこと自体が間違いだった。
### 検証 — センサ単体で測る (`scripts/22_validate_eye.py`)
```powershell
python scripts\22_validate_eye.py
```
自由飛行の中で測ると、飛び方の良し悪しと視覚の良し悪しが混ざって切り分けられない。
物理を回さず、**既知のヨー角速度と前進速度で機体を運動学的に動かして**
複眼が何を出すかだけを見る。36条件 (ヨー9種 x 前進4種)。
| | 符号一致 | 相関 | 前進速度で向きが変わるか |
|---|---|---|---|
| **新** (受容角 + 相関型検出器) | **100%** | +0.961〜+0.978 | 変わらない (感度差1.2倍) |
| 旧 (鋭い像 + 画像ずらし) | 93.8% | 0.922〜0.984 | 変わらない (1.4倍)。**単調でない** |
相関だけ見ると差は小さい。だが **実際に効くのは別の指標** だった。
**まっすぐ飛んでいるのに「回っている」と誤報する量**:
| 前進速度 | 新 | 旧 |
|---|---|---|
| 10 cm/s | −5.3 deg/s | +13.7 deg/s |
| 20 cm/s | −2.6 deg/s | −13.7 deg/s |
| **40 cm/s** | **−0.6 deg/s** | **+96.8 deg/s** |
旧の複眼は 40 cm/s で直進しているだけで **+97 deg/s 回っていると報告していた**。
視運動反応はこれを打ち消そうとするので機体は必ず曲がる。しかも前進が速いほど
悪化する。飛行中に測った「落ち着いたあとも針路が 60-220 deg/s 揺れる」の正体が
これだった。制御ゲインではなく **センサが速度依存の嘘をついていた**。
### 一発の測定で比べてはいけない
この「偽の回転」を視野 140度 と 170度 で比べたとき、最初は
「広げたら 10 cm/s で −1.1 → −5.3 deg/s と悪化した」と読んだ。**誤りだった。**
出発位置を1つしか試していなかった。5箇所に散らすとこうなる:
| 視野 | 出発位置5箇所での偽の回転 [deg/s] | 平均 | 標準偏差 |
|---|---|---|---|
| 140度 | −2.2, −2.2, **−19.5**, +7.8, −3.9 | −4.0 | **8.8** |
| **170度** | −2.6, +1.6, −5.0, +7.4, −0.7 | **+0.1** | **4.2** |
**視野を広げるとばらつきが半分になり、平均もほぼゼロになる**。並進による流れが
左右で打ち消し合うには広い視野のほうが有利で、物理的にもそうなるはず。
ばらつきが 8.8 deg/s ある量を1点だけ測って数 deg/s の差を論じていた。
ついでに「正方格子の四隅が悪さをしているのでは」と疑って円形に切ってみたが、
10 cm/s では良くなり 20 / 40 cm/s では悪くなった。**一貫しないので採用していない。**
### 色覚 (`scripts/fly_color.py`)
花の検出をずっと `(赤 - 緑) > 0.12` と書いていた。**ハエの色覚に赤チャンネルは
無い**ので、カメラの RGB という人間側の都合を使っていたことになる。
ショウジョウバエの個眼は光受容器 8 個でできている:
| 光受容器 | オプシン | ピーク | 役割 |
|---|---|---|---|
| R1-6 | Rh1 | 約478 nm 広帯域 | 明暗と **動き**。ここは主に色盲 |
| R7 pale | Rh3 | 約345 nm (UV) | 色 |
| R8 pale | Rh5 | 約437 nm (青) | 色 |
| R7 yellow | Rh4 | 約375 nm (UV) | 色 |
| R8 yellow | Rh6 | 約508 nm (緑) | 色 |
個眼は pale 約30% / yellow 約70% がランダムに混ざる。**赤はほとんど見えない
かわりに紫外線が見える**。多くの花が持つ UV の蜜標はハエに見えてヒトに見えない。
動きを見る経路 (R1-6 → L1-L3 → T4/T5) が主に色盲なのは好都合で、
**相関型検出器を輝度だけで回しているのは実物に合っている**。色が要るのは
「それが何か」を決める側だけ。
実装は視物質テンプレート (Govardovskii et al. 2000) で感度曲線を作り、
材質の反射スペクトルから励起を積分する。描画は RGB の3枠に **(UV, 青, 緑)** を
積んで1回で済ませ、広帯域 R1-6 はそこから線形和で復元する
(7材質で最大誤差 0.0024 / 値域 0.024-0.656)。
花の判定は **(青+緑)/2 と UV の対立**。候補を比べてこれが花と葉をいちばん
離した (花 +0.571 / 葉 +0.35、土 +0.32、空 −0.10)。昆虫が花を見つけるのに
UV と可視の対立を使うのはよく知られた性質でもある。
| 前向きカメラに写る「花」 | ハエの色 (UV対立) | 旧 (赤−緑) |
|---|---|---|
| 花の 6 cm 手前 | 0.104% | 0.077% |
| 花に背を向ける | 0.000% | 0.000% |
| **草むら (花なし)** | **0.002%** | 0.009% |
誤検出が 4.5 分の1 になった。
**陰影を切って自己発光にするのが必須**だった。切らないと場面の照明で画素値が
0.03-0.05 まで落ち、8bit に量子化した時点でチャンネル比が壊れる
(花の前でも草むらでも対立指標の中央値が同じ 0.20 になり、判別できなかった)。
ここで計算している励起はすでに照明を含んだ反射率なので、描画側で
もう一度陰影をかける必要がない。
**仮定が残っているところ**: 草・土・空・花びらの反射スペクトルは MuJoCo が
RGB しか持たないのでこちらで与えている。形は植生や土のよく知られた特徴
(緑のピーク、土の単調増加、空の短波長優位、花びらの UV 吸収) に沿わせたが、
実測値ではない。
### 視界の見せ方 — 毎フレームの正規化をやめた
動画の視界パネルは、長いあいだフレームごとに最小・最大で 0-255 に引き伸ばしていた。
表示のたびにコントラストを作り直すので、**画面の明暗差が実際の明暗差と
関係なくなる**。暗い場面でも派手に見え、「ハエの視界はこんなに騒がしいのか」
という誤った印象を与えていた。
かわりに光受容器の順応を入れた。応答は Naka-Rushton 型
R = I / (I + sigma)
で、sigma は背景光にゆっくり追随する (時定数 0.25 秒)。背景と同じ明るさなら
R = 0.5。実物の光受容器がやっているのもこれで、**場面ごとに勝手な引き伸ばしを
するのとは別物**。順応の効き方も含めて画面に出る。
| 場面 | 順応あり (いま) | 毎フレーム正規化 (前) |
|---|---|---|
| 花の前 | 37-191 | 0-255 |
| 草むら | 41-192 | 0-255 |
| 空 | 37-184 | 0-255 |
前はどの場面でも必ず 0-255 いっぱいに伸びていた。
### まだ実物と違うところ
- 個眼が **正方格子**。実物は六角格子で、しかも眼の曲面に並ぶ
- オス前方の高解像度域 (いわゆる love spot) が無い
- 視野は 170度。実物の約180度には透視投影では届かない
### 頭の安定化について
光学を直す前は、視覚を使うと必ず墜落した。視覚推定値が **+15〜+20 rad/s** と
実際より桁違いに大きく、原因は羽ばたき (258 Hz) の揺れを視覚の更新 (200 Hz) が
拾うエイリアシングだった。実物のハエが首で頭を逆に振るのに倣い、
`head_abduct` (ヨー軸, ±0.2 rad) と `head` (ロール軸) で速い成分だけ打ち消した。
| | 羽ばたき帯 (258 Hz) の振幅 |
|---|---|
| 胴体のヨー | 2.16度 |
| 頭のヨー (安定化あり) | 0.58度 |
ただし **揺れは個眼1つ (5度) より小さい**。正しい光学なら本物のハエには
そもそも見えていない。この hack は、鋭すぎる像が作った偽のエイリアシングを
別の仕掛けで埋めていたことになる。光学を直した今も残してあるが、
不要かどうかは学習で決めるべき値。
### 針路維持 (t=0.5s にヨーへ 6 rad/s の外乱)
| ヨー角速度の取得元 | 頭の安定化 | 方位のばらつき | 到達距離 |
|---|---|---|---|
| 使わない | - | 91.4度 | 4.9 cm (その場で回る) |
| 物理エンジンの真値 | 入 | 22.1度 | 18.4 cm |
| 複眼のみ | 入 | 12.3度 | 17.8 cm |
| 複眼のみ | 切 | 60.0度 | -13.5 cm (後ろ向き) |
**この表は旧実装 (鋭い像 + 画像ずらし + 真値較正) の測定値**で、
光学を直した後では取り直していない。残してあるのは、頭の安定化を切ると
破綻するという当時の観察の記録として。
## 匂いで花を選ぶ
```powershell
python scripts\20_forage.py # 匂いを頼りに遠くの花へ
python scripts\21_choose.py # 見た目が同じ2つの花を匂いで選び分ける
python scripts\21_choose.py --no-smell # 匂いを無視 -> おとりに引っかかる
```
### なぜ嗅覚が要るか
複眼で花を捉えられるのは **8-10 cm 以内** だった (64px・視野120度で直径2.5cmの花)。
草むらの中を数十cm飛ぶには、視覚だけでは目標が存在しない。
実物のショウジョウバエも遠くの餌は匂いで探す。
`scripts/fly_smell.py` に、花から出て風下へ流れる匂いのガウス型プルームと、
左右の触角 (`antenna_left` / `antenna_right`) での検出を置いた。
風は **-x 向きに 8 cm/s**。花は +x にあるので、匂いを遡れば花に着く
(odor-gated anemotaxis)。
風速を実物なみの 25 cm/s にしたらこの機体 (巡航 17 cm/s) では遡れず後退した。
実物は 20-100 cm/s 出るので物理ではなく飛行性能の制約。
### 切り替えるのは間違いだった
最初の版 (`20_forage.py`) は「遠い→嗅覚 / 近い→視覚」と **排他的に切り替えて** いた。
これは実物と違う。目は常に開いているし、近づいても匂いは消えない。
役割が違うだけで同時に働いている:
| | 分かること | 分からないこと |
|---|---|---|
| 視覚 | そこに「何か」がある方向・形・位置 | それが好む花かどうか |
| 嗅覚 | 朧げな方向 (風上)、**好む花かどうかの判定** | 正確な位置 |
そこで `21_choose.py` では両方を常に計算し、**嗅覚が視覚の接近を許可する**形にした。
```python
pref = nose.preference("target") # (好む匂い - それ以外)/(合計) -1..+1
gate = max(pref, 0.0)
steer = k_odor * gate * left_upwind # 風上へ寄る
if seeing:
steer += -k_see * gate * see_bearing # 見えている的へ寄る
```
切り替え式では原理的に表現できなかった「**目に見えているが寄らない**」が、
これで出せる。`pref` は連続値なので、遠方で匂いが混ざるあいだは中間の値を取り、
近づくほど判定がはっきりする。
### 見た目が完全に同じ2つの花を置く
`world_scene.py` の `two_flowers=True` で、**同じマテリアルを共有する**花を2つ置く
(`_flower(..., make_materials=False)`)。視覚では区別できない。違うのは匂いだけ。
出発 (15, 0, 9)
おとり (42, -5, 9) 先に出会う。匂いが違う
本命 (66, +5, 9)
判定は「どちらの花が近いか」ではなく **4 cm 以内に入ったか** の絶対基準にした。
相対比較にしていたとき、まだ本命に着いていない途中経過が
「おとりに引っかかった」と誤判定されていた。
### ここまでで言えること / 言えないこと
旧実装 (鋭い像 + 画像ずらし + 真値較正した複眼) では、こう出た:
| | 本命への最接近 | おとりへの最接近 | 結果 |
|---|---|---|---|
| 嗅覚を使う (14秒) | 0.54 cm | 11.11 cm | 本命に到達 |
| `--no-smell` (8秒) | 7.68 cm | 1.15 cm | おとりに直撃 |
**この結果は複眼を直したら再現しない。** 同じ設定で走らせると本命 6.68 cm /
おとり 0.42 cm で、おとりに引っかかる。
軌跡を見ると理由が分かる。旧実装では序盤に y=+9.7 へ振れて本命のプルームに
乗ったが、新しい複眼では y=-12 へ振れておとり側に入る。**初期の流され方が
変わっただけ**で結果が反転する。試行 1 本で「嗅覚が効いた」と結論したのが
誤りだった。
もう一つ、設計の欠陥も見えた。`gate = max(pref, 0)` は「好まない匂いなら
寄らない」を正しく実現しているが、そのとき **操舵がゼロになるだけで離れる動きが
無い**。実際、おとりが視野の 19-32% を占める距離で `好み = -0.99`
(これは違う花だと分かっている) のまま 3.6 秒ほど周囲を回り続けた。
つまり現状で言えるのは:
言える … 匂いの情報だけで 2 つの花は区別できる (`preference` は
おとりの近くで -0.99、本命の近くで +1.00 を返す)
言えない … ハエがその情報を使って正しい花に行ける
後者を言うには、感覚運動層のゲイン (k_flow / k_odor / k_see / しきい値 /
時定数、約20個) を手で決めるのをやめ、**初期条件を振った複数試行で評価する**
必要がある。いまはどれも私が手で選んだ値で、学習していたのは羽ばたきと
姿勢保持の 18 個だけだった。
動画は旧実装のもの: `out/choose_smell.mp4` / `out/choose_nosmell.mp4`
---
## 迫ってくるもの (`scripts/escape_scene.py`)
最初はただの黒い球だった。ハエ側が使う情報は「黒い塊の視角が広がる速さ」だけ
なので機能としては足りるが、**見た目に意味が無く、何が迫っているのか分からない
絵**になっていた。平たい面と柄 — ハエ叩き — に変えた。
視角を決めるのは面の直径なので、正面から来るかぎり球とほとんど同じ大きさに見える。
比較が保てる:
| 距離 | ハエ叩き | 球 | 幾何 |
|---|---|---|---|
| 7.0 cm | 11.3° | 11.3° | 9.0° |
| 2.5 cm | 47.2° | 47.9° | 24.8° |
| 1.9 cm | 62.6° | 64.8° | 32.3° |
`shape="disc"` (実験室の標準刺激そのもの) と `shape="sphere"` (以前の版) も
残してある。
### 左右を足してはいけなかった
視角は暗くなった個眼の数から出している。これを**左右で足していた**。
視角は片眼から見た量なので、足すと正面の物体を二重に数えることになる。
視野を 140 度から 170 度に広げたとき両眼の重なりも広がり、幾何の値に対する比が
**1.4 倍から 2.0 倍へ跳ねて**気づいた。大きいほうを取るように直すと、
| 距離 | 視角 (複眼) | 幾何 | 比 |
|---|---|---|---|
| 3.5 cm | 24.6° | 17.9° | 1.38 |
| 2.5 cm | 33.4° | 24.8° | 1.35 |
| 1.9 cm | 44.4° | 32.3° | 1.38 |
| 1.4 cm | 58.4° | 42.9° | 1.36 |
比が 1.36-1.38 で揃う。1 を超えるのは受容角 5.1 度で縁がにじむからで、
実物の網膜でも同じことが起きる。
---
## 脳の絵を、入力から光らせる
`23_synapse_movie.py` と `24_escape_full.py` は、逃避回路の後ろに
**中枢神経系まるごとの点群** (45,000 点で 3億1183万シナプスを代表) を置いている。
この点群をどう光らせるかで 2 回やり直した。
| やったこと | なぜ捨てたか |
|---|---|
| 各シナプスを Poisson 過程で明滅させる | 乱数の点滅であって、生き物の脳ではない |
| 3次元の平面波を数本重ねた滑らかな明るさの場を張る | 滑らかにしただけで、やはりこちらが作った数字 |
どちらも画面に映っていたのは脳ではなく**こちらの細工**だった。いまは作らない。
**シミュレーションから出てくる入力をそのまま配る**:
| 領域 | 何で光るか |
|---|---|
| 視葉 ME / LO / LOP / LA / AME | 複眼に映った像 — 明るさ (光受容器の出力) と、像が動いた量 `\|photo - delayed\|` |
| 脚の神経核 LegNp(T1-T3)(L/R) | その脚の関節が動いた速さ (固有受容) |
| 触角葉 AL | 匂いの濃度。**この場面には匂い源が無いので暗いまま**で、それが正しい |
| それ以外 (中央脳の大半など) | この環境では入力を作っていないので暗いまま |
画面にもそう書いてある。「実際に入力がある所だけが、その入力で光る」が守っている線。
### 報酬はどこにあるのか — キノコ体を光らせない理由
逃避 (`24_escape_full.py`) は **脅威** の側で、巨大繊維はハエの脳で最も強い
単一の応答にあたる。報酬の側は別の場所が扱う。`25_forage_brain.py` は
見た目が同じ2つの花 (匂いだけが違う) に近づく場面で、そちらを描く。
| 場所 | 実物のハエで何を扱うか | このシミュレーションでどうするか |
|---|---|---|
| 触角葉 AL | 匂いが届いたこと | **光らせる** — 左右の触角の濃度を計算している |
| 外側角 LH | **生まれつき**の匂いの好き嫌い | **光らせる** — 好みの判定を計算している |
| キノコ体 + ドーパミン | **学習した**価値 | **暗いまま** |
キノコ体はドーパミンニューロンが運ぶ報酬信号と匂いをそこで結びつけて
「これは甘かった」を覚える場所で、**このシミュレーションは何も学習していない**。
どちらの花を好むかは `--like` で決めた定数なので、キノコ体を光らせたら
作り物に戻ってしまう。外側角なら、学習を要さない生まれつきの valence を扱う
場所なので、ハードコードした好みの置き場所として筋が通る。
言えること … 匂いが届いた (AL)、その匂いは好きな方だ (LH)
言えないこと … それに価値があると脳が判断した (キノコ体・ドーパミン)
**出発位置に注意**: 本命のプルームの中 (15, +5, 9) から出す。y=0 から出すと
近いおとりの匂いが勝って好みが -0.38 になり、`gate = max(pref, 0)` が閉じて
操舵がゼロになる。「好まない匂いなら寄らない」は実現できているが
**離れる動きが無い**ので、そのまま風 (-8 cm/s) に流されて墜落する
(下記「未解決の課題」)。
### 視葉への配り方 (retinotopy)
視葉は個眼の並びが柱の並びにそのまま保たれているので、葉の中での位置が
そのまま視野の中での向きにあたる。脳の座標系は点群から実際に確かめた:
x 右 -> 左 ME(R) x~17000 / ME(L) x~80000
y 背 -> 腹 SMP y~11900 / AL y~27600 / GNG y~40400
z 前 -> 後 AL z~15300 / 萼 z~33900 / 神経索 z~100000
使うのは y (仰角) と z (方位) の 2 軸だけ。x は視葉では**柱の深さ**
(ラミナ側からロブラ側へ層が重なる向き) で、同じ柱の中ならどの層も視野の
同じ点を見ているので落としてよい。
方位の向きは**外側キアズマで前後が入れ替わる**。網膜→ラミナは素通しだが
ラミナ→髄質で反転し、髄質→ロブラ (内側キアズマ) でもう一度反転して元に戻る。
なので ME / AME だけ前後を逆にしている。
検算: 捕食者を正面に置くと、左眼の像では列 25.4 / 右眼では列 1.6 (どちらも
前方の端) が光る。脳では ME(L) / ME(R) とも葉の**後ろ寄り**が、LO(L) / LO(R) は
**前寄り**が光る。左右が鏡映で、ME と LO が逆端 — キアズマの効き方と一致する。
**ここは想定**: 葉の中の位置を葉ごとのバウンディングボックスで正規化して格子に
均等に割っている。実際の柱の並びは一様ではないし、葉のどの隅が視野のどの隅かは
Male CNS の注釈からは読めない。向きの規則 (背腹は保存、前後はキアズマで反転)
だけが解剖の事実。
---
## 五目並べ — 勝ち負けだけで覚えた蝿と打つ
アプリの「五目並べ」タブで打つ相手。盤は 15x15 の自由形 (五以上並べば勝ち)。
飛翔・逃避と同じく **打ち方は教えない**。辿った道は3段ある:
| 版 | 蝿の目 | 読み | 学び方 | 着いた所 |
|---|---|---|---|---|
| 1 | 重み 159 個 (4方向で共有する受容野) | なし | CMA-ES、報酬4つ | 9x9 で素朴な相手に 12.5% |
| 2 | 同じ | 蝿自身が 1〜2 手 | CMA-ES、報酬を密に | 15x15 で素朴な相手に 15%、Rapfi に全敗 |
| 3 | 3x3 の畳み込み 7 層 (重み 5.7 万個) | 自分の目で木を読む | **自己対戦、勝ち負けだけ** | 先手で Rapfi の深さ3の段まで勝ち切る |
どの版も、読みに入れた規則は「五が並んだら勝ち」「石のある所には打てない」
「盤が埋まったら引き分け」だけで、「四は止めろ」「開いた三は強い」は一行も書いていない。
**これは学習した重みで、コネクトームの回路ではない**点はタブにも書いてある。
### 物差しは外のエンジン — Rapfi
自分で書いた相手 (連の長さを数える素朴な打ち手 `greedy_move`) だけで測ると、自分に
都合のよい物差しになる。外で鍛えられた **Rapfi** (Gomocup の自由形で優勝し続けて
いるエンジン、GPL v3) を物差しにした。**同梱はしない** — ソースを取ってきてこの機械で
ビルドする (`tools/` は git 管理外。手順は `scripts/gomoku_pbrain.py` の冒頭)。
読みの深さを 1 / 2 / 3 に制限した Rapfi を並べて**梯子**にし、どの段まで勝てるかを見る。
ビルドと接続で踏んだこと:
- **MinGW の GCC では AVX2 を切る**。既定のまま作ると、盤に石が1つでもある局面で
必ずアクセス違反で落ちた。空盤の初手と、読まずに決まる手 (五を作る・四を止める)
だけが通るので最初は気づかなかった。Windows 64bit の GCC は AVX の 32 バイト境界に
スタック変数を揃えない (古い既知の不具合)
- **Rapfi は考えている最中に届いたコマンドを黙って捨てる**。手を出してから「考え中」を
下ろすまでの隙間に次の盤が届くと、座標の行を別のコマンドとして読んで壊れた。
`ABOUT` に返事が来るまで待ってから次を送る
- **盤は「着手の列」として渡す** (`BOARD`)。Rapfi は並べた順を打った順として扱うので、
行の順に渡すと最後が自分の石のとき相手の番として考え始めた。自分と相手を交互に
並べ、相手の石で終える。パスを含む局面では落ちたので、**お手つきは負け**にそろえた
- **9x9 では NNUE (網の評価) が効かない**。自由形の重みが 9x9 に対応していない。
15x15 なら全力で打つ
### 9x9 は先手必勝ではなかった
最初は 9x9 で作った。「五目並べは先手必勝」は 15x15 で証明された話 (Allis 1994) で、
Rapfi どうしを空盤から打たせると盤の大きさでこう分かれた
(`out/logs/rapfi_selfplay.txt`):
| 盤 | 1手の持ち時間 | 結果 |
|---|---|---|
| 9x9 | 0.2 / 1 / 3 秒 | 引き分け (盤が埋まる) |
| 11x11 | 1 秒 | 引き分け |
| 13x13 | 1 秒 | 先手の勝ち (27 手) |
| 15x15 | 0.2 / 1 秒 | 先手の勝ち (33 手 / 25 手) |
9x9 では「先手が1手だけランダムに誤ったら後手が咎める」も、後手の Rapfi ですら
10 局中 1 局だった (15x15 なら 20 局中 12〜15 局)。盤を 15x15 に移した。
### 読みは蝿の中に置く — 別の打ち手を立てない
先読みを足すとき、最初は蝿の外に「五を作れるなら作る・四は止める・四の連続で詰ます
(VCF)」を先に処理する打ち手を立てた。**それは蝿ではない** — 実際に打っているのは
手書きの規則で、蝿の点数は末端の味付けにしか使われない。捨てて、読みを蝿の
`move` の中に置いた。読む候補を選ぶのも、読んだ先の局面を評価するのも蝿自身の目。
**学習も打つときも同じ関数を通す** (深さ0で覚えさせて深さ4で打たせたら別物になる)。
**お手つきは負けにした。** 最初は「石のあるマスを選んだら空振り (手番を失う)」に
していたが、読みを入れたら、評価の粗いうちは「何も置かない方がまし」と読んで
**わざと空振りを選んだ** (合法手の割合 95% → 55%)。本物の五目並べにパスは無い。
### 版2 — 自分の目で絞ると、見えないものは読めない
重み 159 個の目に 1〜2 手の読みを足し、報酬を密にして CMA-ES で登らせた
(`gomoku_env.py` / `30_train_gomoku.py`):
- 読む候補を「自分の点数の上位8マス」にしたら、**1つだけの五の脅威を塞げた割合が 0%**。
相手の勝ちのマスを高く見る目がないうちは、何手読んでもそこに行き着かない。
**石から2マス以内の空きマスを全部読む**ようにして 100% になった (範囲は盤の幾何
— どの五も既にある石の近くを通る)
- それで遅くなった分は、石を置くたびに点数が変わるマス (最大33) だけを計算し直し、
五が作れるかは「5マスの並び」(572 本) ごとの石の数で数えて取り戻した
(読み2で 1手 409 ms → 58 ms)
- 勝ち負けだけだと毎局すぐ負けて全員同点になるので、「決めた」(五を作れる手番で
作った割合)・「止めた」(1つだけの五の脅威を塞いだ割合) を審判の側で数えて足した
それでも素朴な相手に 15%、Rapfi の段は全敗で報酬が動かず止まった。盤を見ると、
**蝿は隅に寄って打っていた** — 端に寄れば相手の開いた四も作られにくい。
### 版3 — 畳み込みの網を、自分と打った勝ち負けだけで
AlphaZero の型に替えた (`gomoku_net.py` / `32_train_gomoku_net.py`):
- **目**: 3x3 の畳み込み 7 層 (32 チャンネル)。どの場所でも同じ重みを使うのは、
版1の「4方向で共有する受容野」を線から面へ広げたもの。出すものは「各マスに打つ
確からしさ」と「手番の側が勝ちそうか」。盤の大きさに依らないので 9x9 で立ち上げて
15x15 へ移した
- **読み**: その目で候補に重みをつけ、見込みで評価しながら木を読む (候補は石から
2マス以内)
- **学び**: 自分と打ち、「読んだ末の訪問の割合」と「勝ったか負けたか」を教師にする。
**手作りの加点は一つも無い**
学ばせる途中で踏んだこと:
- **自己対戦が「先手が5手で並べ切る9手の局」に落ち込んだ** (先手の勝ち 96%)。
網は「先に打った側が勝つ」を石の数の差から当てられるようになり (見込みの損失
0.99 → 0.18)、後手は塞いでも塞がなくても見込み -1 で、塞ぐ理由が無くなった。
**勝ち負けを決着までの手数で割り引いた** (k 手先の決着は ±0.95^(k-1))。
「すぐ負ける」より「まだ負けていない」方が値が高くなり、後手が塞ぎ始めた。
報酬は勝ち負けの一つのまま。落ち込んだときの網は `out/gomoku_net_linerush.pt`
- **学習した網の計算が 25 倍遅かった**。途中の値に非正規化数 (極端に小さい数) が
混ざり、CPU の浮動小数点が遅くなる。`torch.set_flush_denormal(True)` で
16 局まとめて 136 ms → 12 ms。それまでの数時間はずっとこの遅さで回っていた
- **torch のスレッドを多く立てると、混んだ CPU では遅くなる**。1局面ずつ読むときは
1 本 (4 ms。24 本だと 104 ms)、まとめて通す自己対戦は 4 本が最速だった
### いまの強さ (6.8 万局、約 8 時間の自己対戦)
`scripts/31_gomoku_match.py`。蝿は1手 400 回読む。全力の Rapfi は1手 0.3 秒。
**先手で勝ち切るか** (空盤から蝿が先手):
| 後手 | 版2 (重み159個) | 版3 3.5 時間 | 版3 8 時間 |
|---|---|---|---|
| 素朴な相手 | 負け | 勝ち | **勝ち (25 手)** |
| Rapfi 深さ1 | 負け | 負け | **勝ち (39 手)** |
| Rapfi 深さ2 | 負け | 負け | **勝ち (33 手)** |
| Rapfi 深さ3 | 負け | 負け | **勝ち (29 手)** |
| Rapfi 全力 | 負け (12 手) | 負け (18 手) | 負け (46 手) |
**1ミスを咎めるか** (全力の Rapfi が先手で1手だけランダムに打ち、蝿が後手):
| 後手 | 勝ち / 20 局 |
|---|---|
| Rapfi 全力 (届きうる上限) | 12〜15 |
| 版2 | 4 |
| 版3 3.5 時間 | 8 |
| 版3 8 時間 | 3 |
先手の攻めは梯子を3段登ったが、全力の Rapfi にはまだ届かない。後手で粘って咎める
力は、自己対戦が先手の勝ちに寄った (約 80%) 時期にかえって下がった。
**全力の Rapfi は揺れる**。持ち時間で読むので、計算機の混み具合で手が変わる —
Rapfi どうしでも 33 手で先手勝ち・41 手で先手勝ち・225 手で引き分け、と出た。
深さを制限した段は毎回同じ手を打つので1局で足りるが、全力相手は複数局打つ。
### ブラウザで打つ — `web/gomoku/`
同じ蝿を、Python もサーバも無しでブラウザだけで打てるようにした。網の計算
(畳み込み7層) と読みを JavaScript に書き直し (`web/gomoku/engine.js`)、重みは
`scripts/33_export_gomoku_web.py` で書き出す (224 KB)。外部の部品は使っていない。
- **torch と同じ手を打つ**。`?test` を付けて開くと、書き出しのときに torch が出した
値 (`golden.json`) と突き合わせる。8 盤すべてで網の出力の差は 1e-5 以下、読んだ末の
手も訪問回数も完全に一致した
- 読みは Web Worker で回すので画面は止まらない。開いたときに端末で網1回にかかる時間を
測り、1手が 3 秒以内に収まる読みの回数を初期値にする (学習で測った強さは 200 回)
- 置くだけで公開できる。Cloudflare Pages なら、このリポジトリをつなぎ、Build command を
空、Build output directory を `web/gomoku` にする
- 網を差し替えたら `33_export_gomoku_web.py` を回す。ファイルの URL に版の印 (`?v=…`) を
付け直すので、古い網や `engine.js` がブラウザのキャッシュに残って混ざらない
(手元の確認で一度そうなり、古い `engine.js` と新しい画面がぶつかって止まった)
### 9x9 の頃に分かったこと (版1)
- **自己対戦の解を無条件に採用しない**。自分のコピーと打つ段を通したら、素朴な相手
への勝率が 12.5% → 0.8% に落ちた。段ごとの解を残し、**同じ物差しで測り直して選ぶ**
(`30_train_gomoku.pick_best`)
- **大きくしたら弱くなった — ただし物差しによる**。隠れ素子を倍 (重み 315 個) にすると
素朴な相手に 3.3% に落ちたが、2つを直接当てると 315 個の方が 56% で勝った。
相手が変われば順位が変わる (`out/gomoku_policy_k12.json`)
- **報酬はここでも飽和した**。対ランダムの段は 30 世代で報酬 1.000 に張り付き、
その先は探索ではなく徘徊になる。伸びなくなったら次の段へ移す
---
## ディレクトリ
```
fly/
├─ .venv/
├─ data/
│ ├─ malecns/ Male CNS v1.0 (1.1 GB)
│ │ body-annotations-...feather 21万ボディの注釈(型/左右/性的二型/他DS対応)
│ │ connectome-weights-...feather 全結合 1億5185万本
│ │ syn-partners-...feather 全シナプスの3D座標 (6.8 GB)
│ │ body-neurotransmitters-...feather 神経伝達物質の推定
│ │ skeletons/ 個別ニューロンの骨格 (必要時に取得)
│ ├─ hemibrain/exported-traced-adjacencies-v1.2/
│ │ traced-neurons.csv / traced-total-connections.csv / traced-roi-connections.csv
│ └─ flywire/flywire_783_annotations.tsv 13.9万ニューロンの注釈
├─ notebooks/ 01_first_touch.ipynb 02_malecns.ipynb
├─ scripts/
│ ├─ 00_fetch_data.py データ取得 (--malecns で Male CNS も)
│ ├─ 01_explore_hemibrain.py
│ ├─ 02_neuron_3d.py
│ ├─ 03_flywire_annotations.py
│ ├─ 04_flywire_live.py FlyWire サーバから生データ (要トークン)
│ ├─ 05_explore_malecns.py
│ ├─ 06_escape_circuit_3d.py 逃避回路の3D
│ ├─ 07_synapses_3d.py hemibrain のシナプス点群
│ ├─ 08_malecns_synapses_3d.py MaleCNS のシナプス点群
│ ├─ 09_escape_animation.py 信号伝播アニメ
│ ├─ 10_fly_walk.py 歩行シミュレーション
│ ├─ 11_escape_takeoff.py 跳躍シミュレーション
│ ├─ 12_flight.py 飛行シミュレーション
│ ├─ flybody_model.py 翅つきモデルの読み込み(メッシュ名解決+空力有効化)
│ ├─ flight_env.py 飛行の評価環境と報酬
│ ├─ 13_train_flight.py CMA-ES + カリキュラム学習
│ ├─ 14_render_flight.py 学習した飛び方を録画
│ ├─ 15_bootstrap_flight.py 学習の出発点をランダム探索+CMA-ESで探す
│ ├─ insect_aero.py 準定常翼素理論の空力 (Sane & Dickinson)
│ ├─ 16_validate_aero.py 空力の検証 (揚力・パワー・姿勢)
│ ├─ 17_seed_gains.py 制御ゲインの初期値探し
│ ├─ flight_posture.py 飛行中の脚のたたみ方 (34関節)
│ ├─ world_scene.py 風景 (草むら・葉・花・地面・空・カメラ)
│ ├─ fly_vision.py 複眼のレンダリングと視覚性オプティックフロー
│ ├─ fly_smell.py 匂いのプルームと左右の触角
│ ├─ 18_fly_scene.py 複眼だけで針路を保って飛ぶ
│ ├─ 19_compare_vision.py 視覚の有無・頭の安定化の比較
│ ├─ 20_forage.py 匂いを頼りに遠くの花へ
│ ├─ 21_choose.py 見た目が同じ2つの花を匂いで選び分ける
│ ├─ fly_color.py ハエの光受容器 (Rh1/Rh3/Rh4/Rh5/Rh6) と花の見分け
│ ├─ brain_glow.py 点群と、そこへ感覚入力を配る対応づけ (アプリと共用)
│ ├─ escape_circuit.py 逃避回路の実測シナプスを描く3Dパネル
│ ├─ escape_scene.py 逃避の場面 (迫る捕食者) と、そこで採れる感覚の入力
│ ├─ 23_synapse_movie.py シナプスが1個ずつ光る動画 (脳だけ)
│ ├─ 24_escape_full.py 脳・体・視界を並べた動画
│ ├─ 25_forage_brain.py 花に近づく脳 (匂い・好み・翅)
│ ├─ gomoku_env.py 五目並べの盤・前の打ち手 (重み159個と読み)・報酬
│ ├─ gomoku_pbrain.py 外のエンジン (Rapfi) と Piskvork プロトコルで話す窓口
│ ├─ gomoku_net.py 蝿の目 (畳み込みの網)・読み (木探索)・自己対戦
│ ├─ 30_train_gomoku.py 前の打ち手を CMA-ES で覚えさせる
│ ├─ 31_gomoku_match.py Rapfi の梯子で測る (先手で勝ち切るか・1ミスを咎めるか)
│ └─ 32_train_gomoku_net.py 網を自己対戦の勝ち負けだけで覚えさせる
├─ app/ デスクトップアプリ (PySide6)
│ ├─ core.py コネクトームへの問い合わせ (UI から独立)
│ ├─ desktop.py 画面 (飛ばす / 回路 / 動画 / 五目並べ)
│ ├─ gomoku.py 五目並べの盤と、蝿の目の重ね描き (読みは別スレッド)
│ ├─ livesim.py 走らせながら見るシミュレーション (場面2つ)
│ ├─ livepage.py 脳・体・視界の3枚と、飛ばす / 報酬を置く操作
│ ├─ view3d.py OpenGL の 3D ビュー
│ ├─ player.py 動画プレーヤ
│ ├─ videos.py 動画カタログと「今のコードの姿か」の判定
│ ├─ theme.py / paths.py 配色とスタイル / data・out の在りか
│ └─ make_icon.py アイコンを描く
├─ web/gomoku/ ブラウザだけで打てる五目並べ (Cloudflare Pages などに置く)
├─ run_app.py アプリの入口
├─ build_app.py exe に固める (PyInstaller)
├─ out/ 生成した HTML
└─ requirements.txt
```
## Male CNS の注釈で使える主な列
`superclass` (descending_neuron / vnc_motor / ol_intrinsic など), `type`, `instance`,
`somaSide`, `somaNeuromere` (T1/T2/T3 = 前脚/中脚/後脚の節), `entryNerve` / `exitNerve`,
`dimorphism`, `fruDsx`, そして **`flywireType` / `hemibrainType` / `mancType`**
(他データセットとの対応付け。FlyWire とは 143,156 件が対応)。
内訳: 下行ニューロン 1,314 / 上行 1,846 / 運動 815 / オス特異的 1,258。
## もっと欲しくなったら
- **ブラウザで3D**: https://male-cns.janelia.org (bodyId 検索) / https://codex.flywire.ai / https://neuprint.janelia.org
- **FlyWire の生の接続データ**: 無料登録 → https://join.flywire.ai/ →
トークン https://global.daf-apis.com/auth/api/v1/user/token →
`python scripts\04_flywire_live.py --token <トークン>`
- **骨格(3D形状)**: Male CNS の SWC は `gs://flyem-male-cns/v1.0/segmentation/skeletons-malecns/`
- **BANC** (メスの脳+神経索, Nature 2026): https://doi.org/10.7910/DVN/7WTH1N
— 入れればオス(Male CNS)とメス(BANC)の全CNS比較ができる
## データの出典 (論文で使うときは要引用)
- Male CNS v1.0 — Janelia FlyEM / Google Research (CC BY 4.0)
https://www.janelia.org/project-team/flyem/male-cns-connectome
- hemibrain — Scheffer et al. (2020) *eLife* (CC BY 4.0)
- FlyWire 注釈 — Schlegel et al. / Dorkenwald et al. (2024) *Nature* (CC BY 4.0)