**Flight-testing a fruit-fly connectome as an aircraft yaw damper**
Mutaqin Aryawijaya · ORCID [0009-0008-5199-2413](https://orcid.org/0009-0008-5199-2413)
[](https://fly.aryawijaya.com)
[](https://www.preprints.org/manuscript/202610.1232/v1)
[](https://fly.aryawijaya.com/#guide)
[](https://flywire.ai)
[](https://github.com/philshiu/Drosophila_brain_model)
[](pyproject.toml)
[](https://doi.org/10.5281/zenodo.23156035)
[](LICENSE)
[Live app](https://fly.aryawijaya.com) · [Paper](https://fly.aryawijaya.com/#guide) · [Preprint](https://www.preprints.org/manuscript/202610.1232/v1) · [Results](#key-results) · [Citation](#citation)
The replay workbench at t = 24 s: the fly-brain controller (A, orange) and the classical yaw damper (B, blue) fly the JSBSim Cessna 172 with the same turbulence seed (test seed 100), next to the brain's signal path, group firing rates and spike raster.
Code and data for **"Flight-testing a fruit-fly connectome as an aircraft yaw damper"**: a spiking model of the FlyWire v783 eye-to-steering pathway, evaluated as a rudder yaw damper with standard flight-control methods.
## Summary
We extracted from the FlyWire v783 connectome every neuron on a path of at most 4 hops (edges of at least 5 synapses) from the motion-detecting T4/T5 cells to the steering descending neurons DNa02 and DNg02, together with all edges among them (20,556 neurons, 251,358 simulated edges). The subcircuit was simulated with the leaky integrate-and-fire model and parameters of Shiu et al. (2024), with the fixed-rate input replaced by T4/T5 Poisson rates that follow the aircraft's yaw rate. The rudder command is a single gain on the washed-out difference of right and left DNa02 firing.
The controller was characterised as a flight-control law: open-loop frequency response (Phase 1), closed-loop gust rejection on a 2-state Dutch-roll model and on the JSBSim Cessna 172 (c172x) 6-DOF model in moderate MIL-spec turbulence (second-order Dryden on the 2-state model; JSBSim's first-order approximation on the 6-DOF model) (Phase 2), and degradation under neuron loss (Phase 3). Every result is compared against degree- and sign-preserving shuffles of the wiring, the airframe with no yaw controller and a classical washout yaw damper tuned with the same procedure and budget. Hypotheses, metrics, thresholds and seed splits were fixed before the main experiments; pilot-informed amendments are listed under [Protocol and amendments](#protocol-and-amendments).
In this model, the steering signal follows yaw rotation coherently and with the stabilizing sign. At 1 Hz the real wiring ranked first against 19 degree-preserving shuffles. Against rewirings that keep each connection's target cell type and side, and so leave every input to the HS, VS, H2 and DNa02 cells unchanged (about 60% of connections stay in place), it still ranked first, but only just. In closed loop it had the lowest mean RMS yaw rate of 11 wirings on both plants (p = 1/11, the floor with 10 shuffles). The fly controller reduces yaw-rate RMS by 14-19% relative to the bare airframe; the classical yaw damper reduces it by 52-57%. The lesion hypothesis (H4) could not be tested, because no flight departed controlled flight.
## Key results
Closed-loop and lesion values are from test seeds 100-119, which were held out from tuning; open-loop (H1) values use Poisson seeds 0-4. Brackets are 95% percentile bootstrap CIs over paired test seeds.
| Hypothesis | Measure | Result | Verdict |
| --- | --- | --- | --- |
| H1, open loop | 1 Hz coherence of DNa02 R − L with yaw rate: real vs 19 degree-preserving shuffles | 0.980 vs median 0.31, max 0.90; rank 1 of 20, p = 0.05 | Supported |
| H1, open loop | 1 Hz gain (Hz per deg/s): real vs 19 degree-preserving shuffles | 0.666 vs median 0.075, max 0.29; rank 1 of 20, p = 0.05 | Supported |
| H1, secondary | 1 Hz coherence / gain vs 19 type-preserving shuffles | 0.980 vs max 0.970; 0.666 vs max 0.61; rank 1 of 20, p = 0.05 | Small margin |
| H2, 2-state model | RMS yaw rate, fly − bare | -0.45 °/s [-0.53, -0.36] (-14%) | Supported |
| H3, JSBSim 6-DOF | RMS yaw rate, fly − bare | -0.73 °/s [-0.81, -0.64] (-19%) | Supported |
| H2b / H3 | RMS yaw rate, real − median shuffle (2-state; JSBSim) | -0.44 °/s [-0.53, -0.36]; -0.72 °/s [-0.80, -0.64]; lowest of 11 wirings on both, p = 1/11 | Supported, with caveat |
| Reference | RMS yaw rate, yaw damper − fly (2-state; JSBSim) | -1.15 °/s [-1.27, -1.04]; -1.51 °/s [-1.63, -1.39] | Damper better |
| H4, lesions | Departures in 1,040 lesion-study flights (960 lesioned, 80 intact) | 0 of 1,040; ΔAUC = 0 [0, 0] (degenerate: with no departures the bootstrap interval has zero width and carries no information) | Untestable |
Mean RMS yaw rate on the 20 test seeds (100-119), moderate MIL-spec turbulence (second-order Dryden on the 2-state model; JSBSim's first-order approximation on the 6-DOF model), 60 s flights:
| Controller | 2-state model | JSBSim 6-DOF |
| --- | --- | --- |
| No yaw controller (bare; 6-DOF keeps the common wings-level roll hold) | 3.09 °/s | 3.92 °/s |
| Classical yaw damper (K = 7.2 per rad/s) | 1.50 °/s | 1.69 °/s |
| Fly brain, real wiring (K = 0.0139 per Hz) | 2.65 °/s | 3.19 °/s |
| Best scrambled wiring | 2.91 °/s (fly_shuffle_02) | 3.31 °/s (fly_shuffle_08) |
The two gains are not comparable: the damper's K acts on yaw rate in rad/s, the fly's on DNa02 firing in Hz.
Further results:
- **Response shape (real wiring).** Coherence is 0.968-0.998 at every frequency ≤ 2 Hz (noise-only null 95th percentile 0.22-0.28). The phase is 177.0-179.9° at f ≤ 1 Hz, i.e. DNa02 R − L is in antiphase with the yaw rate. The gain is 0.666-0.686 Hz per deg/s up to 4.8 Hz and, relative to 1 Hz, -0.5 dB at 7.7 Hz and -4.4 dB at 12.5 Hz. The phase slope at f ≥ 2 Hz corresponds to an effective delay of about 22 ms at high frequency (21.6 ms; exploratory fit); it is a slope, not a measured latency.
- **Margins at the selected gains.** These are finite-amplitude injected return-ratio estimates on the two-state model (one seed, 17 injected sine frequencies plus the Nyquist frequency), used as a screening rule, not a stability qualification of the nonlinear, stochastic loop. Fly: nominal gain margin 34.5 dB, read where the phase crosses -180° and |L| is at the lock-in noise floor, so the value is imprecise; |L| < 1 at every tested frequency, so no gain crossover was found and no phase margin applies. Yaw damper: GM 31.9 dB, PM 61.3°. The fly loop is admissible because it is low-gain, not because it is fast.
- **Relay controllers.** On JSBSim, scrambled wirings 01, 02 and 08 selected K = 0.268, where the rudder command is saturated in about 100% of steps. They fly a steady skidding turn: mean RMS sideslip 7.1-7.4°, against 2.01° for the bare airframe, 1.73° for the yaw damper and 1.77° for the real wiring. Ranked by RMS sideslip (secondary), the real wiring is lowest of all 11 wirings on both plants.
- **Lesions (secondary, JSBSim).** Mean RMS yaw rate of the real wiring rises from 3.19 °/s intact to 3.47 °/s at 5%, 3.68 °/s at 20% and 3.85 °/s at 40% random silencing, and equals the bare value (3.92 °/s) from 60%. Its mean RMS yaw rate never exceeded the bare airframe's under the sampled lesions (one of 240 lesioned flights was slightly worse). Relay-type scrambles become worse than bare under some lesions (up to 5.49 °/s). Silencing the 6 HS cells returns the real wiring exactly to the bare value.
- **A single readout cell (JSBSim lesion study).** In the intact seed-100 replays of the real wiring, the right DNa02 fired no spikes in 60 s (the left fired at 2.55 Hz on JSBSim and 2.43 Hz on the 2-state model), so the rudder command came from the left DNa02 alone. Part of the early loss reflects random silencing of that single readout cell. At 5% random silencing it was silenced in 6 of 20 flights, against 1.0 expected; 5 of those 6 flights produced no rudder command and flew exactly like the bare airframe, while one (seed 103) still steered. Of the 37.7% of the benefit lost at 5%, 55% comes from those 5 switched-off flights; the other 15 lost 21% of theirs. The number of real-wiring flights identical to bare was 5, 5, 9, 15, 18 and 20 of 20 at 5, 10, 20, 40, 60 and 80% silenced.
- **Exploratory, input gain g = 4** (2-state only): fly − bare = -1.01 °/s [-1.17, -0.87].
## Figures

*Figure 1. Open-loop gain, phase and coherence of DNa02 R − L versus yaw rate: real wiring (blue), 19 degree-preserving shuffles (grey band), 19 type-preserving shuffles (green band), and a first-order washout yaw damper scaled for shape (dashed).*

*Figure 2. JSBSim c172x 6-DOF in moderate MIL-spec turbulence (JSBSim's first-order approximation of the Dryden model): RMS yaw rate per held-out test seed for each controller, paired differences with 95% CIs, and yaw-rate traces for test seed 100.*

*Figure 3. Lesions on JSBSim: probability of no departure versus random-lesion fraction (H4), mean RMS yaw rate versus lesion fraction, and targeted lesions, for the real wiring and three shuffles at their fixed Phase 2b gains.*
The 2-state closed-loop figure is `results/fig_phase2a.png`; the exploratory amplitude sweep is `results/fig_phase1_amp.png`.
## Interactive replay and paper
The `app/` folder is a static three.js viewer with no build step. It replays recorded flights (real wiring, yaw damper, shuffled wirings and lesioned controllers with the same turbulence seed) next to the group firing rates and spike raster that produced them, and contains a typeset write-up of the study.
```bash
npx serve app # then open the printed URL
```
Alternatively, open `app/index.html` directly; the recordings are also bundled in `app/runs/bundle.js` for `file://` use.
### Recording format
One JSON file per flight in `app/runs/` (≤ 5 MB), listed in `app/runs/manifest.json`:
| Key | Content |
| --- | --- |
| `meta` | controller, aircraft (`c172x`), plant (`lin2` or `jsbsim`), seed, turbulence, lesion `{kind, fraction}`, gain `K`, `dt`, `data_version` (`flywire_v783`), creation time |
| `t` | time (s) |
| `pos_ned_m`, `euler_rad` | position (north, east, down; m) and Euler angles (φ, θ, ψ; rad) |
| `r`, `beta`, `rudder`, `gust` | yaw rate (rad/s), sideslip (rad), rudder command in [-1, 1] (+ = nose-right, 1 = 16°), gust velocity [u, v, w] (m/s) |
| `groups` | mean firing rate (Hz) per control step for T4T5, HS, VS and DNa02, left and right |
| `raster` | `neurons` (index, cell type, side) and `spikes` as `[t_s, index]` |
| `metrics` | `rms_r`, `rms_beta`, `max_abs_phi`, `departed`, `rms_rudder` |
`scripts/make_mock_runs.py` validates this format and rebuilds the bundle.
## Repository layout
```
.github/workflows/sweep.yml sharded sweep on GitHub-hosted runners (one matrix job per shard)
app/
index.html, app.js replay workbench (three.js)
guide.js, paper.css typeset write-up shown below the workbench
brain3d.js 3D view of the FlyWire neuron positions
runs/ flight recordings (seed 100), manifest.json, bundle.js
data/ summary.json (every number the write-up shows, with its source), brain3d.*
data/
sub/subcircuit_v783_t5.npz extracted subcircuit (derived from FlyWire v783; enough for every sweep)
raw/ FlyWire / Shiu source files (not distributed; see below)
results/ result files and figures; shards/ holds per-job outputs
shards_superseded/ Phase 1 shards from the mis-built null (kept, not reported)
scripts/
00_pathway_check.py does the T4/T5 -> HS/VS -> DNa02 pathway exist in v783
brain.py subcircuit extraction, time-varying input, Shiu LIF model in Brian2, fly controller
plant.py JSBSim c172x trim and linearisation, 2-state plant, Dryden gusts, 6-DOF wrapper, loop margins
baselines.py bare airframe, washout yaw damper, shared tuning procedure
nulls.py degree-preserving and type-preserving shuffles, lesion sets
stats.py lock-in transfer function, coherence, bootstrap CIs, permutation tests, departure AUC
run_phase{1,2,3}.py sharded experiment drivers
analyze_phase{1,2,3}.py merge shards, apply the fixed tests, write results and figures
validate_fullbrain.py subcircuit vs full-brain check (needs data/raw)
subcircuit_stats.py HS input composition, edge totals, net signed weight per null
export_app_summary.py numbers for the write-up -> app/data/summary.json
export_brain_points.py neuron positions -> app/data/brain3d.* (needs data/raw)
make_mock_runs.py recording-format validator, bundle builder, mock runs for viewer development
run_toy.py 2-shard toy job for testing the workflow
pyproject.toml, uv.lock Python environment
```
## Reproducing the results
### Environment
Python 3.11 managed with [uv](https://docs.astral.sh/uv/). Pinned in `uv.lock`: Brian2 2.9.0, JSBSim 1.3.1, numpy < 2.3 (Brian2 2.9.0 fails on numpy 2.4).
```bash
uv sync
```
### Data
The committed subcircuit `data/sub/subcircuit_v783_t5.npz` is sufficient for all sweeps and analyses. The raw files are needed only to rebuild it (`scripts/brain.py --extract`), for the pathway check, the full-brain comparison (no 5-synapse cutoff, all 12,246 T4/T5 sources) and the 3D brain export. Download them into `data/raw/`:
```bash
mkdir -p data/raw && cd data/raw
# Shiu et al. 2024 model repository, commit 91bdd1e (FlyWire v783 connectivity)
curl -LO https://raw.githubusercontent.com/philshiu/Drosophila_brain_model/91bdd1e7dcf193f3e7ca5a8933497fcef63b7960/Connectivity_783.parquet
curl -LO https://raw.githubusercontent.com/philshiu/Drosophila_brain_model/91bdd1e7dcf193f3e7ca5a8933497fcef63b7960/Completeness_783.csv
# FlyWire neuron annotations (Schlegel et al. 2024), supplemental file 1
curl -L -o flywire_annotations.tsv \
https://raw.githubusercontent.com/flyconnectome/flywire_annotations/main/supplemental_files/Supplemental_file1_neuron_annotations.tsv
```
`Connectivity_783.parquet` has 15,091,983 rows; the annotation table has 139,248 neurons.
### Local self-checks
Every script ends with an assert-based self-check. Run one Brian2 process at a time.
```bash
uv run python scripts/00_pathway_check.py # -> results/00_pathway_check.txt (needs data/raw)
uv run python scripts/brain.py # spiking model
uv run python scripts/plant.py # plant, turbulence and sign checks (rewrites results/plant_*.json)
uv run python scripts/baselines.py # yaw damper and tuning (rewrites results/baselines_yaw_damper.json)
uv run python scripts/nulls.py
uv run python scripts/stats.py
uv run python scripts/run_phase1.py --smoke
uv run python scripts/run_phase2.py --smoke
uv run python scripts/run_phase3.py --smoke
uv run python scripts/analyze_phase1.py --selfcheck
uv run python scripts/analyze_phase2.py --selfcheck
uv run python scripts/analyze_phase3.py --selfcheck
```
### Cloud sweeps
The sweeps were run on GitHub-hosted runners via `.github/workflows/sweep.yml`. Each shard runs `scripts/