Skip to content

Changelog

All notable changes to connecto.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. Below 1.0 a minor bump can still change behaviour you were relying on; those changes are listed under Changed rather than left for you to find.

0.3.0 - 2026-09-29

Mostly speed: skeletons, meshes and the L2 namespace now fetch in parallel. It is a minor rather than a patch release because two defaults change what you get back - the cell-class columns and the default mesh level of detail - and because the DracoPy floor goes up.

Added

  • meshes.get(lod_fallback=True): small neurons often don't get downsample to the level you ask for. With lod_fallback=True, gets the coarsest level it does have, instead of raising. That is what a mixed batch wants - lod=2, lod_fallback=True.
  • DatasetSpec.mesh_lod, the default level of detail for a dataset's meshes. It belongs to the mesh bucket rather than to a backend, so both doors of a dataset agree.
  • MultiResMeshSource.get(clamp=True), and a has_lods flag on mesh sources so callers ask whether a level means anything rather than sniffing the URL.
  • max_workers= on skeletons.get(), and on meshes.get() for both doors. parallel= also reaches the graphene mesh reader, which previously accepted it and ignored it.
  • Progress bars on every method of the L2 namespace.

Changed

  • superclass, class and subclass are three separate canonical columns, each fed from exactly one source column and never coalesced into one another. A neuron with no cell class now has none, rather than borrowing the tier above. MANC's class and FANC's primary class hold superclass-level values (descending neuron, glia, ...), so both now feed canonical superclass and neither has a canonical class; ids("class:descending neuron") on MANC refuses rather than answering at the wrong level. annotations.search covers all three levels.
  • The default mesh level of detail is per dataset, and clamped per object. hemibrain, male-CNS, MANC, fish2 and probed neuPrint datasets keep level 1; everything else defaults to 0. FlyWire() meshes are therefore finer than before and now identical to FlyWire(backend="cave"). A defaulted level is clamped to the levels each object has; a level you name explicitly is not, and still raises if it does not exist (see lod_fallback above).
  • Requires DracoPy>=2.1.0 (was >=1.4.0). 2.1 is the first release that frees the GIL while decoding, so mesh fragments decode across threads - 4.2x faster serially and 3.5-5.6x more across 8 threads. 2.0 does not, despite the major version. If cloud-volume shares the environment it must be >=12.15.1, since every earlier release pins DracoPy<2.0.0; see the installation page.
  • Skeletons are fetched in parallel on every route. Over 16 neurons: FlyWire's bucket 2.2 s -> 0.22 s, MICrONS' skeleton service 52.6 s -> 5.3 s, hemibrain's neuPrint store 5.6 s -> 1.3 s. Results keep the order you asked for.
  • Meshes are faster: fragment fan-out is no longer capped at 8, decoding runs on the fetch workers, and seam welding is ~8x quicker with byte-identical output. One Aedes neuron 5.3 s -> 2.3 s; eight 22.5 s -> 10.8 s; two hemibrain meshes at LOD 0 2.41 s -> 0.97 s. DEFAULT_MESH_WORKERS goes from 4 to 8.
  • The L2 namespace fans out per neuron, and l2.skeleton overlaps its two fetches. Over 6 MICrONS neurons, all four methods run about 2x faster.
  • neuPrint clients are pooled on the dataset rather than deep-copied per call, so the TLS handshakes are paid once: 9 hemibrain skeletons go 4.6 s -> ~1.3 s after the first call.
  • CAVE's connection pool is raised to fit connecto's fan-out, through caveclient's set_session_defaults. It only ever raises, and keeps any retry policy or larger pool you have already set.
  • meshes.get() builds its navis.MeshNeurons with process=False.
  • male-CNS moved to the public neuPrint server, neuprint.janelia.org.
  • DEFAULT_SKELETON_WORKERS is now connecto.core.parallel.DEFAULT_NEURON_WORKERS.

Fixed

  • FlyWire().meshes.get(...) raised "Level of detail 1 out of range" for neurons whose octree has one level. neuPrint's mesh path hard-coded lod=1, which only holds for the Janelia buckets.
  • FlyWire's and BANC's neuPrint sources overwrote class with superclass - all 107,504 FlyWire cell classes and 89,409 BANC ones were lost, with no class_raw kept. male-CNS put subclass values (lt, xn, pm) into class for 14,067 bodies, and Aedes coalesced the other way round.
  • L2 dotprops and skeleton could carry garbage for chunks the L2 cache had no principal axis or position for: caveclient fills a missing attribute with uninitialised memory, not NaN. connecto now asks for the unsplit frame, where missing is honestly null.
  • Unknown keyword arguments to skeletons.get() and meshes.get() were swallowed silently - wrokers=2 ran at the default. They now raise TypeError.
  • "Connection pool is full, discarding connection" warnings from CAVE at raised max_workers, each of which cost a fresh TLS handshake on the next read.
  • A race in which concurrent skeleton workers could leave cloud-volume's root check permanently stubbed out on a shared client.
  • Cold-cache precomputed volumes are built under a lock, so parallel workers no longer each fetch the same info.

0.2.0 - 2026-08-21

Added

  • connecto.precomputed: an in-house reader for neuroglancer precomputed and graphene volumes over plain HTTPS. Sharded containers (both hash functions, delta-encoded minishard indices, compressed morton codes, per-shard index caching), compressed_segmentation, multi-resolution draco meshes, and graphene - watershed reads, label decoding, and meshes assembled from static shards plus the fragments re-meshed since a proofreader last touched the neuron, with the seams welded. Read-only by design: no writing, no image codecs, no cloud SDK credentials.
  • Per-synapse neurotransmitters for BANC, male-CNS and MANC through neuPrint. Their Synapse nodes carry the eight-class probability vector, so Cap.NT_PER_SYNAPSE is no longer denied backend-wide - synapses(transmitters=True) and transmitters() work on all three.
  • BackendSpec.nt_columns and BackendSpec.nt_table declare where transmitters live for a given (dataset, backend). A dataset that claims NT_PER_SYNAPSE without either is now rejected at import rather than returning a frame with no nt column.
  • Synapse frames carry a transmitter provenance stamp in df.attrs["connecto"]["transmitters"]. BANC's two doors serve different runs of the same model - they agree on the neuron-level call but on only ~57% of shared synapses - so a frame now says which run it holds.
  • nt_source column on neuron-level annotations, naming the column each value came from. known_nt is somebody's immunostaining, top_nt is a CNN's argmax; coalescing them silently let "this neuron is GABAergic" mean either.
  • AnnotationSource.fields: per-source column spellings, and a way for a source to declare that it has not got a field at all.
  • neuPrint annotation sources for FlyWire and BANC. fetch_neuprint now borrows the spec's neuPrint door instead of assuming the query backend is neuPrint, so annotation source and backend are a genuine cross-product: FlyWire(backend="cave", annotations="neuprint") is a sentence, not a crash.
  • DracoPy as a direct dependency (draco has no reasonable pure-Python fallback), and compressed-segmentation in the voxels extra - about 25x quicker per chunk than the numpy decoder in connecto.precomputed.codecs, which stays exact and is used otherwise.
  • The capability tables in the README and on the docs landing page are now checked against the registry, per cell and per door, so a table that says it is generated has to prove it. NT is split into NT/syn (the capability) and NT/neuron (an annotation column, marked dense / sparse / none), and the vox column was added.

Changed

  • FlyWire and BANC default to their neuPrint annotations. For BANC the mirror is the better table almost everywhere: 153,984 neurons with a type against codex's 118,446, 154,830 with a transmitter against 115,601, plus somas and status codex has not got. Both datasets' previous sources are one annotations= argument away.
  • BANC's neuPrint source declares side absent - it covers 8,153 bodies against codex's 158,250. ids("PFNd", side="left") now refuses and names the source that can answer, instead of returning 0 where the truth is 18.
  • Annotation field precedence is stated in one resolver, in one order: spec, then source, then the handle's fields=, then the call's. The handle's fields= is the escape hatch resolve_criteria names by name, so it now outranks the source.
  • Annotations.fields reports the priorities in force for the source the handle will actually read, not spec.fields.
  • male-CNS's neuron-level nt prefers consensusNt over predictedNt.
  • neuPrint meshes are read from the published bucket directly rather than through navis, and mesh fetching is threaded instead of one neuron at a time.
  • PrecomputedMeta.resolution no longer rounds to int. FANC's mip 0 is 17.2 x 17.2 x 45 nm, and rounding to 17 is a 1.2% error - a hundred voxels at the far edge of the volume, far enough to land a point lookup inside the neighbouring neuron.
  • Detecting list-valued annotation columns only scans object columns and stops at the first hit: 0.9 s -> 0.3 s on a 175k x 50 frame, which two datasets now read by default.
  • borrow(ds, kind) in connecto.sources replaces two hand-rolled copies of "reach the dataset's other backend". The CAVE side now gets the cached client, the real version rules and connecto's error translation rather than its own token lookup and a version guess.

Fixed

  • neuPrint synapses() and synapse_counts() silently undercounted, by up to 5x: the unconstrained side was passed as None, which neuprint-python turns into :Neuron-only criteria, dropping every :Segment fragment partner (hemibrain 2,018 -> 4,903; male-CNS 1,186 -> 4,491; BANC 187 -> 994; MANC 3,190 -> 8,044). They now match edges() exactly.
  • soma_x/y/z were all-null in neuPrint annotation frames on hemibrain, male-CNS, MANC and fish2. somaLocation comes back as a raw Neo4j point on the unfiltered query, not the list fetch_neurons hands back for a body-ID list.
  • FlyWire's neuPrint door claimed NT_PER_SYNAPSE without having the columns behind it, so transmitters=True returned a frame with no nt column and no error. It now denies the capability on its own BackendSpec.
  • A source's "declared empty" field rename runs after spec.derive, so a suppressed column is no longer recreated from a regex two lines later.
  • The "another source has this column" suggestion carries the caller's backend= through; annotation source and backend are independent axes, and it was resetting one.
  • get_dataset("banc", fields={"side": (...)}) was silently overruled by the annotation source that declared side absent.

Removed

  • cloud-volume is no longer a runtime dependency. Reading is a much smaller job than reading and writing to half a dozen cloud backends, and paying for the latter cost 56 packages and ~78 MB of a default install (boto3, the Google Cloud SDK, gevent, protobuf and the rest). It stays a dev dependency: tests/test_precomputed.py reads the same cutouts and meshes through both readers and asserts they agree, so cloud-volume is the oracle the new reader is checked against.

0.1.1 - 2026-07-20

Added

  • ds.voxels: sparse volumes - every voxel belonging to a neuron - as navis.VoxelNeurons, or as raw (N, 3) arrays or rle runs, in voxel or nm coordinates. ds.voxels.scales() lists the pyramid levels a dataset actually serves.
  • Cap.VOXELS, kept separate from SEGMENTATION because "there is a segmentation volume" and "you can get one neuron's voxels out of it without reading the whole brain" are different claims. DVID keeps a live per-body index and answers in one request; a chunkedgraph keeps none, so the same question degrades to reading dense blocks and masking them - 100-1000x more voxels touched than kept.
  • ds.voxels.estimate(), and a refusal rather than an apparent hang when a read would transfer billions of voxels. voxels.get() also picks a sensible default scale, since scale 0 for a hemibrain neuron is 1.17 billion voxels.
  • SparseVolSource, for datasets served by an external per-body index - with the scales it really serves and a per-axis downsample factor, because fly pyramids routinely halve X and Y while leaving Z alone.
  • DVID servers and nodes are discovered at runtime from neuPrint and clio metadata, so nothing is hard-coded and the node always matches the snapshot the rest of the query came from.
  • NoSuchBodyError, which is also a KeyError so except KeyError around a per-neuron loop keeps working.
  • The voxels extra, installing fastremap to speed up masking a dense cutout down to one root. connecto.voxels.pcg falls back to numpy without it.

0.1.0 - 2026-07-16

Initial release: one interface over CAVE and neuPrint.

Added

  • Dataset handles for FlyWire, Hemibrain, BANC, FANC, MANC, male-CNS, MICrONS, Aedes and fish2, plus generic CAVE and NeuPrint constructors and register() for pointing connecto at any datastack or neuPrint server. Handles are immutable; ds.at(version) gives you another one.
  • Namespaces with normalised output: annotations, connectivity, skeletons, meshes, rois, somas, viz, and - where the backend has them - segmentation, proofreading and l2. Edges are always pre, post, weight as int64, int64, int32, positions are always nanometres, side is always left/right/center, and skeletons and meshes are always navis neurons.
  • The capability model: Cap, DatasetSpec/BackendSpec, ds.supports() and capability_matrix(). A capability belongs to a (dataset, backend) pair, so FlyWire(backend="neuprint") no longer claims a chunkedgraph it cannot reach. Nothing degrades silently - an explicit request a dataset cannot honour raises CapabilityError, and whole namespaces are absent rather than broken, so hasattr works as feature detection.
  • NeuronCriteria and the mini-language it desugars from, so the same selection reads the same on every dataset.
  • Version handling: materialization versions and neuPrint dataset tags behind one Version, ds.versions(), ds.find_version(), and an error rather than a guess when a request is ambiguous.
  • Annotation sources as a separate axis from the backend - CAVE tables, neuPrint, GitHub TSVs, SeaTable/FlyTable and clio - with per-source freshness tokens.
  • An on-disk cache under ~/.connecto/cache whose key includes the materialization, so it can never serve you the wrong version; live sources join a freshness token to the key and keep one entry rather than one per edit.
  • Neuroglancer scene building via ds.viz.
  • Token handling (set_token, get_token, auth_status) and server status helpers (server_status, wait_until_available), which wait only when you ask - connecto never silently retries a 503.
  • ds.cite(), because connecto knows exactly which dataset produced the numbers in your figure.