# MaleCNS Lab — circuit to motion A runnable first research prototype: **807 anatomically identified neurons, 21,161 directed measured connections, and 329,230 anatomical synapses**, extracted from MaleCNS v1.0. A sparse leaky integrate-and-fire (LIF) model drives a simplified six-leg body readout in a local experiment UI. This is a **real anatomical subset with assumed dynamics**. It is not a whole-brain emulation, recovered natural gait, or demonstration of an advance over published work. It implements the infrastructure for testing that research direction. ## Run locally Python 3.11 or later is recommended. From this directory: ```sh python3 -m venv .venv .venv/bin/pip install -r requirements.txt .venv/bin/python server.py ``` Open the URL printed by the server. It listens only on `127.0.0.1`; the operating system chooses an available port on first startup. The port is saved in `.runtime.json`, checked by binding on restart, and replaced with a free port if occupied. No other listener is stopped. In the original workspace, packages are already installed. Start with: ```sh ../../work/venv/bin/python server.py ``` The UI starts an 8-second experiment. Adjust the descending input, left/right bias, synaptic gain or sensory feedback. Run intact, VNC-silenced, left-DN-silenced, no-feedback, no-stimulus and shuffled controls. Replay, scrub, compare sessions and export the complete run as JSON. Exports include the exact parameters, anatomical source hashes, neuron IDs in firing-rate order, trajectory and sampled raster. The raster is deliberately capped and subsampled for export size; `neuron_hz` and total spike counts use all steps. The visual fly and plot replay saved results; dragging playback does not rerun the model. ## Repository and iterations The project is versioned with git and published with `tools/setup_repo.sh` (default `merolaagi/malecns-lab`, private; set `MALECNS_VISIBILITY=public` to change). Each new iteration arrives as a `malecns-lab*.zip` in `~/Downloads`. `tools/sync.sh` applies the newest one to `~/Sites/malecns-lab`, removes files the iteration dropped, leaves untracked local files such as `raw-data/` and `.venv` alone, runs the full test suite, commits with the top entry of `CHANGES.md` as the message, pushes, and moves the zip to `~/Downloads/malecns-lab-applied/`. It refuses to run over uncommitted local edits and commits nothing if tests fail. It also skips a zip older than the committed version and archives leftover older downloads. Files under `data/` are never deleted by a sync, because results generated on this machine (bridge, eye responses, quality) can be pushed after an iteration zip was built. After each successful sync the lab server is restarted in the background (`tools/lab.sh start|stop|restart|status|open|logs`; set `MALECNS_NO_RESTART=1` to skip). `tools/autosync.sh install` runs all of this automatically whenever a new zip lands in Downloads (macOS launchd agent, log in `~/Library/Logs/malecns-lab-autosync.log`); `tools/autosync.sh watch` does the same in a Terminal tab if macOS blocks background access to Downloads. Paths can be overridden with `MALECNS_PROJECT` and `MALECNS_DOWNLOADS`. ## What is measured Source: [MaleCNS v1.0 downloads](https://male-cns.janelia.org/download/). Original tables: neuron annotations, connection counts, and neurotransmitter predictions. Each source URL and SHA-256 is in `data/circuit.json`. The anatomical data is [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Attribution: FlyEM / HHMI Janelia, University of Cambridge, MRC Laboratory of Molecular Biology, and Google Research; see the [MaleCNS project and linked publication](https://male-cns.janelia.org/). The subset is selected by anatomy, before evaluating simulated behavior: 1. Six Traced DNa01, DNa02 and DNg13 descending neurons. 2. Up to 80 strongest VNC intrinsic/leg motor targets per seed, requiring at least five synapses for selection. 3. Leg motor targets of these selected cells with at least five synapses. 4. Up to 20 strongly connected proprioceptive sensory cells per leg and side, annotated as chordotonal organ or campaniform sensilla, in ProLN/MesoLN/MetaLN. 5. All positive measured connections among the selected cells, including counts below the selection threshold. There are 312 selected motor cells and 91 selected sensory cells. Truncating the circuit removes external inputs and feedback loops; this is a major scientific limitation. The small extracted dataset is bundled, so the application does not need internet or a neuPrint account. To reproduce it from official raw tables (~1.1 GB): ```sh .venv/bin/python fetch_data.py /path/to/raw-data # If tables are already downloaded as annotations.feather, weights.feather, # neurotransmitters.feather: .venv/bin/python build_dataset.py /path/to/raw-data ``` ## What is assumed **Neural dynamics:** 1 ms time step, 20 ms membrane constant, threshold 1, reset 0, two-step refractory counter, 10 ms synaptic trace, 50 ms firing-rate filter. The voltage update is `v += dt/tau * (-v + external + gain*W@syn + noise)`. All cells use the same dynamics; this is not cell-type-specific physiology. Synapses increment their source trace by one on a spike; trace decay is exponential. Noise is independent Gaussian current with standard deviation 0.015 per step. **Weights:** `log(1 + synapse_count)`, multiplied by an assumed transmitter sign and divided by total absolute incoming weight within the subset. Acetylcholine is positive; GABA and glutamate are negative inside the CNS. Other/unresolved transmitters contribute zero. These rules are explicit approximations: transmitter predictions do not fully determine receptor-specific effects. Counts are not measured physiological conductances. Normalization is computed before silencing, so ablations do not renormalize remaining inputs. **Stimulation:** selected descending cells receive current after 0.5 seconds. Pulse mode stops at 1.5 seconds. Target mode computes target bearing from exact world coordinates and injects a hand-designed signal into descending neurons. It does not model visual perception, smell, feeding, or food recognition. Right-side bias specifies stimulus side, not a guaranteed turning direction. **Body:** motor firing is pooled separately for each leg, using annotated side and leg subclass. Six engineered phase oscillators run at up to 5 Hz. Motor rate/50 sets oscillator speed and a bounded stride value. Mean left/right stride controls forward speed and rotation in an unbounded 2D plane. The fly drawing is a replay of those engineered cycles. There are no muscles, force balance, collision physics, or wing aerodynamics. **Feedback:** annotated sensory cells receive a hand-designed stance-times-stride current. The stance signal comes from the engineered oscillator; it is not measured load or joint angle. This closes a computational feedback loop without claiming a realistic proprioceptive model. **Default selection:** gain 10 was chosen after exploratory inspection at 7, 10, 14, 18 and 20 to produce visible, mostly unsaturated motor activity. It was not fitted to recordings or chosen to recover a biological effect. Feedback gain 2 was selected after inspecting 0, 0.6, 1.5 and 2 because lower values did not produce an observable feedback effect at the default synaptic gain. This is an engineering choice, not a physiological estimate. Higher gains can create excessive firing and saturate the body readout; the comparison table exposes clipping. ## Tests and experiments ```sh .venv/bin/python -m unittest -v test_model.py .venv/bin/python benchmark.py ``` The tests check data integrity, deterministic replay, silence enforcement, zero-gain motor isolation, negative controls, shuffled controls, transmitter handling, finite exports and input validation. They validate software behavior only. `data/benchmark.json` contains five seeds for each of six conditions at the default settings, plus gain sensitivity at 7, 10 and 14. These are exploratory model experiments, not biological accuracy measurements. The shuffled control permutes edge destinations, preserving source-associated weights/signs and destination in-degree multiplicity before sparse duplicate aggregation. It does not preserve cell-type or spatial structure. Use the same seed/settings for paired comparisons. ## Next scientific milestone The body is still an engineered readout, so the proposed scientific advance has **not** been achieved yet. The next bounded experiment is to choose one leg/circuit with verified muscle assignments and physiological recordings, fit cell dynamics on a training subset, then predict held-out activation/silencing responses. Replace the leg oscillator with a biomechanical joint and real proprioceptive encoding. Only then expand to coordinated six-leg walking, sensory navigation, and eventually flight. No autonomous learning, hunger, consciousness, biological fidelity, or novelty is claimed by this prototype. ## Circuit atlas (new) Open `/atlas` on the same local server. It reads the same `data/circuit.json` the simulator uses: all 807 cells and 21,161 measured edges, with no extra download. Three views share one side panel: - **Circuit:** the four layers with layer-to-layer synapse totals. The 398 VNC interneurons are shown as their own layer; the cell counts earlier in this README cover only descending, motor and sensory cells. - **Layer:** groups inside a layer (cell type, or developmental hemilineage for interneurons) and their strongest links. Selecting a group shows all of its inputs (blue) and outputs (red). - **Anatomy:** cells at their annotated soma positions, dorsal view, anterior up, fly's left on the left. Drag to pan, scroll to zoom, and shift-drag or use "Select region" to summarize any rectangle. Clicking a cell draws its measured partners and lists them by synapse count. Every panel reports how the simulator treats the selected cells and flags known modeling problems: non-leg-nerve motor neurons pooled into leg stride, cells whose unclear transmitter gives them zero weight, glutamate treated as inhibitory, and estimated positions. The URL hash records the selected cell, group or layer so a view can be reopened. Positions are measured soma locations except for the 91 leg sensory neurons (somata in the legs) and 8 other cells without an annotated soma. These are drawn as rings at the synapse-weighted centroid of their partners, a display convenience rather than anatomy. The scale bar assumes 8 nm isotropic voxels. Neuron morphology and synapse locations are not shown; they live in the separate MaleCNS segmentation data. The odor-learning circuit is not in the atlas yet because `build_learning.py` does not export soma positions. ```sh .venv/bin/python -m unittest -v test_atlas.py node --test atlas-core.test.js ``` ## Neuron workbench (new) Open `/neuron`, or use "Open in neuron workbench" on any cell in the atlas. Any cell in either circuit can be opened by body ID; "Random Kenyon cell" picks one from the learning circuit. The workbench loads the cell's measured presynaptic partners and synapse counts from `/api/neuron`, then simulates the membrane in the browser (`neuron-core.js`). - **Measured:** input partners, synapse counts, output partner count, and for locomotion cells the predicted transmitter that sets each input's sign (ACh +1, GABA and glutamate −1, unclear 0, as in the network model). PAM inputs to Kenyon cells are listed but not simulated. - **Assumed:** two compartments (a passive dendrite that receives all synapses, coupled to an active spike initiation zone, with the soma off the path as in unipolar insect neurons); squid Hodgkin–Huxley kinetics as a placeholder for Drosophila channels; inputs firing at a set rate with random timing; conductance per spike scaled by log(1 + synapses). The learning dataset has no transmitter predictions, so every PN input is treated as excitatory. The default strength (0.065) was chosen so a single claw of Kenyon cell 57729 rarely fires the cell and all seven usually do, matching the qualitative observation that Kenyon cells need several coincident claws. It is not fitted to recordings. For Kenyon cells, the workbench can feed the exact scalar and one-hot number codes used by the numerosity experiment. ## Numerosity experiment (new) Open `/numerosity`. The task is choosing the larger of two numbers, 1 to 9, with some numbers never shown in training. Numbers are encoded on the 314 measured PNs: every number switches on exactly 30 PNs, so total activity carries no size cue. The **scalar code** (an HTM-style scalar encoder over PNs in a random order) gives neighboring numbers overlapping PNs; the **one-hot code** gives each number disjoint PNs. The measured PN→KC wiring and 95th-percentile threshold form the Kenyon cell code, and a plastic pooled readout over the measured KC→MBON edges, gated by PAM→KC synapse totals, learns a value per stimulus. Each trial both options receive feedback: +1 for the larger, −1 for the smaller. With chosen-only feedback, values drift toward "good whenever I picked it" and no ordering forms. Saved benchmark (`data/numerosity-benchmark.json`, five seeds, P(choose the larger), chance 50%), for pairs where both numbers were held out: | Held out | Scalar code | One-hot code | Scalar, shuffled PN→KC | |---|---|---|---| | 2, 5, 8 (interpolate) | 100% ± 0 | 48% ± 2 | 100% ± 0 | | 4, 5, 6 (middle block) | 61% ± 22 | 49% ± 3 | 63% ± 18 | | 8, 9 (extrapolate) | 10% ± 5 | 53% ± 2 | 12% ± 9 | **Findings.** Overlapping codes let the readout interpolate between trained numbers; without overlap, held-out numbers stay at chance. Wide gaps weaken interpolation. Extrapolation fails systematically and is reversed: 9 shares fewer PNs with trained large numbers than 8 does, so it looks smaller. This is similarity-based generalization, not a learned rule of order. Shuffled PN→KC wiring performs as well as the measured wiring, so these results show nothing specific to the measured connectome. "One held-out number" accuracy is partly an artifact when held-out numbers are mid-range, because untrained values sit near 0, the midpoint of the ±1 outcomes. ```sh .venv/bin/python numerosity.py --benchmark .venv/bin/python -m unittest -v test_numerosity.py test_neuron.py node --test neuron-core.test.js ``` The next planned step is a successor ("counting") test with an HTM-style sequence memory on Kenyon cells, labeled as an added assumption because the fly mushroom body has no established equivalent of distal-segment sequence memory. ## Data quality layer (new) `build_quality.py` folds two more official MaleCNS tables into the lab: body statistics (synapse totals for every body, 780 MB) and per-synapse transmitter predictions (one row per pre-synapse, 2.7 GB). It streams only the rows for the lab's cells and writes `data/quality.json` with, for every cell in both circuits: - **Coverage:** the share of the cell's input and output connections that the lab's subset contains. A cell with low input coverage is simulated from a small slice of what drives it. - **Per-synapse transmitter evidence:** mean probability per transmitter, the share of the cell's synapses whose most likely transmitter is each one, and sign probabilities under the models' rule (acetylcholine +, GABA and glutamate −, others 0). The atlas and neuron workbench show both for every cell and flag low coverage, mixed evidence and cells whose aggregate transmitter is "unclear" but whose synapses mostly agree. In the workbench, inputs in the learning circuit take their sign from the MaleCNS per-body consensus transmitter instead of being assumed excitatory. **First results (MaleCNS v1.0).** Input coverage is low throughout the locomotion subset: descending neurons 0.5–2% (their drive comes from the brain), VNC interneurons median 15%, motor neurons 10%, leg sensory cells 4.5%. In the learning subset, Kenyon cells receive a median 27% of their input from the selected PNs and MBONs 55% from the selected KCs; PN and PAM inputs are not in the subset at all. Per-synapse predictions agree with the consensus for every non-motor locomotion cell, give 303 of 314 PNs as cholinergic (11 GABA/glutamate, mostly ventral-tract PNs), MBONs as 50 acetylcholine, 26 glutamate and 21 GABA, and PAM cells as dopamine. They fail systematically for two classes: 4,060 of 4,064 Kenyon cells come out "dopamine" and motor neurons mostly "histamine" or "acetylcholine", contradicting published physiology (cholinergic Kenyon cells, glutamatergic motor neurons). The consensus transmitter therefore remains the model's transmitter, and disagreements are flagged rather than applied. The network models themselves are unchanged in this step; transmitter predictions come from EM appearance, and receptor identity, including glutamate's real sign, is still not measured. ```sh cd raw-data && B=https://storage.googleapis.com/flyem-male-cns/v1.0/connectome-data/flat-connectome && curl -fL -o body-stats.feather $B/body-stats-male-cns-v1.0-minconf-0.5.feather && curl -fL -o tbar-neurotransmitters.feather $B/tbar-neurotransmitters-male-cns-v1.0.feather && cd .. && .venv/bin/python build_quality.py raw-data .venv/bin/python -m unittest -v test_quality.py ``` ## Brain regions (new) `build_regions.py` asks neuPrint (dataset `male-cns:v1.0`) for the region profile (`roiInfo`) of every traced neuron: how many of its input connections (post) and output connections (downstream) fall in each primary region. It writes `data/regions.json` with per-region totals, a region-to-region flow matrix and a region profile for every lab cell. Open `/regions` for the matrix: hover any cell for the connection (flow, neuron count, shares in both directions and the reverse flow), click a region to pin it in the panel. Regions are ordered by neuPrint's region hierarchy by default. Atlas cell panels and the neuron workbench show each cell's top input and output regions. **First result.** All six lab descending neurons receive their input mainly in the premotor regions LAL, VES, IPS and SPS and in GNG: DNa02 34% LAL and 20% VES; DNa01 36–40% VES and 26–28% GNG. DNg13 sends its output to the legs on the opposite side. These regions are what a brain input to the locomotion model would need to represent. Flow from region A to region B sums, over all traced neurons, the fraction of each neuron's input received in A times its output connections in B. Synapses sit inside one region, so this routes influence through neurons; it is not a count of synapses between regions. Only primary regions are used, since super- and sub-regions overlap and would double count. Requires a personal neuPrint token, kept in the environment and never committed (the repository is public): ```sh export NEUPRINT_APPLICATION_CREDENTIALS='' .venv/bin/pip install -r requirements-regions.txt && .venv/bin/python build_regions.py .venv/bin/python -m unittest -v test_regions.py ``` ## 3D view and Neuroglancer links (new) Open `/3d`. It draws neurons as official MaleCNS meshes and brain regions as translucent official region shapes, inside faint brain and VNC outlines. When a neuron's mesh cannot be loaded, it is drawn from its official skeleton instead and marked "skeleton". Presets cover the steering descending neurons, Kenyon cell 57729 and the regions where the descending neurons take their input. The page state is in the URL (`/3d?cells=10360,523769®ions=LAL(R),VES(R)`). Data comes from the public bucket `gs://flyem-male-cns`, the same source as the Neuroglancer view, through a whitelisted, cached proxy in the lab server (`gcs.py`, `/api/gcs/...`): only mesh, skeleton and region layers are allowed, paths are validated, files are capped at 25 MB and cached in git-ignored `data/gcs-cache/`. three.js r128 is bundled in `vendor/` (MIT licence included), so the page does not depend on a CDN. Every atlas cell, workbench cell and region panel also has **View in 3D** and **Neuroglancer ↗** links. The Neuroglancer link opens the official viewer with the cell selected and centred on its soma, together with brain and VNC outlines; region links filter the viewer's region list by name. All layers the viewer uses are in Neuroglancer's legacy precomputed mesh format (confirmed by `tools/probe_ng.py`, results in `data/ng-probe.json`), and region labels in the bucket match the lab's region names. Full-resolution neuron meshes can be very large (DNa02's single fragment exceeds 25 MB), so `meshes.py` assembles each mesh once on the server, simplifies it by vertex clustering to at most 250,000 triangles for neurons and 60,000–80,000 for regions and outlines, and caches only the simplified version; `/api/mesh//` serves it. Clustering keeps the overall shape and branching but loses fine surface detail. The first view of a large neuron takes a while; later views are immediate. Hover a neuron's status to see how far it was simplified. Run the one-time probe on a machine with internet access to confirm the formats of the layers the viewer uses: ```sh .venv/bin/python tools/probe_ng.py node --test ng.test.js && .venv/bin/python -m unittest -v test_gcs.py ``` ## Sparse expansion for continual learning (new) Open `/expansion`. This tests the mushroom body's design as a machine-learning component. Handwritten 8×8 digits (UCI, bundled as `data/digits.npz`) arrive a few classes at a time, and each model is tested on all classes seen so far with no task label (class-incremental learning). The fly-inspired model expands pixels through the measured MaleCNS projection-neuron → Kenyon-cell wiring (pixels drive projection neurons through a fixed random assignment), keeps the 5% most driven Kenyon cells, and learns with an associative readout that strengthens only the correct class's connections from active cells. Controls: shuffled wiring, random sparse wiring with the same per-cell in-degree, dense random projection, expansion without sparsity, the associative readout on raw pixels, an error-driven readout on the sparse code, softmax regression on pixels, and a backpropagation MLP with a similar parameter count. An MLP trained on all classes at once gives the upper bound. Saved benchmark (`data/expansion-benchmark.json`, five seeds, final accuracy on all classes / forgetting): | Model | Digits | Digits, 5 per class | Two-shape classes | |---|---|---|---| | Measured PN→KC, 5% active, associative | 89% / 5% | 80% / 6% | 80% / 8% | | Shuffled PN→KC, 5% active, associative | 88% / 5% | 79% / 7% | 80% / 7% | | Dense random, 5% active, associative | 91% / 4% | 84% / 6% | 82% / 7% | | Measured PN→KC, all active, associative | 89% / 5% | 81% / 7% | 74% / 9% | | Associative readout on raw pixels | 90% / 5% | 84% / 6% | 76% / 9% | | Measured PN→KC, 5% active, error-driven | 32% / 84% | 33% / 77% | 24% / 93% | | MLP, backpropagation | 19% / 100% | 19% / 93% | 22% / 91% | | MLP trained on all classes at once | 98% | 86% | 98% | **Findings.** The associative learning rule is what prevents catastrophic forgetting (about 5% instead of 84–100%), and it works as well on raw pixels as on the expansion; with five examples per class it approaches the all-at-once upper bound. Sparse expansion helps only when classes have several shapes (two-shape classes: 80–82% versus 74–76% without sparsity or expansion). The measured wiring performs like shuffled and random wiring, slightly behind; about 6% of measured Kenyon cells receive no projection-neuron input and never fire. What transfers to machine learning is the learning rule and the sparse code, not the specific connectome. ```sh .venv/bin/python expansion.py --benchmark .venv/bin/python -m unittest -v test_expansion.py ``` ## Theory: does the maths predict the experiment? (new) Open `/theory`. `theory.py` states the sparse-expansion model formally and tests four predictions against simulation, with the "supported" thresholds fixed in the code before running. The model: a fixed expansion `z(x) = top-k indicator of Wx` with sparsity `f = k/N`; an associative readout `w_c = Σ z(x_i)` over class-c examples, predicting `argmax_c ⟨z, w_c⟩ / ‖w_c‖`. Expanding the score shows the classifier is a kernel sum, `⟨z(x), w_c⟩ = Σ_i K(x, x_i)` with `K` the overlap of two sparse codes, which is what the predictions rest on. - **P1 — why the error-driven rule forgets. Refuted as first stated, then replaced.** The original prediction was that forgetting grows with cross-task code overlap. Across 14 sparsity and wiring settings, r = 0.27. Comparing classes by normalised weights with no bias term at test time cuts forgetting by 0.17 on average and up to 0.29, so recency bias, not interference on shared units, dominates. The associative readout normalises by construction and forgets 0.05. - **P2 — an optimal sparsity for classes with several shapes. Supported.** Accuracy on two-shape classes peaks at f = 0.01 (dense random) and f = 0.02 (measured), both interior to the swept range, while single-digit classes are flat in f. - **P3 — overlap as a function of input angle. Supported.** For a Gaussian expansion the expected overlap is the joint upper-tail probability of a bivariate normal with correlation cos θ divided by f, which equals `f^tan²(θ/2)` to leading order and exactly at 90°. Simulated dense codes match the exact curve (RMSE 0.028). The measured wiring deviates (RMSE 0.098, bias −0.074): its weights are non-negative and its in-degrees uneven, so it is not the Gaussian the theory assumes. - **P4 — capacity. Supported for moderate sparsity, breaks when very sparse.** Score statistics measured with 10 synthetic classes predict accuracy out to 400 classes with mean error 0.017 at f = 0.2 and 0.049 at f = 0.05, but 0.102 at f = 0.01, always optimistic. The cause is visible in the data: wrong-class score skew is +0.71 at f = 0.01, +0.25 at 0.05 and −0.01 at 0.2, so the Gaussian assumption fails exactly where the prediction does. ```sh .venv/bin/python theory.py # rewrites data/theory-benchmark.json .venv/bin/python -m unittest -v test_theory.py ``` ## Three flies in one arena (new) Open `/arena`. Two agents labelled male and one labelled female walk in a shared 2D space, each running the same measured 807-cell locomotion circuit with the dynamics of `model.py`, coupled by hand-designed senses. **What is measured:** each agent's circuit, its dynamics and its transmitter signs. **What is engineered:** odour falling off with distance, a two-antenna concentration comparison, seeing another fly as a bearing plus apparent size, who is attracted to whom, the courtship song rule, and the drive levels. There is no female connectome in this lab: MaleCNS is one male fly and the selected subset contains none of the sexually dimorphic courtship circuitry, so "female" labels engineered emissions and responses, not a different brain. There is no collision physics, so agents can overlap. Each agent is drawn to scale as a fly: a 2.5 mm body with abdomen, thorax, head and antennae, six legs animated by that agent's own leg oscillators (the same engineered gait as the motion lab, driven by its motor pools), and wings that fold back except when a male extends one to sing. Faint haloes show the odour field the agents sense, and the view fits itself to the trajectories. Scenarios: **rivalry** (both males approach her odour; each raises his drive on hearing a rival sing), **courtship** (only one male is attracted), **food** (a patch emits odour), **threat** (a looming threat at 4 s that everyone flees). Sensory controls remove vision, odour or song, or make the agents ignore each other entirely. Saved benchmark (`data/arena-benchmark.json`, two seeds per scenario and condition): | | All senses | No vision | No odour | Ignoring each other | |---|---|---|---|---| | Rivalry, closest male–female approach | 2.0 mm | 11.1 mm | 10.7 mm | 13.5 mm | | Courtship, closest male–female approach | 3.8 mm | 11.1 mm | 11.2 mm | 13.5 mm | | Food, closest approach to another fly | 4.9 mm | 7.3 mm | 10.7 mm | 13.5 mm | | Mean path walked | 120–170 mm | 37–60 mm | 61 mm | 61 mm | **Findings.** Both senses now matter: removing either odour or vision leaves the agents wandering at 11 mm separation, against 2 mm intact, and the threat scenario is unchanged by any sensory control because the threat response overrides them. Song is now brief and intermittent, because at walking speed the males pass the female rather than settling beside her. **Bug found while building this.** The motion lab's target mode steered away from its target: a positive descending bias turns the body readout clockwise, but the bearing-to-bias mapping assumed the opposite. `model.steer()` now holds the correct sign, shared by the motion lab and the arena. Target mode reaches within 1.5 mm of its target over 20 s instead of wandering off; `test_arena.py` guards it. ```sh .venv/bin/python arena.py --scenario rivalry .venv/bin/python arena.py --benchmark # about 5 minutes .venv/bin/python -m unittest -v test_arena.py ``` ### Walking speed, calibrated The gait readout used to map a pooled motor rate through two arbitrary constants (`stride = motor_hz / 50`, `speed = 6 mm/s × stride`, step frequency capped at 5 Hz), which put the default at 1.2 mm/s and the ceiling below a real fly's walking speed. `model.gait()` now calibrates against reported *Drosophila* walking: step frequency scales with motor output up to 12 Hz, stride length runs 1.1–2.0 mm, and speed is their product; `model.turn_rate()` derives yaw from the difference between the two sides' leg speeds across a 1 mm track. The motion lab reports speed, step frequency and stride length beside those reported ranges. These are calibration choices, not measurements: there is still no muscle model, load or inertia. **The circuit's operating band is narrow.** Below about 2.1 of descending drive the motor pools are silent; above about 3.0 they saturate. That fits cells receiving only 10–15% of their real input here, and it means the subset behaves close to a switch. The arena therefore maps a behavioural command in [0, 1] into that measured band rather than feeding drive in raw. **Calibration cost something.** At about 13 mm/s the arena's proportional steering overshoots, so one male usually reaches her (1.5–2 mm) while the other stalls several millimetres away, instead of both reliably making contact. Before calibration they made contact, but only because every agent crawled at roughly 1 mm/s. A grid search over drive, approach speed and steering gain found no setting giving both realistic speed and reliable contact, so the limit is the controller, not the speed. ### Gait quality, measured The arena measures duty factor, steps per leg per second and a tripod index (how much legs within a tripod agree, minus agreement across tripods) and shows them against reported ranges. **The measured subset does not produce tripod coordination:** the tripod index is about −0.02, and several legs are almost never lifted, because trochanter depressor motor neurons outnumber levators 14 to 2. Speed (13 mm/s) and duty factor (0.50–0.79) land in the reported ranges; the coordination does not. The drawing uses a relative stance rule (a leg is planted when it is lower than its own recent average) so legs visibly step, while the metrics report both that rule and the absolute one. ### Legs driven joint by joint MaleCNS motor neuron types name the muscle each cell drives, so `bodyplan.py` groups them into the antagonist pairs of each leg joint: coxa protractors against retractors, trochanter levator against depressors, tibia extensor against flexors, tarsus levator against depressors. Each joint angle then follows `rest + span * tanh((agonist Hz − antagonist Hz) / scale)`, and the drawn legs are posed from those angles rather than from one oscillator phase. A leg counts as planted when its trochanter is depressed. All six legs can drive their coxa, trochanter and tibia. Tarsus levator motor neurons are annotated only for the front legs, so four tarsi cannot be driven, and 59 motor neurons have no muscle name or no leg assignment and stay unused; `/arena` reports both. **Body motion** offers two modes. "From pooled motor rates" is the lab's original readout. "From the planted feet" derives translation and turning from the movement of planted feet in the body frame, so body motion follows the joints rather than a stride average. Both are readouts of motor activity: there is no muscle model, tendon, load or inertia. A fly's compound eyes cannot move, so there is nothing to drive there. Head and antennal movement exist (neck and antennal motor neurons) but neither group is in this subset, so head, eyes and antennae stay fixed. Correction to an earlier note: the 59 motor neurons that leave through accessory nerves rather than leg nerves are, by their muscle names (trochanter flexor, sternotrochanter, pleural promotor and similar), leg-muscle motor neurons. Pooling them into leg stride is reasonable; only the 8 through the abdominal nerve remain unclear. The atlas flag now says this. ### 3D arena Open `/arena3d` for the same arena run drawn as jointed 3D bodies: thorax, abdomen, head with fixed compound eyes, antennae, wings and six legs built from coxa, femur, tibia and tarsus segments at the lengths in `bodyplan.py`. Each fly stands on its own legs: the body height is set from the lowest planted foot rather than fixed. **Brain-driven parts:** coxa, trochanter and tibia joints on all six legs, and the tarsus on the front legs, posed from the motor neurons of each joint's muscles. The panel lists every joint angle in degrees, with ▪ marking planted legs. **Hand-driven parts:** head yaw and pitch, antennae, abdomen bend and both wings, with sliders. These have no motor neurons in this subset, so nothing in the simulation drives them; a wing can also extend automatically while a male sings. Compound eyes cannot move in a fly at all, so they are fixed to the head. ### How the neurons are modelled The modules use different levels of detail, and only the workbench is biophysical: | Module | Neuron model | |---|---| | Locomotion network and arena (`model.py`) | Leaky integrate-and-fire: 20 ms membrane constant, threshold 1, reset 0, 2 ms refractory, current-based synapses with a 10 ms exponential trace, Gaussian noise | | Neuron workbench (`neuron-core.js`) | Hodgkin–Huxley with squid kinetics, two compartments, conductance-based synapses (0 mV excitatory, −80 mV inhibitory) | | Odour learning (`learning.py`), numerosity, expansion | Firing-rate models, no membrane | | Vision network (`vision.py`) | Discrete-time recurrent tanh units, one or two states per cell, trained by backpropagation through time | What this leaves out of real fly neurons: graded (non-spiking) transmission, which many fly neurons use; conductance-based synapses in the network models, so no reversal potentials or shunting inhibition; dendritic compartments, although skeletons are now available; cell-type-specific time constants and spike-frequency adaptation; and Drosophila channel kinetics (para, Shaker, Shab) rather than squid. ### Two neuron models `model.py` now offers a second neuron model, selectable in the motion lab and via `neuron` on `/api/run`: - **`lif` (default):** leaky integrate-and-fire, 20 ms membrane constant, threshold 1, 2 ms refractory, current-based synapses. - **`adex`:** adaptive exponential integrate-and-fire with conductance-based synapses (Brette & Gerstner). Adds a spike-initiation nonlinearity, spike-frequency adaptation, and synapses as conductances with reversal potentials (0 mV excitatory, −75 mV inhibitory), so inhibition shunts instead of subtracting a fixed current. The membrane is integrated in 0.1 ms substeps inside each 1 ms network step. Parameters are standard cortical values, not Drosophila measurements, with the drive and conductance scalings calibrated so both models fire at the same rate at the default drive (9.2 versus 9.6 Hz). **Finding: the two models disagree about gain.** Raising descending drive from 2.0 to 4.0 makes the integrate-and-fire network escalate (motor pools 9 → 82 Hz, saturated 79% of the time), while the adaptive network holds motor output near 10 Hz with almost no saturation. Descending rates still rise with drive under both (15 → 25 Hz for the adaptive model), so the clamp is downstream: shunting inhibition and adaptation give the VNC gain control. That also means the narrow "usable band" the arena works around is a property of the integrate-and-fire model rather than of the connectome. Which behaviour is right is unknown: neither set of parameters is fitted to fly recordings. The arena still uses the integrate-and-fire model, because its drive-to-speed mapping depends on that escalation. ## Body studio (new) `/body` shows the fly at inspection size with every joint exposed: six legs with coxa, trochanter, tibia and tarsus controls, wings with an animated stroke, halteres, head yaw and pitch, antennae, proboscis and abdomen, plus part selection by click or menu, visibility, exploded view and model size. `flymesh.js` holds the procedural body: segmented abdomen, faceted compound eyes, veined wings, bristles, antennae with aristae, halteres and proboscis, with leg segments at the lengths in `bodyplan.py`. "Drive legs from the circuit" runs the arena simulation and poses each leg from the motor neurons of that joint's own muscles, with planted feet marked and every joint angle listed in degrees. The panel labels which parts are brain-driven and which are not: head, antennae, proboscis, abdomen, wings and halteres have no motor neurons in this subset, and a fly's compound eyes cannot move at all. The body stands on its lowest foot, so it sits on the floor at any pose. Segment proportions and the mesh are drawn by eye; there is no muscle model, load or inertia. ## Synaptic conductances and receptor kinetics (new) `/synapse` connects measured synapse counts to receptor conductances, which neither the locomotion network (current-based synapses) nor the workbench (generic conductances on one cell) does. For any cell in the circuit, each measured presynaptic partner opens a conductance with transmitter-specific kinetics: acetylcholine on a nicotinic cation channel (0 mV, 0.3/5 ms), GABA on GABA-A (−70 mV, 0.5/8 ms), glutamate on a glutamate-gated chloride channel (−70 mV, 0.5/10 ms, the insect inhibitory receptor) and histamine on a histamine-gated chloride channel (−80 mV, 0.3/6 ms). Conductances sum into one passive compartment with an optional −45 mV threshold. Two ways of using the transmitter predictions: - **Consensus per cell** uses each partner's consensus transmitter, the same call the network model makes. - **Split by measured probabilities** divides each partner's conductance across receptors in proportion to the per-transmitter probabilities in `data/quality.json`, so prediction uncertainty reaches the membrane. It also recovers cells whose consensus label sits outside the receptor set but whose probabilities include one: for DNa02 that is 2 of 297 synapses, and conductance reaches four receptor types instead of three. **What conductances change.** The shunting test runs the same excitatory drive with and without the inhibitory conductances. For DNa02 at double excitation the depolarisation falls from 2.6 mV to 0.4 mV, a ratio of 0.16: an inhibitory conductance shrinks the excitatory response itself, which a current-based synapse cannot do. DNa02 also stays silent under its measured input, since 24 of its 45 inputs are inhibitory against 19 cholinergic ones. Measured: which cells connect, how many synapses, and the transmitter predictions with probabilities. Assumed: every kinetic constant, 0.03 nS per synapse, membrane capacitance and leak, threshold and presynaptic rates. No morphology, no cable structure, no receptor subtypes, no desensitisation; a transmitter prediction is not a receptor identification. ```sh .venv/bin/python -m unittest -v test_synapse.py ``` ## Navigation: heading, home vector and steering (new) `/navigation` runs the fly's published navigation algorithm as a model: a ring of heading cells holds a compass bump, rotation cells shift it with turning, long-range inhibition keeps a single bump, vector cells accumulate the path travelled, goal cells hold a direction, and steering cells compare goal against heading and drive the descending neurons. The fly wanders for 12 s, then tries to get back to where it started. Results with five seeds (`data/navigation-benchmark.json`), at the default compass drift of 0.15 rad/s: | Condition | Closest to home | Got home | Compass error | |---|---|---|---| | Intact | 5.5 mm | 80% | 4.6° | | No rotation cells | 93.0 mm | 0% | 31.8° | | No long-range inhibition | 71.8 mm | 0% | 69.4° | | No vector cells | 92.8 mm | 0% | 5.1° | | Shuffled column map | 74.0 mm | 0% | 71.4° | | Random goal | 93.0 mm | 0% | 5.1° | Each control removes one mechanism and homing stops, for a different reason each time: without rotation the compass no longer tracks turning, without inhibition the bump spreads, without vector cells nothing is accumulated to return along, a shuffled column map breaks the correspondence between cells and directions, and a random goal leaves steering intact but aimed wrongly. Compass drift sets the difficulty: with no drift the integration is exact and homing is perfect, at 0.3 rad/s the fly lands 11 mm out, at 0.6 rad/s 22 mm out. **What is assumed.** That these cell types play these roles (from the navigation literature), the ring dynamics and their gains, the drift, the conversion from steering activity to turning, and the movement model. The bump rotation is implemented as an exact circular shift rather than the discretised gradient version, because at 16 columns the attractor pins the latter; the mechanism is the same, the numerics are not. **Measured connectivity.** `build_centralcomplex.py` extracts the central-complex navigation types (EPG, PEN, PEG, Delta7, PFN, FC2, hDelta, PFL, ER, ExR) and the measured edges among them and onto the lab's descending targets, writing `data/central-complex.json`. The probe on the real annotations found 1,247 traced cells of these types (456 PFN, 282 ER, 189 hDelta, 92 FC2, 50 EPG, 50 PFL, 42 PEN, 42 Delta7, 26 ExR, 18 PEG). Column identity comes from the instance suffix, which the probe made legible: `EPG(PB08)_R2` is bridge glomerulus R2 (the parenthetical is a type code), `PFNv(PB05)_L4_C3` is glomerulus L4 with fan-shaped-body column C3, `Delta7(PB15)_L3R6_R` spans two glomeruli with soma side R, and `FC2A_C2_L` carries only a body column. Glomeruli R1–R8 map to ring positions 0–7 and L1–L8 to 8–15; cells labelled only by a body column spread over the same ring; cells spanning several glomeruli take the circular mean. Ring neurons (`ER5_L`, `ExR1_R`) have no column, which is correct: they carry cues, not compass positions. In measured mode the rotation strength is read off the wiring rather than assumed: the model takes the difference between the two rotation cell classes' mean column offsets, so wiring that shifts the compass oppositely by side gives a gain of about one column and homing works, while wiring that shifts both sides the same way gives a gain of zero and the compass cannot track turning at all, failing exactly like removing the rotation cells. That is the test: the connectome decides whether the algorithm runs. Once the file exists, `/navigation` reports the measured column offsets between functional groups and, separately, the offsets of each rotation cell type by side. That split matters: pooling the two rotation classes cancels their opposite shifts to zero, while the per-type breakdown shows whether the measured wiring really shifts the bump in opposite directions by side, which is what the algorithm needs for a turn to move the compass correctly. The page states plainly that the offsets are measured while the functional grouping and the rotation interpretation are assumptions. ```sh .venv/bin/python build_centralcomplex.py raw-data --probe # what the annotations hold .venv/bin/python build_centralcomplex.py raw-data # writes data/central-complex.json .venv/bin/python navigation.py --benchmark .venv/bin/python -m unittest -v test_navigation.py ``` ## Spectra: is the measured wiring special? (new) `/spectrum` asks the structural version of the question the task experiments kept answering with "no": do the eigenvalues of the measured weight matrix differ from randomisations with the same statistics? `spectral.py` compares six randomisations (destinations permuted, weights permuted among edges, targets redrawn, inhibitory cells permuted, fully random, and random wiring matched to the measured share of reciprocal pairs) under three normalisations. **The lab's default normalisation caps the answer.** Incoming absolute weights sum to one per cell, so the spectral radius cannot exceed 1. A measured value near 1 means the matrix nearly attains its bound, not that the fly sits at the edge of chaos. Locomotion subset, incoming-L1 normalisation: | Condition | Radius | Reciprocity | |---|---|---| | Measured | 0.976 | 28.1% | | Weights permuted among edges | 0.985 | 28.1% | | Inhibitory cells permuted | 0.875 | 28.1% | | Destinations permuted | 0.254 | 5.3% | | Targets redrawn | 0.274 | 3.1% | | Fully random | 0.265 | 3.3% | | Random, reciprocity matched | 0.266 | 30.6% | **What this says.** The near-unit radius follows the measured *topology*: it survives shuffling weights (0.985) and largely survives permuting which cells are inhibitory (0.875), but collapses to about 0.26 under anything that moves edges. Reciprocity alone does not explain it, since random wiring matched on reciprocity still gives 0.266. The gap survives the synapse-count threshold control and input-coverage filtering. **But it does not survive changing the normalisation.** Scaled so the largest singular value is 1, the measured radius is 0.184 against 0.157–0.230 for the randomisations, with fully random highest. So the effect is a property of row-normalised measured topology, not a scale-free property of the graph. **Where it comes from.** The leading eigenvector is concentrated, not global: its participation ratio is 0.019 against 0.609 when shuffled, and its mass sits on five SNpp39 sensory cells plus a couple of motor neurons. Those cells receive all of their measured input from each other, which under row normalisation makes a stochastic sub-block with an eigenvalue of 1 by construction. 619 of 794 cells with input are fed only from within their own strongly connected component, a consequence of how the subsets were cut. **Other graphs.** The mushroom-body subset is essentially feedforward: measured radius about 0, while rewiring creates loops and pushes it to 1. The optic-lobe patch gives exactly 1 for every condition, because its cells' inputs lie entirely inside the patch. ### A subset selected for observability `build_coverage_subset.py` answers the objection above by building a subset the other way round. Coverage is circular, since a cell's input coverage depends on which other cells are present, so it grows outward: seed with the lab's existing subsets, read every edge into them, measure each cell's coverage as the share of its true input synapses (from `body-stats.feather`) arriving from cells *inside* the subset, add the strongest missing upstream partners of the worst-covered cells up to a budget, then keep only cells that reach the target coverage. Dropping a cell removes its edges and lowers its neighbours' coverage, so the keep-set is iterated until stable, which means cells propped up by poorly observed neighbours fall out too. The result is smaller and better observed than the type-selected subsets, and it is the matrix on which the spectral question can actually be asked: if measured still differs from its randomisations here, under a normalisation that does not bound the radius, the difference is about the wiring rather than about the cut. `/spectrum` lists it as "Coverage-selected subset" once built. ```sh .venv/bin/python build_coverage_subset.py raw-data --target 0.5 --budget 3000 .venv/bin/python spectral.py --benchmark ``` Two passes over `weights.feather`, a few minutes each. Lower `--target` if too few cells survive; the run prints the seed coverage, how far it grew and how many cells reached the target. **Conclusion for the deep-learning idea.** The spectral signature is real but it belongs to the lab's matrices, not to the fly: subset selection decides which cells look self-contained, and row normalisation turns those into unit eigenvalues. Using the connectome as an initialisation prior on the strength of "it sits near the edge of stability" would be building on an artefact. A fair test needs a subset chosen without a synapse threshold, a normalisation that does not bound the radius, and cells with high input coverage; `build_coverage_subset.py` above provides the first and third, and the `global_scale` normalisation the second. ```sh .venv/bin/python spectral.py --benchmark .venv/bin/python -m unittest -v test_spectral.py ``` ## Where inhibition sits (new) `/inhibition` follows up the one spectral effect that survived the observability control. On the coverage-selected subset the measured network is less excitable than matched random wiring, and moving which cells are inhibitory changes that substantially, from only about 6% of cells: | Placement | Natural spectral radius | |---|---| | Measured | 33.4 | | Random | 39.9 | | Random, out-degree matched | 41.1 | | Highest-throughput cells | 40.0 | | Lowest-throughput cells | 47.4 | | No inhibition | 47.4 | Inhibitory cells are not the highest-degree ones (mean out-degree 127 against 194 for excitatory cells), but they carry more synapses each and 19% of all outgoing synapses in the subset. **Does the placement compute anything?** `inhibition.py` uses the measured graph as a fixed recurrent network and trains only a linear readout, the standard reservoir test, keeping the number of inhibitory cells identical across conditions and moving only which cells they are. Tasks: memory capacity (reconstructing a random input stream at increasing delays) and delayed integration. Rescaled to a common spectral radius, every placement scores the same (memory 8.2–8.3, integration at ceiling): with gain equalised, placement does nothing. Under one shared scale factor, so gain differences survive, the differences are small but consistent, and the right statistic is the per-seed difference, since the seed fixes which cells the subsample contains: | Comparison | Memory difference | Seeds favouring measured | t | |---|---|---|---| | measured − random | +0.206 ± 0.175 | 14 of 15 | 4.6 | | measured − no inhibition | +0.131 ± 0.133 | 14 of 15 | 3.8 | | measured − lowest-throughput | +0.129 ± 0.134 | 14 of 15 | 3.8 | | measured − out-degree matched | +0.093 ± 0.118 | 13 of 15 | 3.0 | | measured − highest-throughput | +0.020 ± 0.101 | 9 of 15 | 0.8 | **What this supports, and what it does not.** The measured placement beats random placement by about 3% of memory capacity, consistently across seeds, and beats having no inhibition at all by a similar margin. But it ties with simply putting inhibition on the highest-throughput cells, so the result is better described as "inhibition belongs on high-throughput cells" than as anything specific to this connectome. The effect is small, it disappears once gain is equalised, and it rests on transmitter predictions rather than measured receptors. ```sh .venv/bin/python inhibition.py --benchmark --seeds 5 .venv/bin/python -m unittest -v test_inhibition.py ``` ## Object memory: seeing, remembering, recognising a variant (new) Open `/objects`. The question: can the measured circuit take a sensation (a cup under some light), store it, and use the memory to recognise a slightly different sensation (another cup, other light, another distance)? This is the fly's version of what tbp.monty attempts, and the page says plainly where it differs. The route is measured. About 7% of Kenyon cells take visual rather than olfactory input: in `data/mushroom-body.json`, 252 of 4,064 get no olfactory projection-neuron input and 51 more get under ten synapses, almost all KCg-d (173 of 206) and KCab-p (70 of 129). They feed the same compartments and dopamine teachers as the olfactory memory. Pipeline (`objects.py`): 1. **Scene.** Paper cup, mug, bottle, wine glass and can as shaded surfaces of revolution (the mug with a handle that turns with rotation, the glass mostly see-through), under varied light level, light direction, distance, gaze and per-instance shape. 2. **Eye.** 721 hexagonal columns at 5°, the lab's eye-model layout, each averaging the scene over its acceptance angle, with photoreceptor noise. 3. **Lamina.** Contrast against the local and global mean (L1/L2-style adaptation), split into ON, OFF and three edge orientations. 4. **Visual projection neurons.** Each pools one channel over a receptive field, then rectifies and saturates. Measured optic-lobe hex coordinates set the centre when annotated. 5. **Kenyon cells.** Measured input wiring onto visual Kenyon cells, top-k sparse (APL). 6. **Memory.** `memory.py` unchanged: reward depresses active Kenyon-cell synapses in the compartments PAM cells reach; the readout is the change in approach drive. 7. **Tests.** Held-out views per factor (dim, bright, side light, nearer or farther, new kinds), pairwise discrimination, a continuous cup-to-bottle morph, 1–8 glimpses averaged, and a differential protocol (cup rewarded, mug punished). Step 1 needs the raw tables: ```sh .venv/bin/python build_visualmemory.py raw-data --probe # who feeds visual vs olfactory Kenyon cells, and ring neurons .venv/bin/python build_visualmemory.py raw-data # writes data/visual-memory.json, with upstream measures .venv/bin/python build_visualmemory.py raw-data --upstream # add upstream measures to an existing file only .venv/bin/python objects.py --benchmark # five seeds, six circuits, plus the differential protocol ``` Until `data/visual-memory.json` exists, a **synthetic stand-in** (120 random inputs, 6 per visual Kenyon cell) replaces the visual wiring and the page flags it. With the stand-in, the shuffled and random controls compare random against random, so they say nothing about the measured wiring yet; the other results do not depend on it. First results (synthetic visual inputs, measured everything downstream, five seeds, AUC for the trained cup against the other four objects, one look): | | familiar | dim | bright | side light | nearer/farther | new kinds | |---|---|---|---|---|---|---| | Adapting eye | 0.90 | 0.85 | 0.81 | 0.78 | 0.58 | 0.83 | | No adaptation | 0.78 | 0.58 | 0.80 | 0.60 | 0.66 | 0.76 | - **Lighting invariance comes from the eye, not the memory.** Without lamina adaptation, dim light collapses recognition (paired −0.27, 0 of 5 seeds better) and side light costs −0.18. With adaptation, halving the light leaves visual-input codes 0.998 alike; without it they fall to 0.08. - **Size is not invariant.** Recognition falls to near chance when the object is nearer or farther than in training. Nothing in this pathway rescales the image; the model sees a cup at another distance as a different object. - **Similar shapes share a memory.** Kenyon-cell codes for cup, mug and can overlap about as much as two cups do; bottle and wine glass separate. The expansion decorrelates: along the cup-to-bottle morph, visual-input overlap stays above 0.83 while Kenyon-cell overlap falls to 0.30. - **Glimpses do not help here.** Averaging up to eight re-fixations leaves AUC flat, because the errors come from the object and its lighting, not from per-look noise. - **Differential training sharpens the contrast and shifts the peak.** Rewarding cups and punishing mugs raises cup-vs-mug discrimination in dim and bright light (0.72 → 0.81, 0.65 → 0.81), but untrained bottles and glasses then score as high as cups. The memory becomes "not a mug" more than "a cup". Along the cup-to-bottle morph the response peaks away from the punished mug (1.15 at a quarter of the way), the peak shift seen in animal discrimination learning. - **Moving the same wiring onto olfactory Kenyon cells** lowers familiar recognition (−0.10, 0 of 5), so the measured outputs of the visual Kenyon cells carry it somewhat better than the olfactory ones. **With the measured visual inputs (first build).** `build_visualmemory.py` found 345 Traced inputs (15,068 edges) onto the 335 visual Kenyon cells. Only about 29% of their outside input comes from optic-lobe projection neurons (9,736 of 33,771 synapses); the rest comes from central-brain cells (PLP, SLP, lateral horn types such as LHPV3c1), olfactory projection neurons (1,840) and unlabelled fragments. None of the inputs carry optic-lobe hex coordinates. Five seeds, one look: | | familiar | dim | bright | side light | nearer/farther | new kinds | |---|---|---|---|---|---|---| | Measured inputs | 0.81 | 0.87 | 0.85 | 0.77 | 0.76 | 0.84 | | No adaptation | 0.70 | 0.54 | 0.51 | 0.56 | 0.70 | 0.66 | | Shuffled | 0.90 | 0.90 | 0.89 | 0.76 | 0.77 | 0.89 | | Random | 0.91 | 0.85 | 0.83 | 0.84 | 0.73 | 0.84 | The adaptation result holds on measured wiring: without it, dim and bright light fall to chance. The wiring comparison was not a fair test, because every input, olfactory and lateral-horn cells included, was driven by the image with an invented feature and receptive field. **Upstream measures (second build).** `build_visualmemory.py raw-data --upstream` now looks one and two hops up from each input and measures its *visual share* (fraction of annotated input synapses from optic-lobe cells), its *channel mix* (ON, OFF, colour/luminance or form, by the optic-lobe cell types feeding it), and its *receptive field* (synapse-weighted position of the columnar cells with optic-lobe hex coordinates). `objects.py` uses them: an input is driven by the image only in proportion to its visual share, weighs channels by its measured mix, and pools from its measured position. The `assumed_features` control keeps the measured wiring but ignores these measures, so the comparison with the shuffled and random wiring now asks whether the measured pairing of visual features onto Kenyon cells matters. Still assumed: the type-to-channel table (well-established polarity types only; every other optic-lobe cell counts as form), half-weighting of second-hop evidence, and that non-visual inputs stay silent during a purely visual task. **Bug found by the upstream run, and fixed.** With measured visual shares, recognition fell below chance (familiar 0.37) while shuffled and random wiring scored 0.69–0.77. Two errors in the Kenyon-cell code caused it. (1) Each Kenyon cell's visual input was normalised by its visual input alone, so an olfactory Kenyon cell with one small synapse from these cells counted it as its whole input. These inputs make 97,911 synapses onto olfactory Kenyon cells against 23,083 onto visual ones, so most of the code sat in olfactory cells. (2) The number of active cells was set from all 3,925 receiving cells (k = 392) while only about 300 had any drive, so the rest were filled by a fixed set of undriven cells, the same for every object. Now each cell's visual input is normalised by its total input, olfactory projection neurons included; k is set from the 335 visual Kenyon cells; and an undriven cell never fires. With an approximation of the measured shares, about 93% of active cells are then visual Kenyon cells, and the measured, shuffled and random wirings all give similar, structured codes (cup, mug and can nearly identical; bottle and glass distinct). Both earlier measured tables used the faulty code and should be read with that in mind. **Not Monty.** Monty learns features at locations in an object-centred reference frame and moves its sensors to build that model. Nothing here builds an object frame: glimpses add evidence, they do not assemble a model. The fly's closest mechanism, ring neurons whose synapses onto the compass neurons bind a visual scene to heading, works in an egocentric frame and is probed but not modelled here. Assumed: the objects and renderer, monochrome vision, the lamina model, each input's channel (shared within a type) and receptive field, the input nonlinearity, 10% Kenyon-cell sparseness among receiving cells, the viewing distances (10–14 cm in training), and everything `memory.py` assumes. ## Memory: the mushroom body, rebuilt for it (new, in progress) Olfactory associative memory in the fly lives in the mushroom body, and its mechanism is well enough known to model honestly. An odour activates a sparse set of Kenyon cells; their axons run through about fifteen compartments, each with its own dopamine neurons delivering a teaching signal and its own output neurons reading out. When an odour's Kenyon cells fire together with a compartment's dopamine neurons, the Kenyon-cell-to-output synapses in that compartment weaken, and that weakened set of synapses is the memory. Presenting the odour again drives that compartment's output neuron less, tipping the balance of the output population toward approach or avoidance. The wiring decides where a memory can be written; the weights decide what it says. A connectome cannot show memory contents, since short-term memory changes synaptic strength rather than synapse count, but it can show exactly where memory can be written. The lab's learning subset cannot support that model: it has no PPL1 dopamine neurons (the aversive teachers), no APL or DPM, and no loops from output neurons back to dopamine neurons or to each other, which extinction and second-order conditioning need. `build_mushroombody.py` extracts the complete circuit: Kenyon cells, output neurons, PAM and PPL dopamine neurons, DPM, APL, the lab's projection neurons, and the 200 strongest downstream partners of the output neurons, where memory leaves the mushroom body on its way to behaviour. Kenyon-cell-to-Kenyon-cell edges are summarised as a total rather than stored, since there are very many. Compartments are parsed from instance names: `MBON01(y5B'2a)` has dendrites in γ5 and β′2a, `MBON11(y1pedc>a/B)` has dendrites in γ1-pedunculus and an axon to the α/β lobes, and `PAM07(y40.025 and the disk's extrapolated Gaussian radius four frames after the observed sequence exceeds its distance to the central sensor plus 0.45 field units. This is a defined image-space proxy, not collision physics, metric depth or 3D perception. The encoder receives no target labels, simulator velocity or growth parameters. For node i with one or two state components, the supplied update is: `h_i(t) = 0.5 h_i(t-1) + 0.5 tanh(sum_j W_ij h_j(t-1) + h_i(t-1) U_i + G_i x_column(i)(t) + b_i)` The shared scalar weight W_ij transmits each state component along an allowed directed edge. U_i is a learned 1×1 or 2×2 local matrix; G_i and b_i are learned gains and biases. G_i is constrained to zero outside L1/L2/L3. Local state retention/mixing is an added dynamical assumption, including at cells without measured anatomical self-edges. A learned linear readout pools the final Tm1/Tm2/Tm9 states into two velocities and a hazard logit. Biological transmitter signs, synaptic dynamics, dendrites, spiking and ion channels are not reconstructed. Synapse counts are displayed as anatomy but deliberately do not initialize electrical strengths. Four models see identical data and minibatch orders: - Measured graph, one state per cell: 2,070 trainable parameters. - Measured graph, two states per cell: 3,134 parameters. - Rewired graph, two states per cell: 3,134 parameters. Directed double-edge swaps preserve each cell's incoming/outgoing degree and actual self-loops. Local parameters and input/output assignments match the real graph. Initial incoming-weight norms are matched node by node. The null does not preserve spatial or cell-type connectivity, and no mixing-time guarantee is claimed. - Conventional dense recurrent network: 45 hidden units and 3,108 parameters. Learned mixing of all 19 input samples, dense recurrence, scalar local memory and the same activation/leak rule. This is a parameter-matched baseline for the two-state graph, not a match in state count, computation, input/readout geometry or wall time. Training uses explicit NumPy backpropagation through all 10 frames and Adam (learning rate 0.004, beta1 0.9, beta2 0.999, epsilon 1e-8, global gradient norm capped at 2). Loss is mean squared error over the two normalized velocities plus 0.5 binary cross-entropy for hazard. Each run uses 256 training, 64 validation and 128 test sequences from independent RNG streams; the same three partitions are shared across its four models. Defaults use 35 epochs and batches of 32. Validation curves are displayed every five epochs but do not choose checkpoints. Test metrics are evaluated before and after training, not used by the optimizer. Epochs=0 is available as a no-training control. The interface replays the first 12 test clips without selecting favorable examples. Predictions are final full-sequence outputs and remain fixed during replay; only sensory input and node states change frame by frame. Click a neuron, choose it from the selector, or follow an incoming source to inspect the actual saved state trajectory and its numerical update terms. Rewired edges are explicitly synthetic and have no displayed anatomical synapse count. Dense hidden units have no biological body ID. All graph animations display recurrent state variables, not electrical recordings. **Fixed-default results across seeds 401–403:** average velocity RMSE is 0.0175 for measured scalar nodes, 0.0172 for measured two-state nodes, 0.0185 for rewired two-state nodes and 0.0157 for the dense recurrent baseline, in field units/frame. The training-mean baseline is about 0.0429. Hazard accuracy is 91.4%, 90.9%, 91.4% and 92.2%, respectively. Hazard is uncommon in these test samples (10–15% prevalence); always predicting no hazard already achieves 85–90% accuracy, so raw accuracy must be interpreted alongside the displayed probability Brier score and baseline. These small exploratory runs establish learnability of the supplied architecture on this synthetic task, not a statistically demonstrated advantage of the connectome or richer per-neuron models. Removing all intercellular weights after training raises measured-graph motion RMSE to about 0.043, near the constant baseline. This shows the constructed graph models use their communication pathways; it does not show the particular biological topology is necessary. We restore the weights afterward and verify their hash. Tests also verify analytical gradients against finite differences, degree preservation, absent-edge constraints, evaluation without mutation, saved-checkpoint prediction reproduction and the displayed per-node state equation. Reproduce: ```sh OPENBLAS_NUM_THREADS=1 .venv/bin/python build_vision_patch.py /path/to/raw-data OPENBLAS_NUM_THREADS=1 .venv/bin/python vision.py --benchmark OPENBLAS_NUM_THREADS=1 .venv/bin/python -m unittest discover -p 'test_*.py' -q ``` The saved default full run is `data/vision-example.json`; summary comparisons are in `data/vision-benchmark.json`. Model weights for all three seeds and four architectures are stored as `data/vision--.npz`. Browser retraining returns a new experiment without replacing the saved benchmark; use Export to keep it. JSON exports include all test targets/predictions, anatomical circuit/provenance, graph learned-edge weights, node-local parameters, displayed activity, validation curves, settings and audit hashes. The NPZ files also contain full input and output readout weights. Deterministic recomputation is supported within the same numerical environment; NumPy/BLAS version changes may cause small differences. Related research: [Connectome-constrained networks predict neural activity across the fly visual system](https://www.nature.com/articles/s41586-024-07939-3). This prototype is independently implemented, does not use FlyVis weights, and has not been validated against biological recordings. Future steps would be a larger complete motion pathway, receptor/cell-type constraints, stronger matched nulls, unseen scene families and an embodied navigation task. Current verification: **41 unit tests pass**, including all earlier modules and the visual-network checks. ### Trainable compound-eye layer Two further architectures put a trainable eye in front of the measured two-state graph. Each column first passes through a photoreceptor stage with shared adaptation, `y_t = a·y_(t−1) + (1−a)·x_t` and output `g·(x_t − k·y_t)`, then a learned column-to-column projection into L1/L2/L3. **Retinotopic** (`eye_graph`) lets each column mix only with its six hex neighbours, like neural superposition; **unrestricted** (`eye_dense`) lets any column mix with any other. Both are created after all other random draws and start as an exact identity, so untrained they equal the measured graph, and all their other parameters match it. Gradients of the eye parameters are checked against finite differences in `test_vision_patch.py`. “Eye reset” re-evaluates each trained model with the eye returned to identity. Three seeds, 35 epochs (means; lower is better except accuracy): | Architecture | Parameters | Motion RMSE | Direction error | Hazard accuracy | Hazard Brier | |---|---|---|---|---|---| | Measured graph · 2 states | 3,134 | 0.0172 | 16.5° | 90.9% | 0.064 | | + trainable eye, retinotopic | 3,240 | 0.0172 | 16.4° | 92.4% | 0.057 | | + trainable eye, unrestricted | 3,498 | 0.0156 | 13.7° | 92.4% | 0.061 | | Rewired graph · 2 states | 3,134 | 0.0185 | 18.8° | 91.4% | 0.070 | | Dense recurrent baseline | 3,108 | 0.0157 | 14.5° | 92.2% | 0.067 | Always predicting "no hazard" scores 86.7% accuracy (Brier 0.115). The retinotopic eye gives the best hazard calibration with 106 extra parameters and no motion benefit; the unrestricted eye gives the best motion, matching the dense network. In both, the learned photoreceptor adaptation stays near zero (`k` between −0.09 and 0.14) while gain rises to about 1.3 and each column mixes roughly 0.09 of each allowed neighbour, so the benefit comes from spatial pooling, not temporal adaptation. Resetting the eye after training hurts both (retinotopic motion RMSE 0.029, unrestricted hazard Brier 0.34), showing the downstream network adapts to the eye it trained with. Three seeds are too few to call any of these differences reliable.