🏥 Production ML · ★★★ FEATURED
Whiff — A Real Fly's Smell-Learning Brain, in Your Browser
Flies that learn smells with a real fruit fly's wiring. I took the mushroom body out of the FlyWire connectome (2,785 real neurons per fly), put it in a browser, and let the flies learn from dopamine. The same circuit runs live on this page.
Every dot is a real neuron at its real FlyWire v783 position (a sample). Colours follow the afferent / intrinsic / efferent split FlyGM uses. The live circuit further down is the real wiring.
Overview
A few fruit flies in a little room with food dishes. They can smell the food but never see it. Each fly carries a real fruit fly’s smell-learning wiring, neuron for neuron, straight out of the FlyWire connectome. Teach one that smell A means sugar, let it go, and it heads for A. Nothing about how they move is scripted. Flying, following, fighting: all of it is left to training.
It runs entirely in the browser, phones included, with no server and no install. Open Whiff →
I built it after reading FlyGM, a 2026 paper that turned a whole fly brain into the controller of a simulated fly body. So this page covers three things: what those scientists actually did, what I did with the same data, and how every piece of Whiff works. The brain above is real neurons at their real FlyWire positions. The circuit further down is the real smell-learning wiring, running on this page right now.
First, the map: how scientists got a fly’s entire wiring diagram
A connectome is a wiring diagram: every neuron, and who connects to whom. The first complete one, for the 302-neuron worm C. elegans, came out in 1986. In October 2024 the FlyWire consortium published the first complete map of an adult fruit fly brain in Nature.
- The data. One adult female fly brain, about the size of a poppy seed, was cut into roughly 7,000 slices, each 40 nanometres thick, and imaged with an electron microscope. That’s about 100 teravoxels of pictures.
- AI did the first pass. Neural networks traced each neuron’s outline through all those slices, plus machine-learning models that detect synapses and predict which neurotransmitter each neuron uses.
- Humans fixed the rest. Hundreds of proofreaders from 50+ labs, citizen scientists included, corrected the AI’s mistakes by hand: about 33 person-years of proofreading.
- The result. 139,255 neurons, 54.5 million synapses, 149.2 metres of wiring. The map was led from Princeton (Sebastian Seung, Mala Murthy, Sven Dorkenwald), and Cambridge (Greg Jefferis, Philipp Schlegel) led the annotation of every cell into classes and types.
People immediately started running it. Shiu et al. (Berkeley, Nature 2024) simulated the whole brain as spiking neurons on a laptop and predicted feeding and grooming circuits. Whiff’s wiring file is built from their connectivity table plus the FlyWire annotations.
What FlyGM did, in simpler terms
FlyGM (“Fly-connectomic Graph Model”) is by Zehao Jin, Yaoye Zhu, Chen Zhang and Yanan Sui at Tsinghua University, published as a preprint in February 2026 (arXiv 2602.17997). Their question: can the real wiring diagram itself work as the “brain” of a simulated fly that has to walk?
Here’s the trick, stripped down:
- Every neuron becomes a node in a graph, every synapse an edge. An edge’s strength is the synapse count, made negative when the sending neuron is inhibitory (GABA, glycine) and positive when it’s excitatory. So the network’s shape isn’t designed by anyone. It’s the fly’s.
- Neurons are sorted by job, like the colours in the brain above: afferent (senses in), intrinsic (everything in between) and efferent (motor out).
- Each tick, the fly’s senses are squeezed into a small code and injected into the afferent neurons. Then every neuron collects the weighted sum of its inputs (graph message passing, literally
M = W × H), and one small shared network updates each neuron using that sum plus a learnable “personality” vector per neuron. The wiring stays fixed. Only those small pieces learn. - The efferent neurons get decoded into motor commands for flybody, a physics-simulated fruit fly body in MuJoCo, which then produces the next observation.
- Training in two stages. First it imitates an existing expert controller, then it’s fine-tuned with PPO reinforcement learning on the actual task.
flowchart LR
BODY["MuJoCo fly body<br/>legs · wings · physics"] -->|"observation"| ENC["Encoder<br/>squeeze senses to a small code"]
ENC --> AFF["Afferent neurons<br/>senses in"]
subgraph BRAIN["The real FlyWire wiring, fixed"]
AFF --> INT["Intrinsic neurons<br/>message passing M = W·H"]
INT --> EFF["Efferent neurons<br/>motor out"]
INT --> INT
end
EFF --> DEC["Decoder<br/>neuron states to joint commands"]
DEC -->|"action"| BODY
IL["Stage 1: imitate an expert controller"] -.-> BRAIN
PPO["Stage 2: PPO reinforcement learning"] -.-> BRAIN
What they found. The real wiring learned faster and steered better than every fake version with the same budget. Angle error in degrees (lower is better):
| Controller | Walk 2 cm/s | Walk 3 cm/s | Turn (ψ = 4) | Hard turn (ψ = 7) |
|---|---|---|---|---|
| FlyGM (real wiring) | 4.96 | 5.57 | 6.36 | 8.29 |
| Big plain network (MLP, more parameters) | 6.76 | 7.18 | 8.85 | 13.90 |
| Same wiring, shuffled edges | 7.84 | 7.77 | 9.72 | 13.55 |
| Random graph, same size | 12.11 | 11.33 | 17.45 | 125.36 |
It walked with the classic tripod gait, turned by shortening the inner legs’ strides, and flew. After training, sensory, central and motor neurons developed their own distinct activity patterns. A random graph trained the same way didn’t. Even with the synapse counts thrown away, the bare wiring still beat every baseline. The shape of the brain itself turns out to be a useful design for control.
Why I didn’t just copy FlyGM
I looked hard at reproducing it. The honest picture: the authors’ code lists the model checkpoints as a future release, training needs MuJoCo, PPO and serious compute, and a 139k-node graph stepping every frame is not a phone-friendly thing. I wanted something anyone could open on a phone and poke at.
So I asked a smaller question with the same data. FlyGM uses the whole brain for moving. I took one real circuit, the one for learning, and made it do its real job. That’s the mushroom body, the fly’s smell-learning centre.
What Whiff is made of: three layers, one of them real fly
| Layer | What it does | Where it comes from |
|---|---|---|
| World and body | Room, food, smell clouds, walking, flying, bumping, terrain | Plain physics code. Decides nothing |
| Smell brain | Learns which smell means food | Real fly wiring: FlyWire v783, right-hemisphere mushroom body |
| Movement | 20 senses in, 5 moves out: turn, speed, climb, fly/land, lunge | A small network I trained. Not biology |
“Smell A is good” comes from the fly brain. The turning, climbing and landing to get there come from the movement network, every frame.
The smell brain per fly: 53 receptor types (1,134 receptor neurons) → 139 projection neurons → 2,597 Kenyon cells ↔ 1 APL inhibitory neuron → 48 output neurons, with the real synapse counts. Each output neuron’s job (approach or avoid) isn’t hand-set either. It comes from which of the 165 dopamine neurons synapse onto it: the PAM cluster (“reward”) or the PPL1 cluster (“punishment”).
- Sparse code. The single APL neuron inhibits all the Kenyon cells, so only 2–13% of them fire for any smell. That’s why smells don’t blur together.
- Learning from surprise, not from food. Better than expected → PAM dopamine weakens the active Kenyon cells’ synapses onto the avoid outputs, so the smell now pulls the fly in. Worse than expected → PPL1 does the opposite. The other cluster lets synapses recover, which is what makes reversal learning work.
- Simplified bits, said plainly. Neurons are firing rates, not spikes. The antennal lobe’s local neurons are one normalisation step. Scents A, B and C are made up, but each one drives real receptor channels. The fly pheromone cVA uses its real channel, DA1.
LIVE CIRCUIT — try the real thing
This is Whiff’s actual mushroom body, the same class and the same wiring file, running in this page. Every dot is one real neuron. Every line is a real synapse bundle from FlyWire: receptor channels to projection neurons, projection neurons to the Kenyon cells they actually hit, Kenyon cells to the output neurons.
Try this: smell A while it’s naive (value 0, no opinion). Teach it. Smell A again: the red avoid outputs go quiet because their synapses got weakened, and the value swings positive. Now smell C. It has never been taught, but C shares three of A’s six receptor types, so it shares Kenyon cells and already reads about +0.44. Hit reverse and watch the memory flip. These numbers are pinned by tests against the app: after 20 pairings, A +0.91, C +0.44, B −0.33.
STAGE MAP — how Whiff runs, every frame
flowchart TB
subgraph WORLD["engine.ts: the world, physics only"]
ROOM["32 × 32 × 8 room<br/>mesas · ridges · rocks · dishes"]
SMELL["Smell domes per dish<br/>+ cVA left by eating flies"]
end
subgraph SENSES["Encoders: never coordinates"]
ANT["Two antennae<br/>strength + left/right contrast"]
OTHER["Fly-nearby sectors · touch · height<br/>hunger · speed · ground ahead"]
end
subgraph MB["mushroom-body.ts: REAL FlyWire wiring, 10 Hz"]
GL["53 receptor types"] --> PN["139 projection neurons"]
PN --> KC["2,597 Kenyon cells<br/>APL keeps 2–13% firing"]
KC --> OUT["48 output neurons<br/>approach vs avoid"]
DA["Dopamine: PAM / PPL1<br/>error = reward − prediction"] -.->|"weakens active KC synapses"| OUT
end
subgraph MOVE["policy.ts: artificial"]
NET["Movement network<br/>20 senses → 5 moves"]
end
BODY["body.ts<br/>walk · fly · land · lunge"]
VIEW["world.ts (Three.js / WebGL)<br/>inspector.ts (live circuit)"]
ROOM --> SMELL --> ANT --> GL
OUT -->|"learned value, left & right"| NET
OTHER --> NET
NET --> BODY --> ROOM
BODY -->|"tastes food with its feet"| DA
BODY --> VIEW
Two things I held to the whole way through:
- The fly only smells. The brain gets receptor activations. It never sees colours, dish positions, labels or who’s rewarded. The coloured scent overlay is for the audience only and is off by default.
- Food reaches the brain only as dopamine. A fly tastes by walking onto a dish (flies taste with their feet). Sugar is +1, bitter is −0.5, and that goes into the teaching rule as
reward − prediction. Nothing else ever says what’s good.
STAGE MAP — from a lab parquet file to your phone
flowchart LR
CONN["Connectivity_783.parquet<br/>Shiu et al. 2024"] --> PY["extract-mb.py<br/>pandas"]
ANN["FlyWire annotations<br/>Schlegel et al. 2024"] --> PY
PY --> JSON["mb-wiring.json<br/>≈120 KB gzipped"]
JSON --> VITE["Vite + TypeScript build<br/>typecheck · bundle"]
POL["train-policy.ts<br/>Augmented Random Search"] --> PJ["policy.json<br/>trained weights"] --> VITE
VITE --> VERCEL["Vercel static hosting<br/>push to main = deploy"]
VERCEL --> DEV["Your browser<br/>CPU: brains + physics · GPU: WebGL"]
DEV --> TIER{"Device timing"}
TIER -->|"fast"| FULL["Full brain<br/>2,785 neurons per fly"]
TIER -->|"phone"| LITE["Trimmed brain<br/>60% of Kenyon cells, 1,746 neurons"]
There’s no backend at all. No Python server, no API, no database, no model calls. The wiring loads lazily after first paint, every tab runs its own experiment, and the simulation advances on a fixed 1/60 s step so a slow phone just drops frames instead of changing the science. The site even times your device and shows how many real fly neurons it’s running: three flies on my laptop is 8,355 real fly neurons, live.
How the movement network learned to fly
The movement network gets 20 senses: what the fly brain thinks of the smell and which side it’s stronger on, smell strength, other flies ahead/left/right/behind, touch, height, climb speed, hunger, speed, and whether the ground about a body-length ahead is higher or lower. No coordinates, ever. It outputs 5 moves: turn, thrust, climb, fly/land intent and lunge.
I trained it with Augmented Random Search (evolution strategies): try lots of slightly jiggled copies across generated worlds and keep the direction that eats more. The reward is bites minus energy spent, plus potential-based shaping on the fly’s own smelled concentration of food. That kind of shaping can’t change which policy is best, only how fast it’s found. The curriculum goes low ledges first, then taller mesas mixed 50/50 with flat worlds, and 3–6 flies share one network (self-play). In the end the search only tuned the direct input → output weights, so it’s effectively linear, and I say that on the site too.
Nobody told them to fly. I put the food on a mesa that walkers can’t climb.
- Fly. They learned to take off and land on it and spend about 16% of their time in the air. Trained network: 1.36 bites per fly. A blank network and a random one: 0. They never get up there.
- Learn. With the floor dish gone, a fly taught “A = sugar” out-eats the untaught ones 4.5 bites to 1.8. That’s the real mushroom body doing its job.
- Follow. They trail each other about twice as often as chance, mostly by crowding the same food.
- Fight. They can lunge and knock a feeder off a dish. None of them learned it pays: zero lunges on held-out worlds, even in a scarce-food tuning run I tried and didn’t ship.
How learning works: one grid, three teachers
Whiff also has a teaching half (#learn) for people who’ve never seen a neuron. It’s one 4×4 grid of lights (65,536 possible patterns) taught three ways. Only the teaching signal changes, and every rule is rate × something × input:
| Teacher | The nudge | What it’s told | What the page shows |
|---|---|---|---|
| Tell it the answer (supervised) | rate × (answer − guess) × input | The right answer, every time | A knob board with draggable knobs. J vs backwards-J is learned in 2 rounds / 32 knob turns. “An L anywhere” is provably impossible for one board (20/26 best); six stacked boards reach 26/26 |
| Let it figure it out (unsupervised) | rate × (input − knob) | Nothing at all | Competitive learning finds 3 hidden shapes with no labels, and squashes a 1,457-colour sunset down to 3, 8 or 16 |
| Thumbs up, thumbs down (reward) | rate × reward × surprise × input | “Good” or “bad”, after it acts | REINFORCE. One thumb per guess reaches 99%+ in 9/9 runs; one thumb per five guesses stays stuck near a coin flip in 6/9 (credit assignment) |
The bridge back to the fly: nobody tells a fly which smell means food. It eats, and food is its thumbs-up. Dopamine is the teacher.
What’s real, simplified and made up
| Part | Status |
|---|---|
| Smell-learning wiring | Real. FlyWire v783 mushroom body, real synapse counts, valence from real dopamine inputs. MBON11 and MBON01 come out the way the literature says |
| Neurons and plasticity | Simplified. Rate neurons, one normalisation for the antennal lobe, APL settled by fixed-point iteration, error-driven dopamine, slow forgetting |
| Odours | Made up (A, B, C) on real receptor channels. cVA is real (DA1). Geosmin (mould) was tried on its real DA2 channel and dropped: the circuit can’t tell A from A+mould (0.99 similarity), because real flies use the lateral horn for that and Whiff doesn’t wire it |
| Movement | Artificial. Trained, not biological |
| Relationships | Counted, never scripted: trailed, ate-after, fought, bumped |
| Whole brain (opt-in) | Real wiring, simple neurons. 138,639 LIF neurons, fixed synapses, no learning; drives one fly’s bites (MN9) and escapes (giant fibre) |
| Appetite | Simple rule. Hunger thresholds, outside both brain models |
Limits I keep on the page itself: the learning circuit has no spikes; the whole-brain model has spikes but no learning or neuromodulation; no nerve cord, crude vision, no courtship. Every fly runs the smell-learning 2%; one fly at a time can hand its feeding and escape decisions to the whole brain.
Then I ran the whole brain
The mushroom body is 2% of a fly. So I went for the other 98%: all 138,639 neurons and the 2.7 million connections with at least 5 synapses, as the spiking model Shiu et al. used, running in a background thread of your browser.
- Build a slow one first. A plain numpy version updates every neuron every 0.1 ms. It reproduces the paper on FlyWire v783: with no input the whole brain is silent; sugar makes MN9, the neuron that sticks the proboscis out to eat, fire at 69–117 Hz; add bitter and it drops to ~2–6 Hz. Looming detectors (LC4/LPLC2) make the giant fibre, the escape neuron, fire at 70–146 Hz.
- Then a fast one. A brain at rest does nothing, so the browser engine only touches neurons something happened to. The clever bit: a nudged neuron that provably can’t reach threshold “sleeps”, and gets fast-forwarded in one closed-form step when its next input arrives. It matches the slow version spike for spike and runs 17–20× faster than real time. The wiring ships as a 7 MB gzip, CSR-packed with delta + varint encoding.
- Then it drives a fly. Link a fly and its feet feed the real taste neurons, and flies rushing at it feed the looming detectors. It only takes a bite when its MN9 fires, and when its giant fibre fires it jumps and flies off. Swoop at it and you’re watching simulated neurons make it escape.
- And I said what didn’t work. Smell fed into the whole brain runs away (~8,000 neurons, ~600,000 spikes a second, no left/right), so smell and learning stay on the mushroom body. That’s in the app too.
Flies also get full now. They used to eat every time they touched sugar. Now, a few bites in, they stop, get no kick from sugar, wander off, and come back hungry about 25 s later.
Engineering notes
- Two days, 59 commits. TypeScript, Three.js/WebGL, Canvas 2D, Vite. Python and pandas only for the one-time wiring extraction.
- Tests pin the science. 15 test files cover extracted neuron counts, sparse Kenyon coding, conditioning, saturation, reversal, freeze (no forgetting either), per-fly memory, 12-fly determinism, room bounds, one mouth per dish, and that the trained movement network beats blank and random ones on held-out worlds.
- Built to present. Up to 12 flies, editable rooms (drag dishes, rocks, scent drops, with undo), dark mode, a “Help me read this” layer, fullscreen with an iPhone fallback, and a scripted showcase sequence for a live talk.
- Performance. Circuits only walk the synapses of neurons that fire. The brain ticks at 10 Hz, the physics at 60, and phones get a trimmed brain picked by timing the device.
Credits: wiring from FlyWire v783 (Dorkenwald et al. 2024; Schlegel et al. 2024, Nature) via the connectivity file of Shiu et al. 2024. FlyGM by Jin, Zhu, Zhang & Sui, Tsinghua University (2026). The knob board goes back to Rosenblatt’s Mark I Perceptron (1958). The world, body, movement network, rate model and everything on screen are my own code.