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.
- flyreview-0.1.0/LICENSE +9 -0
- flyreview-0.1.0/PKG-INFO +104 -0
- flyreview-0.1.0/README.md +90 -0
- flyreview-0.1.0/flybrain/__init__.py +6 -0
- flyreview-0.1.0/flybrain/brain.py +120 -0
- flyreview-0.1.0/flybrain/graph.py +143 -0
- flyreview-0.1.0/flybrain/lif.cpp +89 -0
- flyreview-0.1.0/flybrain/paths.py +71 -0
- flyreview-0.1.0/flyreview/__init__.py +1 -0
- flyreview-0.1.0/flyreview/__main__.py +2 -0
- flyreview-0.1.0/flyreview/bench.py +68 -0
- flyreview-0.1.0/flyreview/cli.py +138 -0
- flyreview-0.1.0/flyreview/composite.py +234 -0
- flyreview-0.1.0/flyreview/eyefeatures.py +72 -0
- flyreview-0.1.0/flyreview/panel.py +125 -0
- flyreview-0.1.0/flyreview/render.py +149 -0
- flyreview-0.1.0/flyreview/review.py +182 -0
- flyreview-0.1.0/flyreview/trace.py +51 -0
- flyreview-0.1.0/flyreview.egg-info/PKG-INFO +104 -0
- flyreview-0.1.0/flyreview.egg-info/SOURCES.txt +25 -0
- flyreview-0.1.0/flyreview.egg-info/dependency_links.txt +1 -0
- flyreview-0.1.0/flyreview.egg-info/entry_points.txt +2 -0
- flyreview-0.1.0/flyreview.egg-info/requires.txt +4 -0
- flyreview-0.1.0/flyreview.egg-info/top_level.txt +2 -0
- flyreview-0.1.0/pyproject.toml +21 -0
- flyreview-0.1.0/setup.cfg +4 -0
- flyreview-0.1.0/tests/test_flyreview.py +70 -0
flyreview-0.1.0/LICENSE
ADDED
|
@@ -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.
|
flyreview-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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,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."""
|