flyreview 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brekkylab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyreview
3
+ Version: 0.1.0
4
+ Summary: A simulated fruit fly brain (MaleCNS v1.0) reviews pull requests.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy>=1.24
10
+ Requires-Dist: pillow>=10
11
+ Requires-Dist: pyarrow>=14
12
+ Requires-Dist: pandas>=2.0
13
+ Dynamic: license-file
14
+
15
+ # flyreview
16
+
17
+ A simulated fruit fly brain reviews your pull request.
18
+
19
+ ![the fly reads a SWE-bench diff and presses one of three buttons; its brain lights up on the right](docs/img/fly-review.gif)
20
+
21
+ The brain is the complete male *Drosophila* central nervous system, MaleCNS v1.0 (HHMI Janelia and Google Research, September 2026): 166,700 neurons and 25,582,938 connections, simulated as is. The diff is put on a dark screen in front of its eyes. It presses one of three buttons. It cannot read.
22
+
23
+ ```sh
24
+ pip install flyreview
25
+ flyreview pr owner/repo 123 # prints the review
26
+ flyreview pr owner/repo 123 --post # submits it under your gh login
27
+ ```
28
+
29
+ The first run fetches the prepared connectome (about 300 MB) into `~/.cache/flyreview`. A review takes a second or two on a laptop.
30
+
31
+ ## What it does
32
+
33
+ 1. The unified diff becomes 640x480 screens in GitHub dark mode: light text on dark, added rows green, removed rows red, 29 rows per screen.
34
+ 2. Each screen drives 4,146 photoreceptors: 3,335 R1-R6 cells with luminance and 811 R8 cells with colour (R8y green, R8p blue), placed on the screen through the optic-lobe column each one wires into.
35
+ 3. Every neuron is integrated at 0.1 ms with the leaky integrate-and-fire constants of Shiu et al. 2024. Every connection is delivered. Nothing is cropped, added or learned.
36
+ 4. Two readouts, both fixed and published:
37
+ - **Verdict.** R8y (green) spikes per second while the diff is up, minus the same on a blank dark screen. Above 17,692 Hz by more than 15 percent: request changes. Below by more than 15 percent: approve. In between: comment.
38
+ - **Inline comments.** DNpe017, a descending neuron: one comment per 2 Hz, one to four, on the rows that lit the eye most, which on a dark screen are the rows with the most code.
39
+
40
+ ![the same django patch in light and dark mode, with the photoreceptors that fired drawn on top](docs/img/retina-dark.png)
41
+
42
+ Dark mode is not decoration. On a white page the brightest rows are the empty ones, so the photoreceptors fire on whitespace and ignore the code (row-by-row correlation with characters: -0.59). On a dark page light is text (+0.56), and the green rows are exactly the added lines.
43
+
44
+ Green is added code, and in SWE-bench Verified the model patches that add more lines fail more often (median 10 added lines for passing patches, 22 for failing). The fly likes short pull requests. That is the entire mechanism.
45
+
46
+ ## How well
47
+
48
+ Same 200 model-written SWE-bench Verified patches, 100 that passed the hidden tests and 100 that failed. The threshold was set on a random half and scored on the other half, 300 splits.
49
+
50
+ | | |
51
+ | --- | --- |
52
+ | decides | 74% of patches (the rest get a comment) |
53
+ | right when it decides | **65%** |
54
+ | right if forced to decide all | 62% |
55
+ | approves human-written gold patches | 83% of 500 |
56
+
57
+ ![R8y spikes per second for 200 patches, passed above and failed below, with the threshold and the comment band](docs/img/r8y-distribution.png)
58
+
59
+ Tables by repository and the method in [`docs/swebench.md`](docs/swebench.md). A picture-by-picture explanation in [`docs/how-it-works.html`](docs/how-it-works.html).
60
+
61
+ ## Run it on a diff, or in CI
62
+
63
+ ```sh
64
+ flyreview review change.patch
65
+ git diff main | flyreview review -
66
+ ```
67
+
68
+ ```yaml
69
+ # .github/workflows/fly.yml
70
+ on: pull_request
71
+ permissions:
72
+ pull-requests: write
73
+ jobs:
74
+ fly:
75
+ runs-on: ubuntu-latest
76
+ steps:
77
+ - uses: brekkylab/flyreview@main
78
+ ```
79
+
80
+ A workflow token cannot approve a pull request unless the repository (and organization) allows it; the fly then files its approval as a comment and says so.
81
+
82
+ ## Build it yourself
83
+
84
+ ```sh
85
+ git clone https://github.com/brekkylab/flyreview && cd flyreview
86
+ python -m venv .venv && . .venv/bin/activate && pip install -e .
87
+ flyreview setup # release bundle, or the 1.1 GB Janelia tables if the bundle is unreachable
88
+ python -m pytest
89
+ ```
90
+
91
+ `flyreview setup` with `FLYREVIEW_BUNDLE_URL=` unset and no release falls back to downloading the three Janelia tables, checking their SHA-256, and building the graph with `flybrain/graph.py`. Needs a C++ compiler for the kernel, ninety lines of C++.
92
+
93
+ ## What is real and what is a model
94
+
95
+ Real: which neuron connects to which, with how many synapses; each cell's predicted transmitter; cell types and optic-lobe column coordinates. A model: identical LIF dynamics for every cell, photoreceptors as spiking cells, the screen-to-receptor projection, the 12 mV standing current on lamina cells, and every mapping from spike count to button. None of the readouts are what those cells do in a living fly.
96
+
97
+ ## Provenance
98
+
99
+ - MaleCNS v1.0, HHMI Janelia FlyEM with Cambridge, MRC LMB and Google Research. CC BY 4.0. Source URLs and digests in `flybrain/paths.py`.
100
+ - LIF constants from Shiu et al., "A Drosophila computational brain model reveals sensorimotor processing", Nature 2024.
101
+ - The choice of DNp20 and DNpe017 as visually responsive descending neurons and the idea of placing photoreceptors by their lamina column follow DOOMFLY (Alex Wormuth). The simulator, importer and retina in `flybrain/` are ours, and reproduce that kernel spike for spike on identical input.
102
+ - Patches and labels: SWE-bench Verified; AlexCuadron/SWE-Bench-Verified-O1-reasoning-high-results.
103
+
104
+ MIT.
@@ -0,0 +1,90 @@
1
+ # flyreview
2
+
3
+ A simulated fruit fly brain reviews your pull request.
4
+
5
+ ![the fly reads a SWE-bench diff and presses one of three buttons; its brain lights up on the right](docs/img/fly-review.gif)
6
+
7
+ The brain is the complete male *Drosophila* central nervous system, MaleCNS v1.0 (HHMI Janelia and Google Research, September 2026): 166,700 neurons and 25,582,938 connections, simulated as is. The diff is put on a dark screen in front of its eyes. It presses one of three buttons. It cannot read.
8
+
9
+ ```sh
10
+ pip install flyreview
11
+ flyreview pr owner/repo 123 # prints the review
12
+ flyreview pr owner/repo 123 --post # submits it under your gh login
13
+ ```
14
+
15
+ The first run fetches the prepared connectome (about 300 MB) into `~/.cache/flyreview`. A review takes a second or two on a laptop.
16
+
17
+ ## What it does
18
+
19
+ 1. The unified diff becomes 640x480 screens in GitHub dark mode: light text on dark, added rows green, removed rows red, 29 rows per screen.
20
+ 2. Each screen drives 4,146 photoreceptors: 3,335 R1-R6 cells with luminance and 811 R8 cells with colour (R8y green, R8p blue), placed on the screen through the optic-lobe column each one wires into.
21
+ 3. Every neuron is integrated at 0.1 ms with the leaky integrate-and-fire constants of Shiu et al. 2024. Every connection is delivered. Nothing is cropped, added or learned.
22
+ 4. Two readouts, both fixed and published:
23
+ - **Verdict.** R8y (green) spikes per second while the diff is up, minus the same on a blank dark screen. Above 17,692 Hz by more than 15 percent: request changes. Below by more than 15 percent: approve. In between: comment.
24
+ - **Inline comments.** DNpe017, a descending neuron: one comment per 2 Hz, one to four, on the rows that lit the eye most, which on a dark screen are the rows with the most code.
25
+
26
+ ![the same django patch in light and dark mode, with the photoreceptors that fired drawn on top](docs/img/retina-dark.png)
27
+
28
+ Dark mode is not decoration. On a white page the brightest rows are the empty ones, so the photoreceptors fire on whitespace and ignore the code (row-by-row correlation with characters: -0.59). On a dark page light is text (+0.56), and the green rows are exactly the added lines.
29
+
30
+ Green is added code, and in SWE-bench Verified the model patches that add more lines fail more often (median 10 added lines for passing patches, 22 for failing). The fly likes short pull requests. That is the entire mechanism.
31
+
32
+ ## How well
33
+
34
+ Same 200 model-written SWE-bench Verified patches, 100 that passed the hidden tests and 100 that failed. The threshold was set on a random half and scored on the other half, 300 splits.
35
+
36
+ | | |
37
+ | --- | --- |
38
+ | decides | 74% of patches (the rest get a comment) |
39
+ | right when it decides | **65%** |
40
+ | right if forced to decide all | 62% |
41
+ | approves human-written gold patches | 83% of 500 |
42
+
43
+ ![R8y spikes per second for 200 patches, passed above and failed below, with the threshold and the comment band](docs/img/r8y-distribution.png)
44
+
45
+ Tables by repository and the method in [`docs/swebench.md`](docs/swebench.md). A picture-by-picture explanation in [`docs/how-it-works.html`](docs/how-it-works.html).
46
+
47
+ ## Run it on a diff, or in CI
48
+
49
+ ```sh
50
+ flyreview review change.patch
51
+ git diff main | flyreview review -
52
+ ```
53
+
54
+ ```yaml
55
+ # .github/workflows/fly.yml
56
+ on: pull_request
57
+ permissions:
58
+ pull-requests: write
59
+ jobs:
60
+ fly:
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: brekkylab/flyreview@main
64
+ ```
65
+
66
+ A workflow token cannot approve a pull request unless the repository (and organization) allows it; the fly then files its approval as a comment and says so.
67
+
68
+ ## Build it yourself
69
+
70
+ ```sh
71
+ git clone https://github.com/brekkylab/flyreview && cd flyreview
72
+ python -m venv .venv && . .venv/bin/activate && pip install -e .
73
+ flyreview setup # release bundle, or the 1.1 GB Janelia tables if the bundle is unreachable
74
+ python -m pytest
75
+ ```
76
+
77
+ `flyreview setup` with `FLYREVIEW_BUNDLE_URL=` unset and no release falls back to downloading the three Janelia tables, checking their SHA-256, and building the graph with `flybrain/graph.py`. Needs a C++ compiler for the kernel, ninety lines of C++.
78
+
79
+ ## What is real and what is a model
80
+
81
+ Real: which neuron connects to which, with how many synapses; each cell's predicted transmitter; cell types and optic-lobe column coordinates. A model: identical LIF dynamics for every cell, photoreceptors as spiking cells, the screen-to-receptor projection, the 12 mV standing current on lamina cells, and every mapping from spike count to button. None of the readouts are what those cells do in a living fly.
82
+
83
+ ## Provenance
84
+
85
+ - MaleCNS v1.0, HHMI Janelia FlyEM with Cambridge, MRC LMB and Google Research. CC BY 4.0. Source URLs and digests in `flybrain/paths.py`.
86
+ - LIF constants from Shiu et al., "A Drosophila computational brain model reveals sensorimotor processing", Nature 2024.
87
+ - The choice of DNp20 and DNpe017 as visually responsive descending neurons and the idea of placing photoreceptors by their lamina column follow DOOMFLY (Alex Wormuth). The simulator, importer and retina in `flybrain/` are ours, and reproduce that kernel spike for spike on identical input.
88
+ - Patches and labels: SWE-bench Verified; AlexCuadron/SWE-Bench-Verified-O1-reasoning-high-results.
89
+
90
+ MIT.
@@ -0,0 +1,6 @@
1
+ """flybrain: the complete MaleCNS v1.0 fruit fly nervous system as a spiking
2
+ network, with a screen in front of its eyes."""
3
+ from .brain import Brain, DT
4
+ from .paths import HOME, GRAPH, CELLS, ready
5
+
6
+ __all__ = ['Brain', 'DT', 'HOME', 'GRAPH', 'CELLS', 'ready']
@@ -0,0 +1,120 @@
1
+ """The simulated brain: load the graph, drive the photoreceptors with an
2
+ image, integrate every neuron, hand back spike counts.
3
+
4
+ Sensory model (deliberately simple, stated so it can be argued with):
5
+ * R1-R6 receive linear luminance at their screen position, low-passed with a
6
+ 10 ms time constant, through a saturating current 30 * I / (0.02 + I) mV.
7
+ * R8y receive the green channel and R8p the blue channel the same way.
8
+ * Lamina cells L1/L2/L3/L5 get a constant 12 mV: photoreceptor input to the
9
+ lamina is inhibitory in the fly, so light has to be able to turn them down.
10
+ * Everything else is the connectome.
11
+ """
12
+ from __future__ import annotations
13
+ import ctypes as C
14
+ import json
15
+ import math
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+ import numpy as np
20
+
21
+ from .paths import GRAPH, CELLS, LIB, HOME
22
+
23
+ DT = 0.1
24
+ SOURCE = Path(__file__).with_name('lif.cpp')
25
+
26
+
27
+ def build_library(log=print) -> Path:
28
+ """Compile the kernel once per machine; rebuild if the source changed."""
29
+ stamp = LIB.with_suffix(LIB.suffix + '.json')
30
+ src = SOURCE.read_bytes()
31
+ import hashlib
32
+ digest = hashlib.sha256(src).hexdigest()
33
+ if LIB.exists() and stamp.exists() and json.loads(stamp.read_text()).get('source') == digest:
34
+ return LIB
35
+ HOME.mkdir(parents=True, exist_ok=True)
36
+ compiler = 'clang++' if sys.platform == 'darwin' else 'g++'
37
+ log(f'compiling {SOURCE.name} with {compiler}')
38
+ subprocess.run([compiler, '-O3', '-std=c++17', '-shared', '-fPIC', str(SOURCE), '-o', str(LIB)], check=True)
39
+ stamp.write_text(json.dumps({'source': digest}))
40
+ return LIB
41
+
42
+
43
+ def srgb_to_linear(x: np.ndarray) -> np.ndarray:
44
+ x = x.astype(np.float32) / 255
45
+ return np.where(x <= 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4).astype(np.float32)
46
+
47
+
48
+ def sample_bilinear(channel: np.ndarray, uv: np.ndarray) -> np.ndarray:
49
+ h, w = channel.shape
50
+ x = uv[:, 0] * (w - 1); y = uv[:, 1] * (h - 1)
51
+ x0 = x.astype(int); y0 = y.astype(int); x1 = np.minimum(x0 + 1, w - 1); y1 = np.minimum(y0 + 1, h - 1)
52
+ dx = x - x0; dy = y - y0
53
+ return ((1 - dx) * (1 - dy) * channel[y0, x0] + dx * (1 - dy) * channel[y0, x1]
54
+ + (1 - dx) * dy * channel[y1, x0] + dx * dy * channel[y1, x1]).astype(np.float32)
55
+
56
+
57
+ class Brain:
58
+ def __init__(self, graph: Path = GRAPH, cells: Path = CELLS):
59
+ g = np.load(graph)
60
+ self.ptr = g['ptr'].astype(np.int64); self.post = g['post'].astype(np.int32); self.weight = g['weight'].astype(np.float32)
61
+ self.ids = g['ids']; self.retina = g['retina'].astype(np.int32); self.uv = g['uv'].astype(np.float32)
62
+ self.r8 = g['r8'].astype(np.int32); self.r8_uv = g['r8_uv'].astype(np.float32); self.r8_channel = g['r8_channel'].astype(np.int32)
63
+ self.lamina = g['lamina'].astype(np.int32)
64
+ c = np.load(cells)
65
+ self.type = c['type']; self.superclass = c['superclass']; self.side = c['side']; self.soma = c['soma']
66
+ self.n = len(self.ids)
67
+ self.receptors = np.r_[self.retina, self.r8]
68
+ self.r8y = self.r8[self.r8_channel == 1]; self.r8p = self.r8[self.r8_channel == 2]
69
+ self.kc = np.flatnonzero(np.char.startswith(self.type, 'KC')).astype(np.int32)
70
+ self.readout = {name: np.flatnonzero((self.type == t) & (self.side == s)).astype(np.int32)
71
+ for name, (t, s) in {'DNp20 L': ('DNp20', 'L'), 'DNp20 R': ('DNp20', 'R'), 'DNpe017 L': ('DNpe017', 'L'), 'DNpe017 R': ('DNpe017', 'R')}.items()}
72
+ # State
73
+ self.v = np.full(self.n, -52, np.float32); self.g = np.zeros(self.n, np.float32)
74
+ self.refractory = np.zeros(self.n, np.int16); self.drive = np.zeros(self.n, np.float32); self.drive_seen = np.zeros(self.n, np.float32)
75
+ slots = int(round(1.8 / DT)) + 1
76
+ self.queue = np.zeros((slots, self.n), np.int32); self.queue_count = np.zeros(slots, np.int32)
77
+ self.counts = np.zeros(self.n, np.int32)
78
+ self.active = np.zeros(self.n, np.int32); self.is_active = np.zeros(self.n, np.uint8)
79
+ seed = np.unique(np.r_[self.retina, self.r8, self.lamina])
80
+ self.active[:len(seed)] = seed; self.is_active[seed] = 1; self.n_active = np.array([len(seed)], np.int32)
81
+ self.touched = np.full(self.n, -1, np.int64); self.clock = np.zeros(1, np.int64)
82
+ self.lum = np.zeros(len(self.retina), np.float32); self.r8_light = np.zeros(len(self.r8), np.float32)
83
+ self.sim_ms = 0.0
84
+ lib = C.CDLL(str(build_library()))
85
+ self._advance = lib.flybrain_advance
86
+ self._advance.argtypes = [C.c_int] + [C.c_void_p] * 11 + [C.c_int, C.c_float] + [C.c_void_p] * 5
87
+ self._advance.restype = None
88
+
89
+ def step(self, frame: np.ndarray, ms: float, lamina_mv: float = 12.0, extra: list | None = None, colour: bool = True) -> np.ndarray:
90
+ """Show `frame` (H x W x 3 uint8) for `ms` milliseconds; return spikes per neuron."""
91
+ if frame.ndim != 3 or frame.shape[2] != 3 or frame.dtype != np.uint8:
92
+ raise ValueError('frame must be H x W x 3 uint8')
93
+ steps = int(round(ms / DT))
94
+ if steps < 1:
95
+ raise ValueError('ms too short')
96
+ lin = srgb_to_linear(frame)
97
+ luminance = lin @ np.array([0.2126, 0.7152, 0.0722], np.float32)
98
+ alpha = 1 - math.exp(-ms / 10)
99
+ self.lum += alpha * (np.clip(sample_bilinear(luminance, self.uv), 0, 1) - self.lum)
100
+ h, w = frame.shape[:2]
101
+ xi = np.minimum((self.r8_uv[:, 0] * (w - 1)).astype(int), w - 1); yi = np.minimum((self.r8_uv[:, 1] * (h - 1)).astype(int), h - 1)
102
+ channel_light = lin[yi, xi, self.r8_channel]
103
+ self.r8_light += alpha * (channel_light - self.r8_light)
104
+ self.drive.fill(0)
105
+ self.drive[self.lamina] = lamina_mv
106
+ self.drive[self.retina] = 30 * self.lum / (0.02 + self.lum)
107
+ if colour:
108
+ self.drive[self.r8] += 30 * self.r8_light / (0.02 + self.r8_light)
109
+ for idx, mv in (extra or []):
110
+ self.drive[idx] += mv
111
+ self.counts.fill(0)
112
+ arrays = [self.ptr, self.post, self.weight, self.v, self.g, self.refractory, self.drive, self.drive_seen,
113
+ self.queue, self.queue_count, self.clock]
114
+ self._advance(self.n, *[a.ctypes.data for a in arrays], steps, DT,
115
+ *[a.ctypes.data for a in (self.counts, self.active, self.is_active, self.n_active, self.touched)])
116
+ self.sim_ms += steps * DT
117
+ return self.counts.copy()
118
+
119
+ def rate(self, counts: np.ndarray, idx: np.ndarray, ms: float) -> float:
120
+ return float(counts[idx].sum() * 1000 / ms)
@@ -0,0 +1,143 @@
1
+ """Build the simulation graph from the MaleCNS v1.0 tables.
2
+
3
+ Nodes: every body with an assigned neuronal superclass that is not glia.
4
+ Edges: every released connection between retained nodes, weight = synapse
5
+ count x 0.275 mV x transmitter sign (acetylcholine +; GABA, glutamate,
6
+ histamine -; anything else +). Nothing is thresholded away.
7
+
8
+ Retina: R1-R6 photoreceptors have no soma in the volume, so each is placed at
9
+ the optic-lobe column of the lamina cells (L1, L2, L3) it contacts most, and
10
+ the hexagonal column grid is unrolled onto the screen: left eye on the left
11
+ 60 percent, right eye mirrored on the right 60 percent. R8 (colour) cells
12
+ get their column from the column-annotated cells they project to.
13
+
14
+ Outputs graph.npz (ptr, post, weight, ids, retina, uv, r8, r8_uv, r8_channel,
15
+ lamina) and cells.npz (type, superclass, side per node).
16
+ """
17
+ from __future__ import annotations
18
+ import json
19
+ import numpy as np
20
+ import pyarrow as pa
21
+ import pyarrow.feather as feather
22
+ import pyarrow.ipc as ipc
23
+
24
+ from .paths import SOURCE, GRAPH, CELLS, HOME
25
+
26
+ CONTACT_MV = 0.275
27
+ INHIBITORY = {'gaba', 'glutamate', 'histamine'}
28
+
29
+
30
+ def transmitter_sign(label) -> int:
31
+ tokens = set(str(label).lower().split(','))
32
+ excitatory = 'acetylcholine' in tokens
33
+ inhibitory = bool(tokens & INHIBITORY)
34
+ if excitatory == inhibitory: # both, neither, modulator-only: leave the edge active and positive
35
+ return 1
36
+ return 1 if excitatory else -1
37
+
38
+
39
+ def hex_to_plane(h1, h2):
40
+ return np.column_stack([h1 - 0.5 * h2, np.sqrt(3) / 2 * h2])
41
+
42
+
43
+ def viewport(xy, side):
44
+ """Axial hex coordinates -> screen uv in [0,1]. Left eye covers x 0..0.6, right eye 0.4..1.0 mirrored."""
45
+ lo, span = xy.min(axis=0), np.ptp(xy, axis=0)
46
+ z = (xy - lo) / span
47
+ u = 0.6 * z[:, 0] if side == 'L' else 0.4 + 0.6 * (1 - z[:, 0])
48
+ return np.column_stack([u, 1 - z[:, 1]])
49
+
50
+
51
+ def build(log=print):
52
+ a = feather.read_table(SOURCE / 'annotations.feather').to_pandas()
53
+ nt = feather.read_table(SOURCE / 'neurotransmitters.feather').to_pandas().set_index('body')
54
+ keep = a.superclass.notna() & a.superclass.astype(str).ne('') & ~a.status.eq('Glia')
55
+ nodes = a.loc[keep].copy()
56
+ nodes['nt'] = nodes.bodyId.map(nt.consensus_nt)
57
+ nodes = nodes.sort_values('bodyId', ignore_index=True)
58
+ ids = nodes.bodyId.to_numpy(np.int64)
59
+ n = len(ids)
60
+ log(f'{n} neurons retained')
61
+
62
+ signs = np.array([transmitter_sign(x) for x in nodes.nt], np.int8)
63
+ reader = ipc.open_file(pa.memory_map(str(SOURCE / 'edges.feather'), 'r'))
64
+ pre_all, post_all, cnt_all = [], [], []
65
+ for b in range(reader.num_record_batches):
66
+ batch = reader.get_batch(b)
67
+ pre = batch.column('body_pre').to_numpy(zero_copy_only=False).astype(np.int64)
68
+ post = batch.column('body_post').to_numpy(zero_copy_only=False).astype(np.int64)
69
+ cnt = batch.column('weight').to_numpy(zero_copy_only=False)
70
+ i = np.searchsorted(ids, pre); j = np.searchsorted(ids, post)
71
+ ok = (i < n) & (j < n)
72
+ ok &= ids[np.minimum(i, n - 1)] == pre
73
+ ok &= ids[np.minimum(j, n - 1)] == post
74
+ pre_all.append(i[ok]); post_all.append(j[ok]); cnt_all.append(cnt[ok].astype(np.int64))
75
+ pre = np.concatenate(pre_all); post = np.concatenate(post_all); cnt = np.concatenate(cnt_all)
76
+ order = np.argsort(pre, kind='stable')
77
+ ptr = np.r_[0, np.cumsum(np.bincount(pre, minlength=n))].astype(np.int64)
78
+ post = post[order].astype(np.int32)
79
+ weight = (cnt[order].astype(np.float32) * signs[pre[order]] * CONTACT_MV).astype(np.float32)
80
+ log(f'{len(post)} edges, {int(cnt.sum())} synaptic contacts')
81
+
82
+ ann = a.set_index('bodyId').loc[ids]
83
+ types = ann.type.fillna('').astype(str).to_numpy()
84
+ side = ann.rootSide.fillna(ann.somaSide).fillna('').astype(str).to_numpy()
85
+ hex1 = ann.assignedOlHex1.to_numpy(float); hex2 = ann.assignedOlHex2.to_numpy(float)
86
+
87
+ # R1-R6: modal lamina column by contact count.
88
+ receptor = types == 'R1-R6'
89
+ anchor = np.isin(types, ['L1', 'L2', 'L3']) & ~np.isnan(hex1)
90
+ votes: dict[int, dict[tuple, int]] = {}
91
+ src = pre[order]
92
+ sel = receptor[src] & anchor[post]
93
+ for i, j, w in zip(src[sel], post[sel], cnt[order][sel]):
94
+ votes.setdefault(int(i), {})
95
+ key = (hex1[j], hex2[j]); votes[int(i)][key] = votes[int(i)].get(key, 0) + int(w)
96
+ retina = np.array(sorted(votes), np.int32)
97
+ hexes = np.array([max(votes[i], key=votes[i].get) for i in retina])
98
+ uv = np.empty((len(retina), 2), np.float32)
99
+ xy = hex_to_plane(hexes[:, 0], hexes[:, 1])
100
+ for s in 'LR':
101
+ m = side[retina] == s
102
+ uv[m] = viewport(xy[m], s)
103
+ log(f'{len(retina)} of {receptor.sum()} R1-R6 cells placed on the screen')
104
+
105
+ # R8: modal column over all column-annotated targets, weighted by |weight|; same viewport fit as R1-R6.
106
+ r8_all = np.flatnonzero(np.isin(types, ['R8p', 'R8y']))
107
+ r8, r8_hex = [], []
108
+ for i in r8_all:
109
+ e = np.arange(ptr[i], ptr[i + 1]); tg = post[e]
110
+ ok = ~np.isnan(hex1[tg]) & ~np.isnan(hex2[tg])
111
+ if not ok.any():
112
+ continue
113
+ v: dict[tuple, float] = {}
114
+ for ee, j in zip(e[ok], tg[ok]):
115
+ k = (hex1[j], hex2[j]); v[k] = v.get(k, 0.0) + abs(float(weight[ee]))
116
+ r8.append(i); r8_hex.append(max(v, key=v.get))
117
+ r8 = np.array(r8, np.int32); r8_hex = np.array(r8_hex)
118
+ r8_xy = hex_to_plane(r8_hex[:, 0], r8_hex[:, 1])
119
+ r8_uv = np.empty((len(r8), 2), np.float32)
120
+ for s in 'LR':
121
+ base = side[retina] == s; m = side[r8] == s
122
+ lo, span = xy[base].min(axis=0), np.ptp(xy[base], axis=0)
123
+ z = (r8_xy[m] - lo) / span
124
+ u = 0.6 * z[:, 0] if s == 'L' else 0.4 + 0.6 * (1 - z[:, 0])
125
+ r8_uv[m] = np.column_stack([u, 1 - z[:, 1]])
126
+ r8_uv = np.clip(r8_uv, 0, 1)
127
+ r8_channel = np.where(types[r8] == 'R8p', 2, 1).astype(np.int32) # 2 = blue-sensitive, 1 = green/yellow-sensitive
128
+ log(f'{len(r8)} R8 cells placed ({int((r8_channel == 1).sum())} R8y, {int((r8_channel == 2).sum())} R8p)')
129
+
130
+ lamina = np.flatnonzero(np.isin(types, ['L1', 'L2', 'L3', 'L5'])).astype(np.int32)
131
+ HOME.mkdir(parents=True, exist_ok=True)
132
+ np.savez(GRAPH, ptr=ptr, post=post, weight=weight, ids=ids, retina=retina, uv=uv, r8=r8, r8_uv=r8_uv,
133
+ r8_channel=r8_channel, lamina=lamina, retina_hex=hexes.astype(np.float32), r8_hex=r8_hex.astype(np.float32))
134
+ np.savez(CELLS, type=types.astype('U48'), superclass=nodes.superclass.fillna('unassigned').astype(str).to_numpy().astype('U48'),
135
+ side=side.astype('U4'), hex=np.column_stack([hex1, hex2]).astype(np.float32), soma=np.array([list(s) if s is not None and len(s) == 3 else [np.nan] * 3 for s in ann.somaLocation], np.float32))
136
+ manifest = {'dataset': 'MaleCNS v1.0', 'neurons': int(n), 'edges': int(len(post)), 'contacts': int(cnt.sum()),
137
+ 'retina': int(len(retina)), 'r8': int(len(r8)), 'lamina': int(len(lamina))}
138
+ (HOME / 'manifest.json').write_text(json.dumps(manifest, indent=1))
139
+ return manifest
140
+
141
+
142
+ if __name__ == '__main__':
143
+ print(build())
@@ -0,0 +1,89 @@
1
+ // flybrain: event-driven leaky integrate-and-fire over a CSR graph.
2
+ //
3
+ // Model (Shiu et al. 2024 constants): membrane tau 20 ms, synaptic tau 5 ms,
4
+ // rest -52 mV, threshold -45 mV, refractory 2.2 ms, axonal delay 1.8 ms,
5
+ // one synaptic contact = 0.275 mV (already folded into the edge weights).
6
+ // Between events a neuron's state has a closed form, so a cell is only
7
+ // touched when something arrives or when it could possibly cross threshold.
8
+ // Every edge in the graph is delivered; nothing is pruned.
9
+ #include <cmath>
10
+ #include <cstdint>
11
+
12
+ namespace {
13
+ constexpr float REST = -52.f, THRESH = -45.f, TAU_M = 20.f, TAU_G = 5.f;
14
+ constexpr float DELAY_MS = 1.8f, REFRACTORY_MS = 2.2f;
15
+ constexpr int TABLE = 1024;
16
+
17
+ struct Decay { float m[TABLE], g[TABLE]; float dt; };
18
+
19
+ inline void fill(Decay& d, float dt) {
20
+ d.dt = dt;
21
+ for (int k = 0; k < TABLE; k++) { d.m[k] = std::exp(-dt * k / TAU_M); d.g[k] = std::exp(-dt * k / TAU_G); }
22
+ }
23
+ } // namespace
24
+
25
+ extern "C" void flybrain_advance(
26
+ int n, const int64_t* ptr, const int32_t* post, const float* weight,
27
+ float* v, float* g, int16_t* refractory, const float* drive, float* drive_seen,
28
+ int32_t* queue, int32_t* queue_count, int64_t* clock, int steps, float dt,
29
+ int32_t* counts, int32_t* active, uint8_t* is_active, int32_t* n_active, int64_t* touched) {
30
+ Decay decay; fill(decay, dt);
31
+ const int delay = std::lround(DELAY_MS / dt), refractory_steps = std::lround(REFRACTORY_MS / dt);
32
+ const int slots = delay + 1;
33
+
34
+ // Bring cell i up to time `now` under constant input `current`, honouring any refractory period.
35
+ auto settle = [&](int i, int64_t now, float current) {
36
+ int64_t elapsed = now - touched[i];
37
+ if (elapsed <= 0) return;
38
+ const int frozen = refractory[i] > 0 ? refractory[i] - 1 : 0;
39
+ const int skipped = static_cast<int>(elapsed < frozen ? elapsed : frozen);
40
+ refractory[i] = elapsed >= refractory[i] ? 0 : static_cast<int16_t>(refractory[i] - elapsed);
41
+ elapsed -= skipped;
42
+ if (elapsed > 0) {
43
+ const float a = elapsed < TABLE ? decay.m[elapsed] : std::exp(-dt * elapsed / TAU_M);
44
+ const float b = elapsed < TABLE ? decay.g[elapsed] : std::exp(-dt * elapsed / TAU_G);
45
+ v[i] = REST + (v[i] - REST) * a + current * (1.f - a) + g[i] * (a - b) / 3.f;
46
+ g[i] *= b;
47
+ }
48
+ touched[i] = now;
49
+ };
50
+ auto wake = [&](int i) { if (!is_active[i]) { is_active[i] = 1; active[(*n_active)++] = i; } };
51
+
52
+ // A changed input current ends the old closed-form segment first.
53
+ for (int i = 0; i < n; i++) {
54
+ if (drive[i] != drive_seen[i]) { settle(i, *clock - 1, drive_seen[i]); drive_seen[i] = drive[i]; wake(i); }
55
+ }
56
+
57
+ for (int t = 0; t < steps; t++, (*clock)++) {
58
+ const int slot = *clock % slots, arrival = (*clock + delay) % slots;
59
+ int kept = 0;
60
+ const int before = *n_active;
61
+ for (int k = 0; k < before; k++) {
62
+ const int i = active[k];
63
+ settle(i, *clock, drive[i]);
64
+ if (refractory[i] == 0 && v[i] > THRESH) { queue[arrival * n + queue_count[arrival]++] = i; counts[i]++; }
65
+ // The membrane relaxes toward drive + g. If neither can reach threshold and the
66
+ // cell is below it now, nothing can fire until a new event arrives.
67
+ const bool reachable = v[i] > THRESH || drive[i] > 7.f || drive[i] + g[i] > 7.f;
68
+ if (reachable) active[kept++] = i; else is_active[i] = 0;
69
+ }
70
+ *n_active = kept;
71
+ // Spikes emitted `delay` steps ago land now.
72
+ for (int q = 0; q < queue_count[slot]; q++) {
73
+ const int i = queue[slot * n + q];
74
+ for (int64_t e = ptr[i]; e < ptr[i + 1]; e++) {
75
+ const int j = post[e];
76
+ settle(j, *clock, drive[j]);
77
+ if (refractory[j] == 0) { g[j] += weight[e]; wake(j); }
78
+ }
79
+ }
80
+ queue_count[slot] = 0;
81
+ // Cells that just fired reset and go refractory.
82
+ for (int q = 0; q < queue_count[arrival]; q++) {
83
+ const int i = queue[arrival * n + q];
84
+ v[i] = REST; g[i] = 0.f; refractory[i] = static_cast<int16_t>(refractory_steps);
85
+ }
86
+ }
87
+ // Leave every cell materialised at the end of the interval.
88
+ for (int i = 0; i < n; i++) settle(i, *clock - 1, drive[i]);
89
+ }
@@ -0,0 +1,71 @@
1
+ """Where flyreview keeps the connectome: FLYREVIEW_HOME, else ~/.cache/flyreview."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.request
8
+ from pathlib import Path
9
+
10
+ HOME = Path(os.environ.get('FLYREVIEW_HOME', Path.home() / '.cache' / 'flyreview'))
11
+ SOURCE = HOME / 'malecns_v1'
12
+ GRAPH = HOME / 'graph.npz'
13
+ CELLS = HOME / 'cells.npz'
14
+ LIB = HOME / ('libflybrain.dylib' if sys.platform == 'darwin' else 'libflybrain.so')
15
+
16
+ # MaleCNS v1.0 flat connectome, HHMI Janelia FlyEM, CC BY 4.0. Digests pinned on 2026-09-11.
17
+ FILES = {
18
+ 'annotations.feather': ('https://storage.googleapis.com/flyem-male-cns/v1.0/connectome-data/flat-connectome/body-annotations-male-cns-v1.0-minconf-0.5.feather',
19
+ '2177e246113e4cfbf1e7772ec37c6da1955ff22e8063d0b1f833101f99a9a3b2'),
20
+ 'neurotransmitters.feather': ('https://storage.googleapis.com/flyem-male-cns/v1.0/connectome-data/flat-connectome/body-neurotransmitters-male-cns-v1.0.feather',
21
+ '95c9289220663abeb3409f3ad9e5a7f8a53f8093f5139d15502cd08da8879621'),
22
+ 'edges.feather': ('https://storage.googleapis.com/flyem-male-cns/v1.0/connectome-data/flat-connectome/connectome-weights-male-cns-v1.0-minconf-0.5.feather',
23
+ 'e35da783d1c686b2b58b3b87cd6a403ae43bfcfba8bff28e08ef752c1a56afc1'),
24
+ }
25
+ BUNDLE_URL = os.environ.get('FLYREVIEW_BUNDLE_URL', 'https://github.com/brekkylab/flyreview/releases/download/graph-v2/flyreview-graph.tar')
26
+
27
+
28
+ def sha256(path: Path) -> str:
29
+ h = hashlib.sha256()
30
+ with path.open('rb') as f:
31
+ for block in iter(lambda: f.read(1 << 24), b''):
32
+ h.update(block)
33
+ return h.hexdigest()
34
+
35
+
36
+ def download_sources(log=print):
37
+ """Fetch the three Janelia tables (about 1.1 GB) and verify them."""
38
+ SOURCE.mkdir(parents=True, exist_ok=True)
39
+ for name, (url, digest) in FILES.items():
40
+ target = SOURCE / name
41
+ if not target.exists():
42
+ log(f'downloading {name}')
43
+ part = target.with_suffix('.part')
44
+ urllib.request.urlretrieve(url, part)
45
+ part.replace(target)
46
+ got = sha256(target)
47
+ if got != digest:
48
+ raise SystemExit(f'{name}: checksum {got} does not match the pinned {digest}')
49
+ log(f'{name} ok')
50
+ (SOURCE / 'digests.json').write_text(json.dumps({k: v[1] for k, v in FILES.items()}, indent=1))
51
+
52
+
53
+ def fetch_bundle(log=print) -> bool:
54
+ """Prepared graph from the release. Returns False if it is not reachable."""
55
+ import tarfile
56
+ HOME.mkdir(parents=True, exist_ok=True)
57
+ tar = HOME / 'bundle.tar'
58
+ try:
59
+ log(f'fetching {BUNDLE_URL}')
60
+ urllib.request.urlretrieve(BUNDLE_URL, tar)
61
+ except Exception as e: # noqa: BLE001
62
+ log(f'bundle not available ({e}); building from the Janelia tables instead')
63
+ return False
64
+ with tarfile.open(tar) as t:
65
+ t.extractall(HOME)
66
+ tar.unlink()
67
+ return GRAPH.exists() and CELLS.exists()
68
+
69
+
70
+ def ready() -> bool:
71
+ return GRAPH.exists() and CELLS.exists()
@@ -0,0 +1 @@
1
+ """A simulated fruit fly brain (MaleCNS v1.0) reviews pull requests."""
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ main()