vignettes/search_banc_with_lm.Rmd
search_banc_with_lm.RmdTake a 3-D light-microscopy image of a candidate SREN (sex-peptide- receptor rectal enteric neuron) and use it as a query against the BANC female adult connectome to recover the matching EM neuron(s). SRENs are a pair of cells at the posterior tip of the abdominal ganglion that express the sex peptide receptor and send efferent processes to the rectum; they are required for the rod-shaped fecal pellets (“RODs”) that mated females produce in response to seminal-fluid sex peptide. See Chae et al., Citations below, for the upstream/downstream circuit context.
The pipeline starts from the raw .lsm (a 40× confocal
stack of the abdominal tip with the cord at an angle in the field of
view) and lands a clean BANC ranking:
inst/python/lsm_to_nrrd.py.inst/python/lsm_pca_rotate.py). The NC82 tissue mask’s
principal axes are aligned to template (X, Y, Z); the GFP CoG is
anchored to a known abdominal-ganglion reference point so the rotated
volume lands at the right tip in template coords. No Elastix optimiser
run — the partial FOV / weak metric overlap made the rigid+affine
optimiser unreliable, and PCA + an explicit anchor is more robust.inst/python/lsm_neuropil_mask.py). Over half of the raw
GFP voxels (52% in our female sample) sit outside the cord neuropil —
bleed-through into cuticle, rectal muscle, and background. Restricting
to in-neuropil GFP triples the cMIP pixel-match count and promotes the
right BANC matches.JRC2018VNCU_HR (the NeuronBridge VNC reference grid, 573 ×
1119 × 219 voxels @ 0.461 × 0.461 × 0.7 µm) via the Saalfeld lab H5
displacement field shipped by navis-flybrains.neuronbridger::nrrd_to_mip(target_space = "VNC").flow == "efferent" cells in v888, 782
rendered) using colormip_search().nat.nblast::nblast(..., normalised = TRUE) both directions
and reporting the fwd+rev mean. Points are bridged from
JRC2018VNCF µm into BANC µm via the full elastix JRC2018F→BANC
chain
(bancr::banc_to_JRC2018F(..., method = "elastix", inverse = TRUE))
— a stack of manual affine + elastix affine + coarse B-spline + fine
B-spline. An earlier revision of this vignette used the simpler
bancr::jrcvnc2018f_to_banc_tpsreg (a 5710-landmark TPS
approximation) but that mapping collapses bilateral signal onto one side
of the cord at the abdominal tip; the full elastix chain preserves
it.nat.ggplot panels in BANC native µm, plus a side-by-side
cMIP of the SREN query and the top BANC hit, plus a bilateral-pair
overlay for the current best hypothesis (EN00B016).Why use BANC for this? BANC is the only female adult Drosophila CNS currently reconstructed at synapse resolution (v888 here, brain + VNC). Identifying the EM counterpart of the SRENs gives us their downstream connectivity — the descending input from SAG (Sex Peptide Abdominal Ganglion) neurons, the ascending afferents that signal back to the brain, and the rectal target. Colour-MIP search + NBLAST against BANC is the route to recovering these cells from a single LM volume.

Neither the source .lsm (~480 MB, private) nor the BANC
efferent MIP library (~12 MB across 782 PNGs) ship with
neuronbridger. The vignette walks through the download
steps; the only artefacts shipped are the rendered figures under
inst/images/:
banc_colormip_sren_query.png — SREN query MIP in
JRC2018VNCU_HR after PCA pre-rotation + neuropil mask.banc_colormip_sren_top_hits.png — top-6 hits from
colormip_search() (BANC EM mesh in purple, SREN LM cloud
coloured by depth, faded by intensity). Each tile shows a top-down YX
view + an XZ side projection. Plotted in BANC native µm; SREN cloud
bridged via the full elastix JRC2018F→BANC chain.banc_colormip_sren_nblast_top.png — same layout, ranked
by NBLAST fwd+rev mean (normalised) against the full ~1,033-cell
efferent library.banc_colormip_sren_en00b016_pair.png — the two BANC
cells whose manc_cell_type = EN00B016 (candidate SREN
bilateral pair): 720575941680133053 (left, BANC
cell_type = EFFabg09, ACh) +
720575941659397456 (right, BANC
cell_type = EN00B016, octopamine).banc_colormip_sren_vs_top_hit.png — side-by-side cMIP
of the SREN query and the current top BANC cMIP hit (label written
dynamically from live SeaTable annotations, since these have shifted
over successive proofreading rounds).A self-contained reproducer is at
inst/scripts/banc_colormip_sren.R. The Python helpers it
shells out to live under inst/python/:
lsm_to_nrrd.py, lsm_build_nc82_mask.py,
lsm_pca_rotate.py, lsm_neuropil_mask.py.
# R packages
if (!require("remotes")) install.packages("remotes")
remotes::install_github("natverse/neuronbridger")
remotes::install_github("flyconnectome/bancr") # BANC meshes + meta
remotes::install_github("natverse/nat.ggplot") # 2-D neuron panels
# External tooling (one-off)
# * Java 17+ (Saalfeldlab RenderTransformed JAR runtime)
# * gsutil (Google Cloud SDK), authenticated to BANC's GCS bucket
# * Python deps for the LSM pipeline:
# conda activate r-reticulate
# pip install tifffile SimpleITK scipy scikit-image pillow matplotlib
#
# The Saalfeldlab JRC VNC H5 transforms (~110 MB), via the navis
# `flybrains` Python package:
#
# reticulate::py_run_string(
# "import flybrains; flybrains.download_jrc_vnc_transforms()")
#
# This writes JRCVNC2018U_JRCVNC2018F.h5 to ~/flybrain-data/.The raw LSM here is a partial FOV of the abdominal tip with the cord at an angle — the tip is roughly in the middle of the image and the rest of the VNC would be off to the top-right. A whole-VNC Elastix recipe doesn’t work (the optimiser keeps falling into local minima that put the cord on the wrong hemisphere), so we use a deterministic PCA pre-rotation instead.
WORK <- tempfile("banc_colormip_work_"); dir.create(WORK, recursive = TRUE)
LSM <- "/path/to/20250306_C24-53_EN1-5-1060_female_vnc_40x-2.lsm"
TPL <- "/path/to/JRC2018_VNC_FEMALE_461.nrrd"
py <- reticulate::conda_python("r-reticulate")
pydir <- system.file("python", package = "neuronbridger")
# (a) Split the LSM into per-channel NRRDs. For this acquisition
# ch0 = GFP, ch1 = unused red, ch2 = NC82.
system2(py, c(file.path(pydir, "lsm_to_nrrd.py"), LSM, WORK,
"--prefix", "sren_female",
"--gfp-channel", 0L, "--nc82-channel", 2L))
# (b) NC82 tissue mask (Otsu + close + largest CC + small dilation).
system2(py, c(file.path(pydir, "lsm_build_nc82_mask.py"),
file.path(WORK, "sren_female_nc82.nrrd"),
file.path(WORK, "sren_female_nc82_mask.nrrd")))
# (c) PCA pre-rotate onto JRC2018VNCF axes, anchor GFP CoG at
# (131, 508, 92) um -- this is the GFP CoG of the upstream KDRC
# registered TIF, taken as a known-good rough placement of the SREN
# arborisation in template coords.
#
# PC1 (longest mask axis) -> template Y (anterior-posterior)
# PC2 (medium) -> template X (lateral)
# PC3 (shortest) -> template Z (dorso-ventral)
# Sign of PC1 is set so the GFP CoG projects positively onto PC1
# (i.e. lands at high template Y, the posterior tip). det(R)=+1.
dir.create(file.path(WORK, "rot"), recursive = TRUE)
system2(py, c(file.path(pydir, "lsm_pca_rotate.py"),
file.path(WORK, "sren_female_nc82.nrrd"),
file.path(WORK, "sren_female_gfp.nrrd"),
file.path(WORK, "sren_female_nc82_mask.nrrd"),
TPL,
file.path(WORK, "tip_mask.nrrd"), # ignored, see --target-um
file.path(WORK, "rot"),
"--align-source", "gfp_cog",
"--target-um", "\"131,508,92\""))The script writes rot/nc82_rot.nrrd,
rot/gfp_rot.nrrd, and rot/nc82_mask_rot.nrrd,
all sharing the same physical origin and voxdims (0.4 µm XY / 0.7 µm Z)
with identity Direction. The R wrapper in
inst/scripts/banc_colormip_sren.R then resamples these onto
the JRC2018VNCF grid via SimpleITK (one-shot linear resample), so the
output reads cleanly as a template-space NRRD.
A simple Otsu of the resampled NC82 gives a neuropil mask; we zero any GFP voxel that falls outside it. About half of the raw GFP voxels in this acquisition sit outside the neuropil — fluorescence bleed-through into cuticle, rectal muscle, and background — so this step is a big lever:
system2(py, c(file.path(pydir, "lsm_neuropil_mask.py"),
"nc82_PCAonly_JRC2018VNCF.nrrd", # rotated + resampled NC82
"gfp_PCAonly_JRC2018VNCF.nrrd", # rotated + resampled GFP
"gfp_PCAonly_neuropilmasked_JRC2018VNCF.nrrd",
"--mask-out", "neuropil_mask_JRC2018VNCF.nrrd"))
#> NC82 Otsu threshold: 40.04
#> neuropil mask voxels: 5,462,433
#> GFP after neuropil mask: nonzero 36,798 (49.9% of pre-mask)The masked GFP NRRD on the JRC2018VNCF grid is what feeds into the rest of the pipeline.
ser_in_HR <- jrcvnc2018f_to_jrcvnc2018u_hr_h5(
input = "gfp_PCAonly_neuropilmasked_JRC2018VNCF.nrrd",
output = file.path(WORK, "sren_female_in_JRC2018VNCU_HR.nrrd"))
# ~70 s on Apple silicon with 8 threads. Output: 573 x 1119 x 219
# voxels at 0.461 / 0.461 / 0.7 um.The H5 (JRCVNC2018U_JRCVNC2018F.h5 from
flybrains) stores its displacement field on the JRCVNC2018U
grid, with dfield direction JRCVNC2018F →
JRCVNC2018U (the file follows the brain
<DEST>_<SRC>.h5 naming convention used by
nat.jrcbrains). For F → U image resampling we want OUTPUT
(U) → SOURCE (F) lookup — the inverse direction — so
jrcvnc2018f_to_jrcvnc2018u_hr_h5() passes -i
to RenderTransformed. Asking for the
JRC2018VNCU_HR output grid directly works because
JRC2018VNCU and JRC2018VNCU_HR share the same
physical coordinate system; the JAR interpolates the dfield onto the
requested grid in one shot.
query_png <- nrrd_to_mip(
input = ser_in_HR,
savefolder = WORK,
method = "direct",
target_space = "VNC",
threshold = 0.80,
denoise = "median3d",
format = "png",
save = TRUE,
overwrite = TRUE)threshold = 0.80 keeps the top 20 % of non-zero voxels
by intensity in the bridged volume; the neuropil mask already removed
most of the dim halo so this floor is just a small extra noise cut.

LIB_DIR <- file.path(WORK, "library", "mips")
dir.create(LIB_DIR, showWarnings = FALSE, recursive = TRUE)
# Shared root for every BANC GCS path used in this vignette.
GCS_ROOT <- "gs://lee-lab_brain-and-nerve-cord-fly-connectome"
# (a) BANC metadata — pull from the LIVE SeaTable `banc_meta` so we
# get the current `cell_type` + `manc_cell_type` bridge (both change
# with proofreading). The frozen `banc_888_meta.feather` on GCS is a
# snapshot; SeaTable is authoritative for annotation state and carries
# both the current `root_id` and the frozen `root_888` (which is what
# the cMIP filenames and precomputed layer object lists key on).
# Requires `BANCTABLE_TOKEN` in `.Renviron` — see `bancr::banctable_login`.
meta_live <- bancr::banctable_query(paste(
"SELECT root_id, root_888, cell_type, manc_cell_type, cell_class,",
"cell_sub_class, side, hemilineage, nerve, neuromere,",
"neurotransmitter_predicted, peripheral_target_type,",
"cell_function, flow",
"FROM banc_meta"))
meta <- as.data.frame(meta_live) |>
dplyr::filter(flow == "efferent") |>
dplyr::select(root_888, side, hemilineage, nerve, neuromere,
cell_class, cell_sub_class, cell_type, manc_cell_type,
neurotransmitter_predicted, peripheral_target_type,
cell_function)
nrow(meta)
#> [1] 1033
# (b) Match against the actual BANC color-MIP set (some efferents
# don't have a MIP rendered yet).
mips_root <- file.path(GCS_ROOT,
"neuron_colormips/template_alignment_240721",
"JRC2018_VNC_UNISEX_461") |> paste0("/")
all_mips <- system2("gsutil", c("ls", mips_root), stdout = TRUE)
mip_ids <- sub(".*/", "", sub("_in_JRC2018_VNC_UNISEX_461\\.png$", "", all_mips))
to_get <- all_mips[mip_ids %in% meta$root_888]
length(to_get)
#> [1] 782
# (c) Parallel download (~2 MB total, takes <1 minute).
writeLines(to_get, file.path(WORK, "to_get.txt"))
system(sprintf("xargs -P 16 -I {} gsutil -q cp {} %s/ < %s",
LIB_DIR, file.path(WORK, "to_get.txt")))
res <- colormip_search(
query = query_png,
library = LIB_DIR,
threshold = 100L, # channel-sum brightness cutoff (signal floor)
z_tolerance = 8L, # depth-LUT-index tolerance
xy_shift = 3L, # +/- 3 px translation grid
mirror = FALSE, # SREN is unilateral
mc.cores = 8L,
verbose = TRUE)
res$root_888 <- sub(".*/(\\d+)_in_JRC2018.*", "\\1", res$path)
head(res[, c("root_888", "score", "n_match", "dx", "dy", "mirror")], 6)
#> root_888 score n_match dx dy mirror
#> 1 720575941545785276 0.2122492 201 3 -3 FALSE
#> 2 720575941661415804 0.1774023 168 3 -3 FALSE
#> 3 720575941549868093 0.1636748 155 3 -3 FALSE
#> 4 720575941513645891 0.1499472 142 3 -3 FALSE
#> 5 720575941686473228 0.1488912 141 3 -3 FALSE
#> 6 720575941494055038 0.1478353 140 3 -3 FALSENeuropil-masking the query pushed the top cMIP score from ~0.072 (without the mask) to 0.21 here — three times more pixel matches because the noise floor that previously dominated the MIP is gone.
We score the SREN dotprops against the full ~1,033-cell BANC efferent
library using nat.nblast::nblast(..., normalised = TRUE)
both directions and averaging the two — the standard NBLAST scorer.
Points are bridged from JRC2018VNCF µm into BANC µm via
bancr::banc_to_JRC2018F with
method = "elastix" (the full manual-affine + elastix-affine
+ coarse-B-spline + fine-B-spline chain shipped at
gs://lee-lab_brain-and-nerve-cord-fly-connectome/registrations/vnc_240721/),
which preserves the bilateral SREN signal at the tip. Earlier revisions
used bancr::jrcvnc2018f_to_banc_tpsreg (a TPS approximation
of the same registration) and reported forward-only NBLAST because the
TPS placed the bridged signal on one side of the cord — that shortcut is
no longer needed.
library(nat.nblast)
# (a) Intensity-weighted SREN dotprops in BANC microns. Use ALL non-zero
# neuropil-masked voxels (no median filter, no largest-CC selection) so
# the bilateral pattern in the LSM survives.
ser <- nat::read.nrrd("gfp_PCAonly_neuropilmasked_JRC2018VNCF.nrrd")
storage.mode(ser) <- "integer"
idx <- which(ser > 0, arr.ind = TRUE)
ser_full <- data.frame(
X = (idx[,1] - 1L) * 0.461122 + 0.461122/2,
Y = (idx[,2] - 1L) * 0.461122 + 0.461122/2,
Z = (idx[,3] - 1L) * 0.7 + 0.35,
I = as.integer(ser[ser > 0]))
set.seed(11L); N <- 50000L
keep <- sample.int(nrow(ser_full), min(N, nrow(ser_full)),
prob = ser_full$I / sum(ser_full$I))
pts_F <- as.matrix(ser_full[keep, c("X","Y","Z")])
# Full elastix JRC2018F -> BANC (points in um).
pts_BANC_um <- bancr::banc_to_JRC2018F(
pts_F, region = "vnc", banc.units = "um",
inverse = TRUE, method = "elastix")
ser_dp <- nat::dotprops(pts_BANC_um, k = 5L)
# (b) Full-library NBLAST (~1,033 BANC efferents). The L2 dotprops are
# pre-cached locally as an .rds: fetch each cell's L2 skeleton with
# `bancr::banc_read_l2dp(meta$root_888)` once, drop NULLs/duplicates,
# and `saveRDS()` the resulting `neuronlist` so this step is offline.
banc_dps <- readRDS("banc_l2dp_efferent_full.rds")
banc_dps <- banc_dps[!sapply(banc_dps, is.null)]
banc_dps <- banc_dps[!duplicated(names(banc_dps))]
fwd <- nat.nblast::nblast(ser_dp, nat::as.neuronlist(banc_dps),
smat = nat.nblast::smat.fcwb, normalised = TRUE)
rev <- nat.nblast::nblast(nat::as.neuronlist(banc_dps), ser_dp,
smat = nat.nblast::smat.fcwb, normalised = TRUE)
nb <- data.frame(root_888 = names(fwd),
nblast_mean = (as.numeric(fwd) + as.numeric(rev)) / 2)
nb <- merge(nb, meta, by = "root_888")
nb <- nb[order(-nb$nblast_mean), ]
head(nb[, c("root_888", "nblast_mean", "cell_type", "manc_cell_type",
"cell_sub_class", "side", "neurotransmitter_predicted")], 6)Under this pipeline, the top-6 NBLAST hits are dominated by BANC
cell_type = EFFabg40 and its close relatives
(EFFabg43, EFFabg45), all sitting inside
cell_sub_class = "abdomen_neurosecretory_cell" or
"abd_Dsx_OA". These are doublesex-expressing
sex-dimorphic abdominal neurosecretory cells, largely
octopaminergic — a much better biological match for
“sex-peptide-receptor rectal enteric neuron” than the motor-neuron
interpretation an earlier revision of this vignette proposed. The MANC
bridge (manc_cell_type) resolves these to legacy MANC names
like MNad17, MNad48, MNad04,
MNad69.

Neither the cMIP top nor the NBLAST top puts specifically
the canonical SPR pair at rank 1, but both consistently rank a bilateral
pair with manc_cell_type = "EN00B016" (exactly 2 cells, one
per side in BANC) in the top ~10 % of the library. These are the
biologically motivated SREN candidates:
720575941680133053, BANC
cell_type = EFFabg09, MANC EN00B016,
abdomen_motor_neuron, ACh (hemilineage 06A);720575941659397456, BANC
cell_type = EN00B016, MANC EN00B016,
ventral_nerve_cord_neurosecretory_cell /
abdomen_neurosecretory_cell, octopamine (hemilineage
00B).They meet the bilateral-pair expectation for a sex-peptide-receptor
population and land in the abd_Dsx_OA family the search
prefers. Their MANC bridge to a single EN00B016 label
suggests MANC groups them as one cell type across sides even though BANC
currently annotates them as distinct cell_type names.

Under PCA pre-rotation + neuropil mask + full-elastix JRC2018F→BANC
bridge + NBLAST fwd+rev mean, the top-6 BANC efferents are all in the
abd_Dsx_OA / abdomen-neurosecretory family
(cell_class = ventral_nerve_cord_neurosecretory_cell),
spread across a handful of BANC EFFabg* cell_types and
their MANC bridges:
| rank | BANC cell_type
|
MANC manc_cell_type
|
side | NT | cell_sub_class |
|---|---|---|---|---|---|
| 1 | EFFabg40 | MNad17 | right | octopamine | abdomen_neurosecretory |
| 2 | EFFabg40 | MNad17 | right | octopamine | abdomen_neurosecretory |
| 3 | EFFabg43 | MNad48 | right | octopamine | abd_Dsx_OA |
| 4 | EFFabg39 | MNad04 | left | octopamine | abd_Dsx_OA |
| 5 | EFFabg40 | MNad17 | right | dopamine | abdomen_neurosecretory |
| 6 | EFFabg40 | MNad48 | left | — | abdomen_neurosecretory |
EFFabg40 (7 cells total in v888) accounts for most of
the top hits; its MANC bridge is MNad17. The precise
identity is most likely a member of the abd_Dsx_OA
/ abdomen-neurosecretory family; the best specific candidate
for the SREN bilateral pair is manc_cell_type = EN00B016 (2
cells, one per side). Both L and R members appear in the top ~10 % of
the ranking.
Four Neuroglancer precomputed layers ship the SREN LM signal in BANC voxel space, corresponding to successive refinements of the JRC2018F→BANC bridge:
| GCS path | registration | voxel size |
|---|---|---|
light_level/kdrc/SREN_female_aligned240721_to_BANC.ng |
bancr TPS (npmasked GFP) | 800 nm |
light_level/kdrc/SREN_female_aligned240721_to_BANC_elastix.ng |
bancr TPS + per-specimen elastix | 800 nm |
light_level/kdrc/SREN_female_aligned240721_to_BANC_elastix_400nm.ng |
bancr TPS + per-specimen elastix | 400 nm |
light_level/kdrc/SREN_female_aligned240721_to_BANC_fullElastix_400nm.ng |
full elastix chain (recommended) | 400 nm |
Prefer ..._fullElastix_400nm.ng — that’s the
registration-correct version, bilateral signal preserved, matching the
NBLAST bridge used in Step 7.
LM_GS <- paste0("precomputed://", GCS_ROOT,
"/light_level/kdrc/",
"SREN_female_aligned240721_to_BANC_fullElastix_400nm.ng")
BASE <- paste0("https://spelunker.cave-explorer.org/#!middleauth+",
"https://global.daf-apis.com/nglstate/api/v1/",
"6450802162925568")
top <- head(nb, 6)
top$ngl_url <- vapply(top$root_888, function(rid)
bancr::banc_lm_scene(
lm_url = LM_GS,
layer_name = "KDRC SREN (female, full elastix, 400 nm)",
range = c(1, 60),
opacity = 0.55,
blend = "additive",
ids = as.character(rid),
url = BASE,
shorten = TRUE),
character(1))lsm_pca_rotate.py (default
--align-source nc82_mask against a Y > 350 µm posterior
tip mask) and accept a few µm of placement drift.--pc2-sign +1 works for this acquisition; if your data
lands on the wrong hemisphere, re-run with
--pc2-sign -1.lm_to_jrc2018u_elastix for the brain).flow == "efferent" BANC v888 cells (782 / 1035) had a
colour-MIP rendered at the time of writing. Re-run Step 5 as more get
rendered.mirror = FALSE since SREN is unilateral. The contralateral
twin of the top hit sits at NBLAST rank 88 — visible only with a
mirror-aware secondary scoring pass.flybrains Python package (Saalfeldlab
JRC VNC H5 distribution): Schlegel et al.,
navis-org/navis-flybrains.