logo Netzschleuder network catalog, repository, and centrifuge

History of flywire

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/flywire/__init__.py b/entries/flywire/__init__.py
index 41834f3..dc8c448 100644
--- a/entries/flywire/__init__.py
+++ b/entries/flywire/__init__.py
@@ -57,7 +57,7 @@ citation = [('S. Dorkenwald et al., "Neuronal wiring diagram of an adult brain."
             ('J. Buhmann et al., "Automatic detection of synaptic partners in a whole-brain Drosophila electron microscopy data set." Nature Methods 18, 771-774 (2021)', 'https://doi.org/10.1038/s41592-021-01183-7'),
             ('L. Heinrich, J. Funke, C. Pape, J. Nunez-Iglesias, S. Saalfeld, "Synaptic cleft segmentation in non-isotropic volume electron microscopy of the complete Drosophila brain." Medical Image Computing and Computer Assisted Intervention (MICCAI 2018), 317-325 (2018)', 'https://doi.org/10.1007/978-3-030-00934-2_36'),
             ('N. Eckstein et al., "Neurotransmitter classification from electron microscopy images at synaptic sites in Drosophila melanogaster." Cell 187, 2574-2594 (2024)', 'https://doi.org/10.1016/j.cell.2024.03.016')]
-icon_hash = None
+date_added = '2026-10-27'
 upstream_license = ("CC BY 4.0", "https://creativecommons.org/licenses/by/4.0/")
 upstream_prefix = 'https://zenodo.org/records/10676866/files'
 # Supplementary Data 1 of Schlegel et al. (2024), the neuron annotations
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/flywire/__init__.py b/entries/flywire/__init__.py
index 4322f44..41834f3 100644
--- a/entries/flywire/__init__.py
+++ b/entries/flywire/__init__.py
@@ -34,21 +34,21 @@ The neuron annotations are those published with the paper of Schlegel et al. (Su
 
 The networks have the following properties:
 
-- `root_id`: the neuron's identifier in the FlyWire data set (root ID of the v783 release).
-- `flow`: whether the neuron is `intrinsic` to the brain, `afferent` or `efferent`.
-- `super_class`: the coarsest neuron classification (e.g. `central`, `optic`, `sensory`, `visual_projection`, `ascending`, `descending`, `motor`, `endocrine`).
-- `cell_class`, `cell_sub_class`: finer neuron classes (e.g. `Kenyon_Cell`, `ME>LO`); empty if not assigned.
-- `cell_type`: the cell type of the neuron, either new or a refinement of a hemibrain type; empty if not assigned.
-- `hemibrain_type`: the hemibrain cell type(s) matched to the neuron (see the `fly_hemibrain` entry); empty if not assigned.
-- `hemilineage`: the developmental hemilineage of the neuron (Ito-Lee nomenclature); empty if not assigned.
-- `side`: the side of the brain where the neuron's soma (or, for sensory neurons, its entry point) is located (`left`, `right`, `center`).
-- `nerve`: the nerve through which the neuron enters or exits the brain, for afferent and efferent neurons; empty otherwise.
-- `top_nt`: the neurotransmitter predicted for the neuron from its synapses (`acetylcholine`, `glutamate`, `gaba`, `dopamine`, `serotonin`, `octopamine`); empty if not assigned.
-- `top_nt_conf`: the confidence of the neurotransmitter prediction (NaN if not assigned).
-- `neuropil` (edges): the neuropil in which the synapses of the connection are located (e.g. `ME_L` for the medulla of the left hemisphere; `UNASGD` if unassigned).
-- `weight` (edges): the number of synapses from $i$ to $j$ in that neuropil.
-- `gaba`, `ach`, `glut`, `oct`, `ser`, `da` (edges): the probabilities that the synapses of the connection release GABA, acetylcholine, glutamate, octopamine, serotonin or dopamine, predicted from the electron microscopy images and averaged over the synapses.
-- `cleft_score`, `connection_score` (edges, `synapses_all` only): the cleft score (from the synaptic cleft segmentation of Heinrich et al., used for the threshold) and the connection score (from the synaptic partner detection of Buhmann et al.) of the synapses, averaged over the synapses; higher is better."""
+- (Node) `root_id`: the neuron's identifier in the FlyWire data set (root ID of the v783 release).
+- (Node) `flow`: whether the neuron is `intrinsic` to the brain, `afferent` or `efferent`.
+- (Node) `super_class`: the coarsest neuron classification (e.g. `central`, `optic`, `sensory`, `visual_projection`, `ascending`, `descending`, `motor`, `endocrine`).
+- (Node) `cell_class`, `cell_sub_class`: finer neuron classes (e.g. `Kenyon_Cell`, `ME>LO`); empty if not assigned.
+- (Node) `cell_type`: the cell type of the neuron, either new or a refinement of a hemibrain type; empty if not assigned.
+- (Node) `hemibrain_type`: the hemibrain cell type(s) matched to the neuron (see the `fly_hemibrain` entry); empty if not assigned.
+- (Node) `hemilineage`: the developmental hemilineage of the neuron (Ito-Lee nomenclature); empty if not assigned.
+- (Node) `side`: the side of the brain where the neuron's soma (or, for sensory neurons, its entry point) is located (`left`, `right`, `center`).
+- (Node) `nerve`: the nerve through which the neuron enters or exits the brain, for afferent and efferent neurons; empty otherwise.
+- (Node) `top_nt`: the neurotransmitter predicted for the neuron from its synapses (`acetylcholine`, `glutamate`, `gaba`, `dopamine`, `serotonin`, `octopamine`); empty if not assigned.
+- (Node) `top_nt_conf`: the confidence of the neurotransmitter prediction (NaN if not assigned).
+- (Edge) `neuropil` (edges): the neuropil in which the synapses of the connection are located (e.g. `ME_L` for the medulla of the left hemisphere; `UNASGD` if unassigned).
+- (Edge) `weight` (edges): the number of synapses from $i$ to $j$ in that neuropil.
+- (Edge) `gaba`, `ach`, `glut`, `oct`, `ser`, `da` (edges): the probabilities that the synapses of the connection release GABA, acetylcholine, glutamate, octopamine, serotonin or dopamine, predicted from the electron microscopy images and averaged over the synapses.
+- (Edge) `cleft_score`, `connection_score` (edges, `synapses_all` only): the cleft score (from the synaptic cleft segmentation of Heinrich et al., used for the threshold) and the connection score (from the synaptic partner detection of Buhmann et al.) of the synapses, averaged over the synapses; higher is better."""
 tags = ['Biological', 'Connectome', 'Weighted', 'Multigraph', 'Metadata']
 url = ['https://zenodo.org/records/10676866',
        'https://doi.org/10.1038/s41586-024-07686-5']
[entries] New entry: flywire by Tiago de Paula Peixoto 330d952
diff --git a/entries/flywire/__init__.py b/entries/flywire/__init__.py
new file mode 100644
index 0000000..4322f44
--- /dev/null
+++ b/entries/flywire/__init__.py
@@ -0,0 +1,281 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2020-2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .. import *
+import numpy as np
+import pandas as pd
+import pyarrow.feather
+
+title = "FlyWire whole adult fly brain (2024)"
+description = r"""The connectome of the entire brain of an adult female fruit fly *Drosophila melanogaster*, reconstructed by the FlyWire Consortium from the serial-section transmission electron microscopy volume "Full Adult Fly Brain" (FAFB), with automated segmentation and synapse detection followed by extensive manual proofreading by a community of researchers and citizen scientists. This is the public release v783 of the data set. Nodes are the 139,255 proofread neurons, and a directed edge $(i, j)$ indicates chemical synapses from neuron $i$ (presynaptic) to neuron $j$ (postsynaptic) within a given brain region (neuropil).
+
+Synapses were detected automatically, and only those with a cleft score of at least 50 are included, as in the authors' analyses. As in the `fly_hemibrain` entry, the connections between a pair of neurons are split according to the neuropil where the synapses reside, so that the same pair of neurons may be connected by several parallel edges, one per neuropil. There are three networks:
+
+- `min5`: following the authors, who consider a pair of neurons to be connected if there are at least five synapses between them (which makes false-positive connections very unlikely), only the pairs of neurons with at least five synapses in total (summed over all neuropils) are included: these are 2,700,513 of the 15,091,983 connected pairs, carrying 34.2 million of the 54.5 million synapses between proofread neurons (the weight of an individual edge, in one neuropil, may be smaller than five).
+- `all`: all connections, without threshold on the number of synapses.
+- `synapses_all`: the same as `all`, but aggregated here from the table of all the individual synapses released with the data (130,054,535 synapses, with their positions, cleft scores and neurotransmitter predictions), instead of the table of connections between proofread neurons. This table is not unthresholded: the synapses with a cleft score below the threshold were not released (all the scores in the table are above 50), so that it contains the same synapses, and the network differs from `all` only in details. Of these synapses, the 54,492,970 between two proofread neurons are included, while the 75,561,565 that involve at least one segment that is not a proofread neuron (e.g. fragments that could not be attached to a neuron) are left out. The network has 16,848,018 edges, 21 more than `all` (in `ME_R`, with 48 synapses in total), which are missing from the table of connections. The synapses without a neuropil (marked `None` in the table) are assigned to `UNASGD`, as in the other networks. The neurotransmitter probabilities are the averages of the predictions for the individual synapses; they differ from those of the other networks for the outgoing connections of the Kenyon cells (whose predictions are mostly dopamine), for which the table of connections gives an acetylcholine probability of one. The predictions are missing for 5,347 synapses, which are left out of the averages (they are NaN for the 370 edges where they are missing for all the synapses). The edges also carry the averages of the cleft and connection scores of the synapses; the positions of the individual synapses are not included.
+
+The neuron annotations are those published with the paper of Schlegel et al. (Supplementary Data 1), with cell types matched to the hemibrain and new ones; all networks have the same nodes, so that in `min5` the neurons that make no connection with at least five synapses are isolated nodes.
+
+The networks have the following properties:
+
+- `root_id`: the neuron's identifier in the FlyWire data set (root ID of the v783 release).
+- `flow`: whether the neuron is `intrinsic` to the brain, `afferent` or `efferent`.
+- `super_class`: the coarsest neuron classification (e.g. `central`, `optic`, `sensory`, `visual_projection`, `ascending`, `descending`, `motor`, `endocrine`).
+- `cell_class`, `cell_sub_class`: finer neuron classes (e.g. `Kenyon_Cell`, `ME>LO`); empty if not assigned.
+- `cell_type`: the cell type of the neuron, either new or a refinement of a hemibrain type; empty if not assigned.
+- `hemibrain_type`: the hemibrain cell type(s) matched to the neuron (see the `fly_hemibrain` entry); empty if not assigned.
+- `hemilineage`: the developmental hemilineage of the neuron (Ito-Lee nomenclature); empty if not assigned.
+- `side`: the side of the brain where the neuron's soma (or, for sensory neurons, its entry point) is located (`left`, `right`, `center`).
+- `nerve`: the nerve through which the neuron enters or exits the brain, for afferent and efferent neurons; empty otherwise.
+- `top_nt`: the neurotransmitter predicted for the neuron from its synapses (`acetylcholine`, `glutamate`, `gaba`, `dopamine`, `serotonin`, `octopamine`); empty if not assigned.
+- `top_nt_conf`: the confidence of the neurotransmitter prediction (NaN if not assigned).
+- `neuropil` (edges): the neuropil in which the synapses of the connection are located (e.g. `ME_L` for the medulla of the left hemisphere; `UNASGD` if unassigned).
+- `weight` (edges): the number of synapses from $i$ to $j$ in that neuropil.
+- `gaba`, `ach`, `glut`, `oct`, `ser`, `da` (edges): the probabilities that the synapses of the connection release GABA, acetylcholine, glutamate, octopamine, serotonin or dopamine, predicted from the electron microscopy images and averaged over the synapses.
+- `cleft_score`, `connection_score` (edges, `synapses_all` only): the cleft score (from the synaptic cleft segmentation of Heinrich et al., used for the threshold) and the connection score (from the synaptic partner detection of Buhmann et al.) of the synapses, averaged over the synapses; higher is better."""
+tags = ['Biological', 'Connectome', 'Weighted', 'Multigraph', 'Metadata']
+url = ['https://zenodo.org/records/10676866',
+       'https://doi.org/10.1038/s41586-024-07686-5']
+citation = [('S. Dorkenwald et al., "Neuronal wiring diagram of an adult brain." Nature 634, 124-138 (2024)', 'https://doi.org/10.1038/s41586-024-07558-y'),
+            ('P. Schlegel et al., "Whole-brain annotation and multi-connectome cell typing of Drosophila." Nature 634, 139-152 (2024)', 'https://doi.org/10.1038/s41586-024-07686-5'),
+            ('J. Buhmann et al., "Automatic detection of synaptic partners in a whole-brain Drosophila electron microscopy data set." Nature Methods 18, 771-774 (2021)', 'https://doi.org/10.1038/s41592-021-01183-7'),
+            ('L. Heinrich, J. Funke, C. Pape, J. Nunez-Iglesias, S. Saalfeld, "Synaptic cleft segmentation in non-isotropic volume electron microscopy of the complete Drosophila brain." Medical Image Computing and Computer Assisted Intervention (MICCAI 2018), 317-325 (2018)', 'https://doi.org/10.1007/978-3-030-00934-2_36'),
+            ('N. Eckstein et al., "Neurotransmitter classification from electron microscopy images at synaptic sites in Drosophila melanogaster." Cell 187, 2574-2594 (2024)', 'https://doi.org/10.1016/j.cell.2024.03.016')]
+icon_hash = None
+upstream_license = ("CC BY 4.0", "https://creativecommons.org/licenses/by/4.0/")
+upstream_prefix = 'https://zenodo.org/records/10676866/files'
+# Supplementary Data 1 of Schlegel et al. (2024), the neuron annotations
+annot_prefix = 'https://media.springernature.com/original/springer-static/esm/art%3A10.1038%2Fs41586-024-07686-5/MediaObjects'
+annot_fname = '41586_2024_7686_MOESM5_ESM.tsv'
+files = [(('proofread_connections_783.feather', 'proofread_root_ids_783.npy',
+           annot_fname), alt, None) for alt in ["min5", "all"]]
+# all the synapses, from which the network is aggregated
+files += [(('flywire_synapses_783.feather', 'proofread_root_ids_783.npy',
+            annot_fname), "synapses_all", None)]
+
+# minimum number of synapses between two neurons for them to be connected
+min_synapses = {"min5": 5, "all": 0}
+
+def fetch_upstream(force=False):
+    name = __name__.split(".")[-1]
+    fetch_upstream_files(name, upstream_prefix,
+                         [(fnames[:2], alt, fmt) for fnames, alt, fmt in files],
+                         force)
+    fetch_upstream_files(name, annot_prefix,
+                         [(fnames[2:], alt, fmt) for fnames, alt, fmt in files],
+                         force)
+
+vprops = ["flow", "super_class", "cell_class", "cell_sub_class", "cell_type",
+          "hemibrain_type", ("ito_lee_hemilineage", "hemilineage"), "side",
+          "nerve", "top_nt"]
+nts = ["gaba", "ach", "glut", "oct", "ser", "da"]
+
+# the per-synapse values averaged over the synapses of each edge, in the table
+# of all synapses
+syn_vals = ["cleft_score", "connection_score"] + nts
+
+def aggregate_synapses(f, ids, stats=None):
+    """Aggregate the synapses of the table of all synapses (a feather file,
+    read as a stream from `f`) whose both partners are in `ids`, per (pre,
+    post, neuropil). Returns the arrays of the presynaptic and postsynaptic
+    vertex indices, the neuropil names, the number of synapses and the means of
+    the `syn_vals` over the synapses where they are not missing (as columns,
+    NaN if missing for all the synapses), sorted by presynaptic root id, postsynaptic
+    root id and neuropil (as the other networks). If `stats` is a dict, counts
+    of the synapses are stored in it."""
+    import pyarrow as pa
+    import pyarrow.compute as pc
+
+    # The feather (Arrow IPC file) format is the magic number, followed by the
+    # stream format, which can be read without seeking (i.e. without
+    # decompressing the file to a temporary one first)
+    if f.read(8) != b"ARROW1\0\0":
+        raise ValueError("not a feather (version 2) file")
+    reader = pa.ipc.open_stream(pa.PythonFile(f, mode="r"))
+
+    # (the root ids are compared as int64, as in the table: comparing uint64
+    # with int64 values converts them to floating point, losing precision)
+    ids = np.asarray(ids).astype("int64")
+    order = np.argsort(ids)
+    sids = ids[order]
+    N = len(ids)
+    K = 256                     # bound on the number of neuropils
+    npils = {}                  # neuropil -> code, in order of appearance
+    cols = ["pre_pt_root_id", "post_pt_root_id", "neuropil"] + syn_vals
+    st = defaultdict(int)
+
+    def reduce(keys, cnt, sums):
+        keys, inv = np.unique(keys, return_inverse=True)
+        cnt = np.bincount(inv, weights=cnt, minlength=len(keys))
+        sums = np.column_stack([np.bincount(inv, weights=x, minlength=len(keys))
+                                for x in sums.T])
+        return keys, cnt, sums
+
+    acc = None
+    parts = []
+    nparts = 0
+    for batch in reader:
+        batch = batch.select(cols)
+        if (batch.column("pre_pt_root_id").null_count > 0 or
+            batch.column("post_pt_root_id").null_count > 0):
+            raise ValueError("missing root ids")
+        pre = batch.column("pre_pt_root_id").to_numpy()
+        post = batch.column("post_pt_root_id").to_numpy()
+        i = np.searchsorted(sids, pre).clip(max=N - 1)
+        j = np.searchsorted(sids, post).clip(max=N - 1)
+        ok_pre = sids[i] == pre
+        ok_post = sids[j] == post
+        ok = ok_pre & ok_post
+        st["synapses"] += len(pre)
+        st["proofread_pre"] += int(ok_pre.sum())
+        st["proofread_post"] += int(ok_post.sum())
+        st["proofread_both"] += int(ok.sum())
+        cleft = batch.column("cleft_score").to_numpy(zero_copy_only=False).astype("float64")
+        st["cleft_min"] = min(st.get("cleft_min", np.inf), np.nanmin(cleft))
+        st["cleft_below_50"] += int((cleft < 50).sum())
+        st["cleft_below_50_proofread"] += int((cleft[ok] < 50).sum())
+        st["autapses_proofread"] += int((pre[ok] == post[ok]).sum())
+
+        np_col = batch.column("neuropil")
+        st["neuropil_null"] += np_col.null_count
+        np_col = pc.dictionary_encode(pc.fill_null(np_col, ""))
+        codes = np.array([npils.setdefault(x, len(npils))
+                          for x in np_col.dictionary.to_pylist()], dtype="int64")
+        if len(npils) > K:
+            raise ValueError("too many neuropils")
+        c = codes[np_col.indices.to_numpy(zero_copy_only=False)]
+
+        keys = (i[ok] * N + j[ok]) * K + c[ok]
+        # (some values are missing: they are left out of the averages)
+        vals = []
+        for v in syn_vals:
+            col = batch.column(v)
+            st[f"null_{v}"] += col.null_count
+            x = col.to_numpy(zero_copy_only=False).astype("float64")[ok]
+            vals.append(x)
+        vals = np.column_stack(vals)
+        valid = ~np.isnan(vals)
+        vals = np.column_stack((np.where(valid, vals, 0), valid))
+        parts.append(reduce(keys, np.ones(len(keys)), vals))
+        nparts += len(parts[-1][0])
+        if nparts > (1 << 23):
+            if acc is not None:
+                parts.append(acc)
+            acc = reduce(np.concatenate([p[0] for p in parts]),
+                         np.concatenate([p[1] for p in parts]),
+                         np.concatenate([p[2] for p in parts]))
+            parts = []
+            nparts = 0
+    if acc is not None:
+        parts.append(acc)
+    keys, cnt, sums = reduce(np.concatenate([p[0] for p in parts]),
+                             np.concatenate([p[1] for p in parts]),
+                             np.concatenate([p[2] for p in parts]))
+    del parts, acc
+
+    # (the synapses without a neuropil are marked "None" in this table, and
+    # "UNASGD" in the table of connections, which we follow)
+    if "None" in npils:
+        if "UNASGD" in npils:
+            raise ValueError("both None and UNASGD neuropils")
+        npils["UNASGD"] = npils.pop("None")
+    names = sorted(npils)
+    rank = np.empty(len(npils), dtype="int64")
+    rank[[npils[x] for x in names]] = np.arange(len(names))
+    c = rank[keys % K]
+    keys //= K
+    i, j = np.divmod(keys, N)
+    del keys
+    # (i and j are ranks of the root ids)
+    idx = np.lexsort((c, j, i))
+    s, t, c, cnt, sums = order[i[idx]], order[j[idx]], c[idx], cnt[idx], sums[idx]
+    del i, j, idx
+    st["edges"] = len(s)
+    st["neuropils"] = len(names)
+    nv = len(syn_vals)
+    with np.errstate(invalid="ignore", divide="ignore"):
+        means = sums[:, :nv] / sums[:, nv:]
+    st["edges_missing_values"] = int((sums[:, nv:] < cnt[:, None]).any(axis=1).sum())
+    if stats is not None:
+        stats.update(st)
+    return s, t, np.array(names, dtype=object)[c], cnt.astype("int64"), means
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    name = __name__.split(".")[-1]
+    for (fconn, fids, fannot), alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        with open_upstream_file(name, fids, "rb") as f:
+            ids = np.load(io.BytesIO(f.read()))
+        with open_upstream_file(name, fannot, "rb") as f:
+            annot = pd.read_csv(f, sep="\t", low_memory=False)
+        annot = annot.set_index("root_id").loc[ids]
+
+        g = Graph()
+        g.add_vertex(len(ids))
+        g.vp.root_id = g.new_vp("int64_t", vals=ids)
+        for p in vprops:
+            col, pname = p if isinstance(p, tuple) else (p, p)
+            vals = annot[col].fillna("").astype(str)
+            if col == "side":
+                vals = vals.replace("na", "")
+            g.vp[pname] = g.new_vp("string", vals=vals.tolist())
+        g.vp.top_nt_conf = g.new_vp("double", vals=annot["top_nt_conf"].to_numpy())
+
+        if alt == "synapses_all":
+            with open_upstream_file(name, fconn, "rb") as f:
+                s, t, npil, cnt, means = aggregate_synapses(f, ids)
+            g.add_edge_list(np.column_stack((s, t)))
+            del s, t
+            g.ep.neuropil = g.new_ep("string", vals=npil.tolist())
+            del npil
+            g.ep.weight = g.new_ep("int", vals=cnt)
+            for k, v in enumerate(syn_vals):
+                g.ep[v] = g.new_ep("double", vals=means[:, k])
+            del cnt, means
+            yield alt, g
+            continue
+
+        with open_upstream_file(name, fconn, "rb", seekable=True) as f:
+            t = pyarrow.feather.read_table(f)
+        df = t.to_pandas(strings_to_categorical=True)
+        del t
+        df["neuropil"] = df["neuropil"].cat.reorder_categories(
+            sorted(df["neuropil"].cat.categories))
+        if min_synapses[alt] > 0:
+            total = df.groupby(["pre_pt_root_id", "post_pt_root_id"])["syn_count"].transform("sum")
+            df = df[total.to_numpy() >= min_synapses[alt]]
+        df = df.sort_values(["pre_pt_root_id", "post_pt_root_id", "neuropil"],
+                            kind="stable")
+
+        idx = pd.Series(np.arange(len(ids)), index=ids)
+        s = idx.loc[df["pre_pt_root_id"].to_numpy()].to_numpy()
+        t = idx.loc[df["post_pt_root_id"].to_numpy()].to_numpy()
+        g.add_edge_list(np.column_stack((s, t)))
+        # (the category strings are shared, instead of one string per edge)
+        npil = df["neuropil"].cat
+        g.ep.neuropil = g.new_ep("string", vals=np.array(list(npil.categories), dtype=object)[npil.codes.to_numpy()].tolist())
+        g.ep.weight = g.new_ep("int", vals=df["syn_count"].to_numpy())
+        for nt in nts:
+            g.ep[nt] = g.new_ep("double", vals=df[f"{nt}_avg"].to_numpy())
+        del df, s, t
+        yield alt, g