OSCR

An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish.

Code ↔ Paper

1 match between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 1 match
  1. [1] § Materials and methods › Generation of DNA constructs and transgenic lines ↔ navis/transforms/templates.py, lines 1025 to 1170 · score 0.53 · Janelia Research Campus, sequence · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,790 lines · 67 KB · GPL-3.0 · 1 match

  1. # This script is part of navis (http://www.github.com/navis-org/navis).
  2. # Copyright (C) 2018 Philipp Schlegel
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. """Functions to work with templates."""
  14. import functools
  15. import os
  16. import pathlib
  17. import warnings
  18. import numpy as np
  19. import pandas as pd
  20. import matplotlib.pyplot as plt
  21. import matplotlib.colors as mcl
  22. import networkx as nx
  23. import seaborn as sns
  24. import sparsecubes
  25. import trimesh as tm
  26. from matplotlib.lines import Line2D
  27. from collections import namedtuple
  28. from typing import List, Union, Optional
  29. from typing_extensions import Literal
  30. from .. import config, core, utils
  31. from . import factory
  32. from .base import TransformSequence, BaseTransform, AliasTransform, is_invertible
  33. from .xfm_funcs import mirror, xform
  34. # Catch some stupid warning about installing python-Levenshtein
  35. with warnings.catch_warnings():
  36. warnings.simplefilter("ignore")
  37. import fuzzywuzzy as fw
  38. import fuzzywuzzy.process
  39. __all__ = ["xform_brain", "mirror_brain", "symmetrize_brain", "render_template"]
  40. logger = config.get_logger(__name__)
  41. # Defines entry the registry needs to register a transform
  42. transform_reg = namedtuple(
  43. "Transform",
  44. ["source", "target", "transform", "type", "invertible", "weight", "weight_inv"],
  45. defaults=[None], # for weight_inv
  46. )
  47. # Check for environment variable pointing to registries
  48. _OS_TRANSPATHS = os.environ.get("NAVIS_TRANSFORMS", "")
  49. try:
  50. _OS_TRANSPATHS = [i for i in _OS_TRANSPATHS.split(";") if len(i) > 0]
  51. except BaseException:
  52. logger.error("Error parsing the `NAVIS_TRANSFORMS` environment variable")
  53. _OS_TRANSPATHS = []
  54. def _deprecate_reciprocal(reciprocal, inverse_weight):
  55. """Map the old `reciprocal` argument onto `inverse_weight`."""
  56. if reciprocal is None:
  57. return inverse_weight
  58. warnings.warn(
  59. "`reciprocal` is deprecated and will be removed in a future version - "
  60. "use `inverse_weight` instead. Note the default changed from 0.5 to 1: "
  61. "navis no longer discounts inverse transforms across the board, because "
  62. "each transform now says for itself how expensive it is to invert.",
  63. DeprecationWarning,
  64. stacklevel=3,
  65. )
  66. return reciprocal
  67. def _pick_edge(G, n1, n2, prefer_forward: bool = True):
  68. """Pick which transform to use for the hop n1 -> n2.
  69. Two templates can be connected by more than one registration - typically a
  70. purpose-built registration for this direction alongside the inverse of its
  71. counterpart, but sometimes several independent registrations.
  72. Selection is by weight, and - as everywhere else - **lower weight wins**. That
  73. is the same rule `nx.shortest_path` used to cost the route in the first place,
  74. so the transform we hand back is the one the route was planned around.
  75. On top of that, if `prefer_forward` is True (the default), a forward
  76. registration beats the inverse of its counterpart *regardless of weight*: it is
  77. the map its authors actually fitted for this direction. Weights still decide
  78. among several forward registrations (or, if there is no forward edge, among
  79. several inverse ones).
  80. Why the preference is not simply expressed as a weight: `weight` is what
  81. `shortest_path` minimises when choosing the path, and the two uses pull in
  82. opposite directions. To stop an inverse edge dragging unrelated routes through
  83. it you must weight it *up*; to stop it being picked over a forward edge you
  84. must weight it *down*. No single number does both. So weight means one thing
  85. only - what a hop costs - and forward-vs-inverse is decided here.
  86. Parameters
  87. ----------
  88. prefer_forward : bool
  89. If False, pick purely by weight and let an inverse edge win
  90. if it is cheaper. Use this if you have weighted your graph
  91. deliberately and want it taken at face value.
  92. """
  93. edges = []
  94. i = 0
  95. # Collect all edges between those two nodes
  96. # - this is annoyingly complicated with MultiDiGraphs
  97. while True:
  98. try:
  99. e = G.edges[(n1, n2, i)]
  100. except KeyError:
  101. break
  102. edges.append(e)
  103. i += 1
  104. candidates = edges
  105. if prefer_forward:
  106. # (Edges added before `inverse` was tracked are treated as forward.)
  107. forward = [e for e in edges if not e.get("inverse", False)]
  108. candidates = forward if forward else edges
  109. # Lower weight wins - same rule `shortest_path` used to pick the route.
  110. return min(candidates, key=lambda e: e["weight"])["transform"]
  111. class TemplateRegistry:
  112. """Tracks template brains, available transforms and produces bridging sequences.
  113. Parameters
  114. ----------
  115. scan_paths : bool
  116. If True will scan paths on initialization.
  117. """
  118. def __init__(self, scan_paths: bool = True):
  119. # Paths to scan for transforms
  120. self._transpaths = _OS_TRANSPATHS.copy()
  121. # Transforms
  122. self._transforms = []
  123. # Template brains
  124. self._templates = []
  125. if scan_paths:
  126. self.scan_paths()
  127. def __contains__(self, other) -> bool:
  128. """Check if transform is in registry.
  129. Parameters
  130. ----------
  131. other : transform, filepath, tuple
  132. Either a transform (e.g. CMTKtransform), a filepath (e.g.
  133. to a .list file) or a tuple of `(source, target, transform)`
  134. where `transform` can be a transform or a filepath.
  135. """
  136. if isinstance(other, (tuple, list)):
  137. return any([t == other for t in self.transforms])
  138. else:
  139. return other in [t.transform for t in self.transforms]
  140. def __len__(self) -> int:
  141. return len(self.transforms)
  142. def __repr__(self):
  143. return self.__str__()
  144. def __str__(self):
  145. return f"TemplateRegistry with {len(self)} transforms"
  146. @property
  147. def transpaths(self) -> list:
  148. """Paths searched for transforms.
  149. Use `.scan_paths` to trigger a scan. Use `.register_path` to add
  150. more path(s).
  151. """
  152. return self._transpaths
  153. @property
  154. def templates(self) -> list:
  155. """Registered template (brains)."""
  156. return self._templates
  157. @property
  158. def transforms(self) -> list:
  159. """Registered transforms (bridging + mirror)."""
  160. return self._transforms
  161. @property
  162. def bridges(self) -> list:
  163. """Registered bridging transforms."""
  164. return [t for t in self.transforms if t.type == "bridging"]
  165. @property
  166. def mirrors(self) -> list:
  167. """Registered mirror transforms."""
  168. return [t for t in self.transforms if t.type == "mirror"]
  169. def clear_caches(self):
  170. """Clear caches of all cached functions."""
  171. self.bridging_graph.cache_clear()
  172. self.shortest_bridging_seq.cache_clear()
  173. def summary(self) -> pd.DataFrame:
  174. """Generate summary of available transforms."""
  175. return pd.DataFrame(self.transforms)
  176. def register_path(self, paths: str, trigger_scan: bool = True):
  177. """Register path(s) to scan for transforms.
  178. Parameters
  179. ----------
  180. paths : str | list thereof
  181. Paths (or list thereof) to scans for transforms. This
  182. is not permanent. For permanent additions set path(s)
  183. via the `NAVIS_TRANSFORMS` environment variable.
  184. trigger_scan : bool
  185. If True, a re-scan of all paths will be triggered.
  186. """
  187. paths = utils.make_iterable(paths)
  188. for p in paths:
  189. # Try not to duplicate paths
  190. if p not in self.transpaths:
  191. self._transpaths.append(p)
  192. if trigger_scan:
  193. self.scan_paths()
  194. def register_templatebrain(self, template: "TemplateBrain", skip_existing=True):
  195. """Register a template brain.
  196. This is used, for example, by navis.mirror_brain.
  197. Parameters
  198. ----------
  199. template : TemplateBrain
  200. TemplateBrain to register.
  201. skip_existing : bool
  202. If True, will skip existing template brains.
  203. """
  204. utils.eval_param(template, name="template", allowed_types=(TemplateBrain,))
  205. if template not in self._templates or not skip_existing:
  206. self._templates.append(template)
  207. def register_transform(
  208. self,
  209. transform: BaseTransform,
  210. source: str,
  211. target: str,
  212. transform_type: str,
  213. skip_existing: bool = True,
  214. weight: int = 1,
  215. weight_inv: Optional[int] = None,
  216. ):
  217. """Register a transform.
  218. Parameters
  219. ----------
  220. transform : subclass of BaseTransform | TransformSequence
  221. A transform (AffineTransform, CMTKtransform, etc.)
  222. or a TransformSequence.
  223. source : str
  224. Source for forward transform.
  225. target : str
  226. Target for forward transform. Ignored for mirror
  227. transforms.
  228. transform_type : "bridging" | "mirror"
  229. Type of transform.
  230. skip_existing : bool
  231. If True will skip if transform is already in registry.
  232. weight : float
  233. What this transform costs to traverse forwards.
  234. **Lower weight = more likely to be used** - both when
  235. choosing a route and when picking between several
  236. registrations connecting the same two templates.
  237. weight_inv : float, optional
  238. What this transform costs to traverse *backwards*.
  239. If not given, defaults to
  240. `weight * transform.inverse_weight_factor` - i.e. the
  241. transform says for itself how much dearer it is to
  242. invert. That is 1 for anything whose inverse is stored
  243. or exact (affine, H5, thin-plate spline), and more for
  244. anything that has to solve for it numerically (CMTK,
  245. and elastix especially).
  246. Passing this explicitly overrides
  247. `inverse_weight_factor` entirely - use it when you know
  248. better than the default for a particular registration.
  249. Note that weight decides which *route* is taken; it does
  250. not, on its own, decide whether an inverse is used in
  251. place of a purpose-built registration. That is
  252. `prefer_forward` (see
  253. `TemplateRegistry.find_bridging_path`), which is on by
  254. default.
  255. See Also
  256. --------
  257. register_transformfile
  258. If you want to register a file instead of an
  259. already constructed transform.
  260. """
  261. assert transform_type in ("bridging", "mirror")
  262. assert isinstance(transform, (BaseTransform, TransformSequence))
  263. # Translate into edge
  264. edge = transform_reg(
  265. source=source,
  266. target=target,
  267. transform=transform,
  268. type=transform_type,
  269. invertible=is_invertible(transform),
  270. weight=weight,
  271. # Some transforms are dearer to traverse backwards than forwards -
  272. # elastix in particular, where the inverse is an iterative numerical
  273. # solve rather than a stored map. Those advertise an
  274. # `inverse_weight_factor` to say so, and their inverse edges cost more.
  275. # Note this only affects what a hop *costs*; a forward registration is
  276. # preferred over an inverse one regardless (see `_pick_edge`).
  277. weight_inv=(
  278. weight_inv
  279. if weight_inv is not None
  280. else weight * getattr(transform, "inverse_weight_factor", 1)
  281. ),
  282. )
  283. # Don't add if already exists
  284. if not skip_existing or edge not in self:
  285. self.transforms.append(edge)
  286. # Clear cached functions
  287. self.clear_caches()
  288. def register_transformfile(self, path: str, **kwargs):
  289. """Parse and register a transform file.
  290. File/Directory name must follow the a `{TARGET}_{SOURCE}.{ext}`
  291. convention (e.g. `JRC2013_FCWB.list`).
  292. Parameters
  293. ----------
  294. path : str
  295. Path to transform.
  296. **kwargs
  297. Keyword arguments are passed to the constructor of the
  298. Transform (e.g. CMTKtransform for `.list` directory).
  299. See Also
  300. --------
  301. register_transform
  302. If you want to register an already constructed transform
  303. instead of a transform file that still needs to be
  304. parsed.
  305. """
  306. assert isinstance(path, (str, pathlib.Path))
  307. path = pathlib.Path(path).expanduser()
  308. if not path.is_dir() and not path.is_file():
  309. raise ValueError(f'File/directory "{path}" does not exist')
  310. # Parse properties
  311. try:
  312. if "mirror" in path.name or "imgflip" in path.name:
  313. transform_type = "mirror"
  314. source = path.name.split("_")[0]
  315. target = None
  316. else:
  317. transform_type = "bridging"
  318. target = path.name.split("_")[0]
  319. source = path.name.split("_")[1].split(".")[0]
  320. # Initialize the transform
  321. transform = factory.factory_methods[path.suffix](path, **kwargs)
  322. self.register_transform(
  323. transform=transform,
  324. source=source,
  325. target=target,
  326. transform_type=transform_type,
  327. )
  328. except BaseException as e:
  329. logger.error(f"Error registering {path} as transform: {str(e)}")
  330. def scan_paths(self, extra_paths: List[str] = None):
  331. """Scan registered paths for transforms and add to registry.
  332. Will skip transforms that already exist in this registry.
  333. Parameters
  334. ----------
  335. extra_paths : list of str
  336. Any Extra paths to search.
  337. """
  338. search_paths = self.transpaths
  339. if isinstance(extra_paths, str):
  340. extra_paths = [i for i in extra_paths.split(";") if len(i) > 0]
  341. search_paths = np.append(search_paths, extra_paths)
  342. for path in search_paths:
  343. path = pathlib.Path(path).expanduser()
  344. # Skip if path does not exist
  345. if not path.is_dir():
  346. continue
  347. # Go over the file extensions we can work with (.h5, .list, .json)
  348. # These file extensions are registered in the
  349. # `navis.transforms.factory` module
  350. for ext in factory.factory_methods:
  351. for hit in path.rglob(f"*{ext}"):
  352. if hit.is_dir() or hit.is_file():
  353. # Register this file
  354. self.register_transformfile(hit)
  355. # Clear cached functions
  356. self.clear_caches()
  357. @functools.lru_cache()
  358. def bridging_graph(
  359. self,
  360. inverse_weight: Union[Literal[False], int, float] = 1,
  361. reciprocal=None,
  362. ) -> nx.DiGraph:
  363. """Generate networkx Graph describing the bridging paths.
  364. Parameters
  365. ----------
  366. inverse_weight : bool | float
  367. Whether to add inverse edges for transforms that can be
  368. inverted, and what to charge for them.
  369. `1` (default) trusts the weights already on the graph: an
  370. inverse edge costs its transform's `weight_inv`, which by
  371. default already accounts for how expensive that particular
  372. transform is to invert (see `register_transform`).
  373. Pass another number to scale every inverse edge by it - a
  374. blunt, global "avoid going backwards" (> 1) or "don't mind
  375. going backwards" (< 1) dial. Remember lower weight = more
  376. likely to be used.
  377. `False` drops inverse edges altogether.
  378. reciprocal : bool | float
  379. Deprecated alias for `inverse_weight`.
  380. Returns
  381. -------
  382. networkx.MultiDiGraph
  383. """
  384. inverse_weight = _deprecate_reciprocal(reciprocal, inverse_weight)
  385. # Drop mirror transforms
  386. bridge = [t for t in self.transforms if t.type == "bridging"]
  387. # Note we re-check invertibility here rather than trusting the snapshot
  388. # taken at registration time: for elastix transforms it depends on the
  389. # transform backend, which the user can change at run time.
  390. bridge_inv = [t for t in bridge if is_invertible(t.transform)]
  391. # Generate graph
  392. # Note we are using MultiDi graph here because we might
  393. # have multiple edges between nodes. For example, there
  394. # is a JFRC2013DS_JFRC2013 and a JFRC2013_JFRC2013DS
  395. # bridging registration. If we include the inverse, there
  396. # will be two edges connecting JFRC2013DS and JFRC2013 in
  397. # both directions
  398. G = nx.MultiDiGraph()
  399. edges = [
  400. (
  401. t.source,
  402. t.target,
  403. {
  404. "transform": t.transform,
  405. "type": type(t.transform).__name__,
  406. "weight": t.weight,
  407. "inverse": False,
  408. },
  409. )
  410. for t in bridge
  411. ]
  412. if inverse_weight is not False:
  413. # `True` means "as weighted" - i.e. the same as 1.
  414. scale = 1 if inverse_weight is True else inverse_weight
  415. edges += [
  416. (
  417. t.target,
  418. t.source,
  419. {
  420. "transform": -t.transform, # note inverse transform!
  421. "type": type(t.transform).__name__,
  422. "weight": t.weight_inv * scale,
  423. "inverse": True,
  424. },
  425. )
  426. for t in bridge_inv
  427. ]
  428. G.add_edges_from(edges)
  429. return G
  430. def find_bridging_path(
  431. self,
  432. source: str,
  433. target: str,
  434. via: Optional[str] = None,
  435. avoid: Optional[str] = None,
  436. inverse_weight=1,
  437. prefer_forward: bool = True,
  438. reciprocal=None,
  439. ) -> tuple:
  440. """Find bridging path from source to target.
  441. Parameters
  442. ----------
  443. source : str
  444. Source from which to transform to `target`.
  445. target : str
  446. Target to which to transform to.
  447. via : str | list thereof, optional
  448. Force specific intermediate template(s). Multiple
  449. templates are visited in the given order. We use the
  450. cheapest path between each pair of waypoints which means
  451. the full path may go through a template more than once.
  452. avoid : str | list thereof, optional
  453. Avoid going through specific intermediate template(s).
  454. inverse_weight : bool | float
  455. What to charge for traversing a transform backwards. See
  456. `TemplateRegistry.bridging_graph`. Lower = more likely to
  457. be used.
  458. prefer_forward : bool
  459. Where two templates are connected by both a purpose-built
  460. registration and the inverse of its counterpart, use the
  461. purpose-built one - regardless of weight. Set to False to
  462. pick on weight alone, i.e. to take your graph's weights
  463. entirely at face value.
  464. reciprocal : bool | float
  465. Deprecated alias for `inverse_weight`.
  466. Returns
  467. -------
  468. path : list
  469. Path from source to target: [source, ..., target]
  470. transforms : list
  471. Transforms as [[path_to_transform, inverse], ...]
  472. """
  473. inverse_weight = _deprecate_reciprocal(reciprocal, inverse_weight)
  474. # Generate (or get cached) bridging graph
  475. G = self.bridging_graph(inverse_weight=inverse_weight)
  476. if len(G) == 0:
  477. raise ValueError("No bridging registrations available")
  478. # Do not remove the conversion to list - fuzzy matching does act up
  479. # otherwise
  480. nodes = list(G.nodes)
  481. if source not in nodes:
  482. best_match = fw.process.extractOne(
  483. source, nodes, scorer=fw.fuzz.token_sort_ratio
  484. )
  485. raise ValueError(
  486. f'Source "{source}" has no known bridging '
  487. f'registrations. Did you mean "{best_match[0]}" '
  488. "instead?"
  489. )
  490. if target not in G.nodes:
  491. best_match = fw.process.extractOne(
  492. target, nodes, scorer=fw.fuzz.token_sort_ratio
  493. )
  494. raise ValueError(
  495. f'Target "{target}" has no known bridging '
  496. f'registrations. Did you mean "{best_match[0]}" '
  497. "instead?"
  498. )
  499. if via:
  500. via = list(utils.make_iterable(via)) # do not remove the list() here
  501. for v in via:
  502. if v not in G.nodes:
  503. best_match = fw.process.extractOne(
  504. v, nodes, scorer=fw.fuzz.token_sort_ratio
  505. )
  506. raise ValueError(
  507. f'Via "{v}" has no known bridging '
  508. f'registrations. Did you mean "{best_match[0]}" '
  509. "instead?"
  510. )
  511. else:
  512. via = []
  513. if avoid:
  514. avoid = list(utils.make_iterable(avoid))
  515. for v in [source, target] + via:
  516. if v in avoid:
  517. raise ValueError(
  518. f'"{v}" can not be both in `avoid` and source, target or `via`.'
  519. )
  520. # Note: this is a read-only view and does not alter the cached graph
  521. G = G.subgraph([n for n in G.nodes if n not in avoid])
  522. # Find the cheapest path for each leg between consecutive waypoints. Note
  523. # that this deliberately allows the path to pass through the same template
  524. # more than once: e.g. if `via` is a dead end (B in A -> B -> A -> C).
  525. waypoints = [source] + via + [target]
  526. path = [source]
  527. for n1, n2 in zip(waypoints[:-1], waypoints[1:]):
  528. try:
  529. path += nx.shortest_path(G, n1, n2, weight="weight")[1:]
  530. except nx.NetworkXNoPath:
  531. msg = f"No bridging path connecting {source} and {target}"
  532. if via:
  533. msg += f" via {', '.join(map(str, via))}"
  534. if avoid:
  535. msg += f" avoiding {', '.join(map(str, avoid))}"
  536. if via:
  537. msg += f" (no path from {n1} to {n2})"
  538. raise nx.NetworkXNoPath(f"{msg}.") from None
  539. if len(set(path)) < len(path):
  540. logger.warning(f"Bridging sequence contains loop: {' -> '.join(path)}")
  541. # `path` holds the sequence of nodes we are traversing but not which
  542. # transforms (i.e. edges) to use
  543. transforms = [
  544. _pick_edge(G, n1, n2, prefer_forward=prefer_forward)
  545. for n1, n2 in zip(path[:-1], path[1:])
  546. ]
  547. return path, transforms
  548. def find_all_bridging_paths(
  549. self,
  550. source: str,
  551. target: str,
  552. via: Optional[str] = None,
  553. avoid: Optional[str] = None,
  554. inverse_weight=1,
  555. prefer_forward: bool = True,
  556. cutoff: int = None,
  557. reciprocal=None,
  558. ) -> tuple:
  559. """Find all bridging paths from source to target.
  560. Parameters
  561. ----------
  562. source : str
  563. Source from which to transform to `target`.
  564. target : str
  565. Target to which to transform to.
  566. via : str | list thereof, optional
  567. Force specific intermediate template(s).
  568. avoid : str | list thereof, optional
  569. Avoid specific intermediate template(s).
  570. inverse_weight : bool | float
  571. What to charge for traversing a transform backwards. See
  572. `TemplateRegistry.bridging_graph`. Lower = more likely to
  573. be used.
  574. prefer_forward : bool
  575. Where two templates are connected by both a purpose-built
  576. registration and the inverse of its counterpart, use the
  577. purpose-built one - regardless of weight. See
  578. `TemplateRegistry.find_bridging_path`.
  579. cutoff : int, optional
  580. Depth to stop the search. Only paths of length
  581. <= cutoff are returned.
  582. reciprocal : bool | float
  583. Deprecated alias for `inverse_weight`.
  584. Returns
  585. -------
  586. path : list
  587. Path from source to target: [source, ..., target]
  588. transforms : list
  589. Transforms as [[path_to_transform, inverse], ...]
  590. """
  591. inverse_weight = _deprecate_reciprocal(reciprocal, inverse_weight)
  592. # Generate (or get cached) bridging graph
  593. G = self.bridging_graph(inverse_weight=inverse_weight)
  594. if len(G) == 0:
  595. raise ValueError("No bridging registrations available")
  596. # Do not remove the conversion to list - fuzzy matching does act up
  597. # otherwise
  598. nodes = list(G.nodes)
  599. if source not in nodes:
  600. best_match = fw.process.extractOne(
  601. source, nodes, scorer=fw.fuzz.token_sort_ratio
  602. )
  603. raise ValueError(
  604. f'Source "{source}" has no known bridging '
  605. f'registrations. Did you mean "{best_match[0]}" '
  606. "instead?"
  607. )
  608. if target not in G.nodes:
  609. best_match = fw.process.extractOne(
  610. target, nodes, scorer=fw.fuzz.token_sort_ratio
  611. )
  612. raise ValueError(
  613. f'Target "{target}" has no known bridging '
  614. f'registrations. Did you mean "{best_match[0]}" '
  615. "instead?"
  616. )
  617. if via and via not in G.nodes:
  618. best_match = fw.process.extractOne(
  619. via, nodes, scorer=fw.fuzz.token_sort_ratio
  620. )
  621. raise ValueError(
  622. f'Via "{via}" has no known bridging '
  623. f'registrations. Did you mean "{best_match[0]}" '
  624. "instead?"
  625. )
  626. # This will raise a error message if no path is found
  627. for path in nx.all_simple_paths(G, source, target, cutoff=cutoff):
  628. # Skip paths that don't contain `via`
  629. if isinstance(via, str) and (via not in path):
  630. continue
  631. elif isinstance(via, (list, tuple, np.ndarray)) and not all(
  632. [v in path for v in via]
  633. ):
  634. continue
  635. # Skip paths that contain `avoid`
  636. if isinstance(avoid, str) and (avoid in path):
  637. continue
  638. elif isinstance(avoid, (list, tuple, np.ndarray)) and any(
  639. [v in path for v in avoid]
  640. ):
  641. continue
  642. # `path` holds the sequence of nodes we are traversing but not which
  643. # transforms (i.e. edges) to use
  644. transforms = [
  645. _pick_edge(G, n1, n2, prefer_forward=prefer_forward)
  646. for n1, n2 in zip(path[:-1], path[1:])
  647. ]
  648. yield path, transforms
  649. @functools.lru_cache()
  650. def shortest_bridging_seq(
  651. self,
  652. source: str,
  653. target: str,
  654. via: Optional[str] = None,
  655. inverse_weight: float = 1,
  656. prefer_forward: bool = True,
  657. ) -> tuple:
  658. """Find shortest bridging sequence to get from source to target.
  659. Parameters
  660. ----------
  661. source : str
  662. Source from which to transform to `target`.
  663. target : str
  664. Target to which to transform to.
  665. via : str | list of str
  666. Waystations to traverse on the way from source to
  667. target.
  668. inverse_weight : float
  669. Scales the cost of traversing a transform backwards.
  670. The default of `1` takes the graph's weights at face
  671. value: each transform already declares how expensive it
  672. is to invert (see `register_transform`). Raise it to
  673. make navis detour further to avoid going backwards at
  674. all. Remember lower weight = more likely to be used.
  675. prefer_forward : bool
  676. Where two templates are connected by both a
  677. purpose-built registration and the inverse of its
  678. counterpart, use the purpose-built one - regardless of
  679. weight. Set to False to pick on weight alone.
  680. Returns
  681. -------
  682. sequence : (N, ) array
  683. Sequence of registrations that will be traversed.
  684. transform_seq : TransformSequence
  685. Class that collates the required transforms to get
  686. from source to target.
  687. """
  688. path, transforms = self.find_bridging_path(
  689. source,
  690. target,
  691. via=via,
  692. inverse_weight=inverse_weight,
  693. prefer_forward=prefer_forward,
  694. )
  695. return np.array(path), TransformSequence(*transforms)
  696. def find_mirror_reg(self, template: str, non_found: str = "raise") -> tuple:
  697. """Search for a mirror transformation for given template.
  698. Typically a mirror transformation specifies a non-rigid transformation
  699. to correct asymmetries in an image.
  700. Parameters
  701. ----------
  702. template : str
  703. Name of the template to find a mirror transformation for.
  704. non_found : "raise" | "ignore"
  705. What to do if no mirror transformation is found. If "ignore"
  706. and no mirror transformation found, will silently return
  707. `None`.
  708. Returns
  709. -------
  710. tuple
  711. Named tuple containing a mirror transformation. Will only
  712. ever return one - even if multiple are available.
  713. """
  714. for tr in self.mirrors:
  715. if tr.source == template:
  716. return tr
  717. if non_found == "raise":
  718. raise ValueError(f"No mirror transformation found for {template}")
  719. return None
  720. def find_closest_mirror_reg(self, template: str, non_found: str = "raise") -> str:
  721. """Search for the closest mirror transformation for given template.
  722. Typically a mirror transformation specifies a non-rigid transformation
  723. to correct asymmetries in an image.
  724. Parameters
  725. ----------
  726. template : str
  727. Name of the template to find a mirror transformation for.
  728. non_found : "raise" | "ignore"
  729. What to do if there is no path to a mirror transformation.
  730. If "ignore" and no path is found, will silently return
  731. `None`.
  732. Returns
  733. -------
  734. str
  735. Name of the closest template with a mirror transform.
  736. """
  737. # Templates with mirror registrations
  738. temps_w_mirrors = [t.source for t in self.mirrors]
  739. # Add symmetrical template brains
  740. temps_w_mirrors += [
  741. t.label for t in self.templates if getattr(t, "symmetrical", False) == True
  742. ]
  743. if not temps_w_mirrors:
  744. raise ValueError("No mirror transformations registered")
  745. # If this template has a mirror registration:
  746. if template in temps_w_mirrors:
  747. return template
  748. # Get bridging graph
  749. G = self.bridging_graph()
  750. if template not in G.nodes:
  751. raise ValueError(
  752. f'"{template}" does not appear to be a registered template'
  753. )
  754. # Get path lengths from template to all other nodes
  755. pl = nx.single_source_dijkstra_path_length(G, template)
  756. # Subset to targets that have a mirror reg
  757. pl = {k: v for k, v in pl.items() if k in temps_w_mirrors}
  758. # Find the closest mirror
  759. cl = sorted(pl.keys(), key=lambda x: pl[x])
  760. # If any, return the closests
  761. if cl:
  762. return cl[0]
  763. if non_found == "raise":
  764. raise ValueError(
  765. f'No path to a mirror transformation found for "{template}"'
  766. )
  767. return None
  768. def find_template(self, name: str, non_found: str = "raise") -> "TemplateBrain":
  769. """Search for a given template (brain).
  770. Parameters
  771. ----------
  772. name : str
  773. Name of the template to find a mirror transformation for.
  774. Searches against `name` and `label` (short name) properties
  775. of registered templates.
  776. non_found : "raise" | "ignore"
  777. What to do if no mirror transformation is found. If "ignore"
  778. and no mirror transformation found, will silently return
  779. `None`.
  780. Returns
  781. -------
  782. TemplateBrain
  783. """
  784. for tmp in self.templates:
  785. if getattr(tmp, "label", None) == name:
  786. return tmp
  787. if getattr(tmp, "name", None) == name:
  788. return tmp
  789. if non_found == "raise":
  790. raise ValueError(f'No template brain registered that matches "{name}"')
  791. return None
  792. def plot_bridging_graph(self, **kwargs):
  793. """Draw bridging graph using networkX.
  794. Parameters
  795. ----------
  796. **kwargs
  797. Keyword arguments are passed to `networkx.draw_networkx`.
  798. Returns
  799. -------
  800. None
  801. """
  802. # Get graph
  803. G = self.bridging_graph(inverse_weight=False)
  804. # Draw nodes and edges
  805. node_labels = {n: n for n in G.nodes}
  806. pos = nx.kamada_kawai_layout(G)
  807. # Draw all nodes
  808. nx.draw_networkx_nodes(
  809. G, pos=pos, node_color="lightgrey", node_shape="o", node_size=300
  810. )
  811. nx.draw_networkx_labels(
  812. G, pos=pos, labels=node_labels, font_color="k", font_size=10
  813. )
  814. # Draw edges by type of transform
  815. edge_types = set([e[2]["type"] for e in G.edges(data=True)])
  816. lines = []
  817. labels = []
  818. for t, c in zip(edge_types, sns.color_palette("muted", len(edge_types))):
  819. subset = [e for e in G.edges(data=True) if e[2]["type"] == t]
  820. nx.draw_networkx_edges(
  821. G, pos=pos, edgelist=subset, edge_color=mcl.to_hex(c), width=1.5
  822. )
  823. lines.append(Line2D([0], [0], color=c, linewidth=2, linestyle="-"))
  824. labels.append(t)
  825. plt.legend(lines, labels)
  826. def xform_brain(
  827. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  828. source: str,
  829. target: str,
  830. via: Optional[str] = None,
  831. avoid: Optional[str] = None,
  832. affine_fallback: bool = True,
  833. caching: bool = True,
  834. verbose: bool = True,
  835. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  836. """Transform 3D data between template brains.
  837. This requires the appropriate transforms to be registered with `navis`.
  838. See the docs/tutorials for details.
  839. Notes
  840. -----
  841. For Neurons only: transforms can introduce a change in the units (e.g. if
  842. the transform goes from micron to nanometer space). Some template brains have
  843. their units hard-coded in their meta data (as `_navis_units`). If that's
  844. not the case we fall-back to trying to infer any change in units by comparing
  845. distances between x/y/z coordinate before and after the transform. That
  846. approach works reasonably well with base 10 increments (e.g. nm -> um) but
  847. may be off with odd changes in units (e.g. physical -> voxel space).
  848. Regardless of whether hard-coded or inferred, any change in units is used to
  849. update the `.units` property and node/soma radii for Skeletons.
  850. Parameters
  851. ----------
  852. x : Neuron/List | numpy.ndarray | pandas.DataFrame
  853. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  854. columns. Numpy array must be shape `(N, 3)`.
  855. source : str
  856. Source template brain that the data currently is in.
  857. target : str
  858. Target template brain that the data should be
  859. transformed into.
  860. via : str | list thereof, optional
  861. Optionally set intermediate template(s). This can be
  862. helpful to force a specific transformation sequence.
  863. Multiple templates are visited in the given order using
  864. the cheapest path between each of them.
  865. avoid : str | list thereof, optional
  866. Prohibit going through specific intermediate template(s).
  867. affine_fallback : bool
  868. In some cases the non-rigid transformation of points
  869. can fail - for example if points are outside the
  870. deformation field. If that happens, they will be
  871. returned as `NaN`. If `affine_fallback=True`
  872. we will apply only the rigid affine part of the
  873. transformation to those points to get as close as
  874. possible to the correct coordinates.
  875. caching : bool
  876. If True, will (pre-)cache data for transforms whenever
  877. possible. Depending on the data and the type of
  878. transforms this can speed things up significantly at the
  879. cost of increased memory usage:
  880. - `False` = no upfront cost, lower memory footprint
  881. - `True` = higher upfront cost, most definitely faster
  882. Only applies if input is NeuronList and if transforms
  883. include H5 transform.
  884. verbose : bool
  885. If True, will print some useful info on transform.
  886. Returns
  887. -------
  888. same type as `x`
  889. Copy of input with transformed coordinates.
  890. Examples
  891. --------
  892. This example requires the
  893. [flybrains](https://github.com/navis-org/navis-flybrains)
  894. library to be installed: `pip3 install flybrains`
  895. Also, if you haven't already, you will need to have the optional Saalfeld
  896. lab (Janelia Research Campus) transforms installed (this is a one-off):
  897. >>> import flybrains # doctest: +SKIP
  898. >>> flybrains.download_jrc_transforms() # doctest: +SKIP
  899. Once `flybrains` is installed and you have downloaded the registrations,
  900. you can run this:
  901. >>> import navis
  902. >>> import flybrains
  903. >>> # navis example neurons are in raw (8nm voxel) hemibrain (JRCFIB2018Fraw) space
  904. >>> n = navis.example_neurons(1)
  905. >>> # Transform to FAFB14 space
  906. >>> xf = navis.xform_brain(n, source='JRCFIB2018Fraw', target='FAFB14') # doctest: +SKIP
  907. See Also
  908. --------
  909. [`navis.xform`][]
  910. Lower level entry point that takes data and applies a given
  911. transform or sequence thereof.
  912. [`navis.mirror_brain`][]
  913. Uses non-rigid transforms to mirror neurons from the left
  914. to the right side of given template brain and vice versa.
  915. """
  916. if not isinstance(source, str):
  917. TypeError(f'Expected source of type str, got "{type(source)}"')
  918. if not isinstance(target, str):
  919. TypeError(f'Expected target of type str, got "{type(target)}"')
  920. # Get the transformation sequence
  921. path, transforms = registry.find_bridging_path(source, target, via=via, avoid=avoid)
  922. if verbose:
  923. path_str = path[0]
  924. for p, tr in zip(path[1:], transforms):
  925. if isinstance(tr, AliasTransform):
  926. link = "="
  927. else:
  928. link = "->"
  929. path_str += f" {link} {p}"
  930. print("Transform path:", path_str)
  931. # Combine into transform sequence
  932. trans_seq = TransformSequence(*transforms)
  933. # Apply transform and returned xformed points
  934. xf = xform(x, transform=trans_seq, caching=caching, affine_fallback=affine_fallback)
  935. # We might be able to set the correct units based on the target template's
  936. # meta data (the "guessed" new units can be off if the transform is
  937. # not base 10 which happens for e.g. voxels -> physical space)
  938. if isinstance(xf, (core.NeuronList, core.BaseNeuron)):
  939. # First we need to find the last non-alias template space
  940. for tmp, tr in zip(path[::-1], transforms[::-1]):
  941. if not isinstance(tr, AliasTransform):
  942. # There is a chance that there is no meta data for this template
  943. try:
  944. last_temp = registry.find_template(tmp)
  945. except ValueError:
  946. break
  947. except BaseException:
  948. raise
  949. # If this template brain has a property for navis units
  950. if hasattr(last_temp, "_navis_units"):
  951. for n in core.NeuronList(xf):
  952. n.units = last_temp._navis_units
  953. break
  954. return xf
  955. def symmetrize_brain(
  956. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  957. template: Union[str, "TemplateBrain"],
  958. via: Optional[str] = "auto",
  959. progress: bool = True,
  960. verbose: bool = False,
  961. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  962. """Symmetrize 3D object (neuron, coordinates).
  963. The way this works is by:
  964. 1. Finding the closest mirror transform (unless provided)
  965. 2. Mirror data on the left-hand-side to the right-hand-side using the
  966. proper (warp) mirror transform to offset deformations
  967. 3. Simply flip that data back to the left-hand-side
  968. This works reasonably well but may produce odd results around the midline.
  969. For high quality symmetrization you are better off generating dedicated
  970. transform (see `navis-flybrains` for an example).
  971. Parameters
  972. ----------
  973. x : Neuron/List | Volume/trimesh | numpy.ndarray | pandas.DataFrame
  974. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  975. columns. Numpy array must be shape `(N, 3)`.
  976. template : str | TemplateBrain
  977. Source template brain space that the data is in. If string
  978. will be searched against registered template brains.
  979. via : "auto" | str
  980. By default ("auto") it will find and apply the closest
  981. mirror transform. You can also specify a template that
  982. should be used. That template must have a mirror transform!
  983. progress : bool
  984. Whether to show a progress bar when symmetrizing multiple
  985. neurons.
  986. verbose : bool
  987. If True, will print some useful info on the transform(s).
  988. Returns
  989. -------
  990. xs
  991. Same object type as input (array, neurons, etc) but
  992. hopefully symmetrical.
  993. Examples
  994. --------
  995. This example requires the
  996. [flybrains](https://github.com/navis-org/navis-flybrains)
  997. library to be installed: `pip3 install flybrains`
  998. >>> import navis
  999. >>> import flybrains
  1000. >>> # Get the FAFB14 neuropil mesh
  1001. >>> m = flybrains.FAFB14.mesh
  1002. >>> # Symmetrize the mesh
  1003. >>> s = navis.symmetrize_brain(m, template='FAFB14')
  1004. >>> # Plot side-by-side for comparison
  1005. >>> m.plot3d() # doctest: +SKIP
  1006. >>> s.plot3d(color=(1, 0, 0)) # doctest: +SKIP
  1007. """
  1008. if not isinstance(template, str):
  1009. TypeError(f'Expected template of type str, got "{type(template)}"')
  1010. if via == "auto":
  1011. # Find closest mirror transform
  1012. via = registry.find_closest_mirror_reg(template)
  1013. if isinstance(x, core.NeuronList):
  1014. if len(x) == 1:
  1015. x = x[0]
  1016. else:
  1017. xf = []
  1018. for n in config.tqdm(
  1019. x,
  1020. desc="Mirroring",
  1021. disable=config.pbar_hide or not progress,
  1022. leave=config.pbar_leave,
  1023. ):
  1024. xf.append(symmetrize_brain(n, template=template, via=via))
  1025. return core.NeuronList(xf)
  1026. if isinstance(x, core.BaseNeuron):
  1027. x = x.copy()
  1028. if isinstance(x, core.Skeleton):
  1029. x.nodes = symmetrize_brain(x.nodes, template=template, via=via)
  1030. elif isinstance(x, core.Dotprops):
  1031. x.points = symmetrize_brain(x.points, template=template, via=via)
  1032. # Set tangent vectors and alpha to None so they will be regenerated
  1033. x._vect = x._alpha = None
  1034. elif isinstance(x, core.Mesh):
  1035. x.vertices = symmetrize_brain(x.vertices, template=template, via=via)
  1036. else:
  1037. raise TypeError(f"Don't know how to transform neuron of type '{type(x)}'")
  1038. if x.has_connectors:
  1039. x.connectors = symmetrize_brain(x.connectors, template=template, via=via)
  1040. return x
  1041. elif isinstance(x, tm.Trimesh):
  1042. x = x.copy()
  1043. x.vertices = symmetrize_brain(x.vertices, template=template, via=via)
  1044. return x
  1045. elif isinstance(x, pd.DataFrame):
  1046. if any([c not in x.columns for c in ["x", "y", "z"]]):
  1047. raise ValueError("DataFrame must have x, y and z columns.")
  1048. x = x.copy()
  1049. x[["x", "y", "z"]] = symmetrize_brain(
  1050. x[["x", "y", "z"]].values.astype(float), template=template, via=via
  1051. )
  1052. return x
  1053. else:
  1054. try:
  1055. # At this point we expect numpy arrays
  1056. x = np.asarray(x)
  1057. except BaseException:
  1058. raise TypeError(f'Unable to transform data of type "{type(x)}"')
  1059. if not x.ndim == 2 or x.shape[1] != 3:
  1060. raise ValueError("Array must be of shape (N, 3).")
  1061. # Now find the meta info for this template brain
  1062. if isinstance(template, TemplateBrain):
  1063. tb = template
  1064. else:
  1065. tb = registry.find_template(template, non_found="raise")
  1066. # Get the bounding box
  1067. if not hasattr(tb, "boundingbox"):
  1068. raise ValueError(f'Template "{tb.label}" has no bounding box info.')
  1069. if not isinstance(tb.boundingbox, (list, tuple, np.ndarray)):
  1070. raise TypeError(
  1071. "Expected the template brain's bounding box to be a "
  1072. f"list, tuple or array - got '{type(tb.boundingbox)}'"
  1073. )
  1074. # Get bounding box of template brain
  1075. bbox = np.asarray(tb.boundingbox)
  1076. # Reshape if flat array
  1077. if bbox.ndim == 1:
  1078. bbox = bbox.reshape(3, 2)
  1079. # Find points on the left
  1080. center = bbox[0][0] + (bbox[0][1] - bbox[0][0]) / 2
  1081. is_left = x[:, 0] > center
  1082. # Make a copy of the original data
  1083. x = x.copy()
  1084. # If nothing to symmetrize - return
  1085. if is_left.sum() == 0:
  1086. return x
  1087. # Mirror with compensation for deformations
  1088. xm = mirror_brain(
  1089. x[is_left], template=template, via=via, mirror_axis="x", verbose=verbose
  1090. )
  1091. # And now flip them back without compensation for deformations
  1092. xmf = mirror_brain(xm, template=template, warp=False, mirror_axis="x")
  1093. # Replace values
  1094. x[is_left] = xmf
  1095. return x
  1096. def mirror_brain(
  1097. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  1098. template: Union[str, "TemplateBrain"],
  1099. mirror_axis: Union[Literal["x"], Literal["y"], Literal["z"]] = "auto",
  1100. warp: Union[Literal["auto"], bool] = "auto",
  1101. via: Optional[str] = None,
  1102. verbose: bool = False,
  1103. progress: bool = True,
  1104. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  1105. """Mirror 3D object (neuron, coordinates) about given axis.
  1106. The way this works is:
  1107. 1. Look up the length of the template space along the given axis. For this,
  1108. the template space has to be registered (see docs for details).
  1109. 2. Flip object along midpoint of axis using a affine transformation.
  1110. 3. (Optional) Apply a warp transform that corrects asymmetries.
  1111. Parameters
  1112. ----------
  1113. x : Neuron/List | Volume/trimesh | numpy.ndarray | pandas.DataFrame
  1114. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  1115. columns. Numpy array must be shape `(N, 3)`.
  1116. template : str | TemplateBrain
  1117. Source template brain space that the data is in. If string
  1118. will be searched against registered template brains.
  1119. Alternatively check out [`navis.transforms.mirror`][]
  1120. for a lower level interface.
  1121. mirror_axis : 'auto' | 'x' | 'y' | 'z', optional
  1122. Axis to mirror. If "auto" (default), will try get the correct
  1123. mirror axis from the template brain's meta data. If that is
  1124. not available, will default to "x".
  1125. warp : bool | "auto" | Transform, optional
  1126. If 'auto', will check if a non-rigid mirror transformation
  1127. exists for the given `template` and apply it after the
  1128. flipping. Alternatively, you can also pass a Transform or
  1129. TransformSequence directly.
  1130. via : str | None
  1131. If provided, (e.g. "FCWB") will first transform coordinates
  1132. into that space, then mirror and transform back.
  1133. Use this if there is no mirror registration for the original
  1134. template, or to transform to a symmetrical template in which
  1135. flipping is sufficient. Note that `mirror_axis` must match
  1136. the mirror axis of the "via" template!
  1137. verbose : bool
  1138. If True, will print some useful info on the transform(s).
  1139. progress : bool
  1140. Whether to show a progress bar when mirroring multiple
  1141. neurons.
  1142. Returns
  1143. -------
  1144. xf
  1145. Same object type as input (array, neurons, etc) but with
  1146. transformed coordinates.
  1147. Examples
  1148. --------
  1149. This example requires the
  1150. [flybrains](https://github.com/navis-org/navis-flybrains)
  1151. library to be installed: `pip3 install flybrains`
  1152. Also, if you haven't already, you will need to have the optional Saalfeld
  1153. lab (Janelia Research Campus) transforms installed (this is a one-off):
  1154. >>> import flybrains # doctest: +SKIP
  1155. >>> flybrains.download_jrc_transforms() # doctest: +SKIP
  1156. Once `flybrains` is installed and you have downloaded the registrations,
  1157. you can run this:
  1158. >>> import navis
  1159. >>> import flybrains
  1160. >>> # navis example neurons are in raw hemibrain (JRCFIB2018Fraw) space
  1161. >>> n = navis.example_neurons(1)
  1162. >>> # Mirror about x axis (this is a simple flip in this case)
  1163. >>> mirrored = navis.mirror_brain(n * 8 / 1000, tem plate='JRCFIB2018F', via='JRC2018F') # doctest: +SKIP
  1164. >>> # We also need to get back to raw coordinates
  1165. >>> mirrored = mirrored / 8 * 1000 # doctest: +SKIP
  1166. See Also
  1167. --------
  1168. [`navis.mirror`][]
  1169. Lower level function for mirroring. You can use this if
  1170. you want to mirror data without having a registered
  1171. template for it.
  1172. """
  1173. utils.eval_param(
  1174. mirror_axis,
  1175. name="mirror_axis",
  1176. allowed_values=("x", "y", "z", "auto"),
  1177. on_error="raise",
  1178. )
  1179. if not isinstance(warp, (BaseTransform, TransformSequence)):
  1180. utils.eval_param(
  1181. warp, name="warp", allowed_values=("auto", True, False), on_error="raise"
  1182. )
  1183. # If we go via another brain space
  1184. if via and via != template:
  1185. # Xform to "via" space
  1186. xf = xform_brain(x, source=template, target=via, verbose=verbose)
  1187. # Mirror
  1188. xfm = mirror_brain(
  1189. xf,
  1190. template=via,
  1191. mirror_axis=mirror_axis,
  1192. warp=warp,
  1193. progress=progress,
  1194. via=None,
  1195. )
  1196. # Xform back to original template space
  1197. xfm_inv = xform_brain(xfm, source=via, target=template, verbose=verbose)
  1198. return xfm_inv
  1199. if isinstance(x, core.NeuronList):
  1200. if len(x) == 1:
  1201. x = x[0]
  1202. else:
  1203. xf = []
  1204. for n in config.tqdm(
  1205. x,
  1206. desc="Mirroring",
  1207. disable=config.pbar_hide or not progress,
  1208. leave=config.pbar_leave,
  1209. ):
  1210. xf.append(
  1211. mirror_brain(
  1212. n, template=template, mirror_axis=mirror_axis, warp=warp
  1213. )
  1214. )
  1215. return core.NeuronList(xf)
  1216. if isinstance(x, core.BaseNeuron):
  1217. x = x.copy()
  1218. if isinstance(x, core.Skeleton):
  1219. x.nodes = mirror_brain(
  1220. x.nodes, template=template, mirror_axis=mirror_axis, warp=warp
  1221. )
  1222. elif isinstance(x, core.Dotprops):
  1223. if isinstance(x.k, type(None)) or x.k <= 0:
  1224. # If no k, we need to mirror vectors too. Note that this is less
  1225. # than ideal though! Here, we are scaling the vector by the
  1226. # dotprop's sampling resolution (i.e. ideally a representative
  1227. # distance between the points) because if the vectors are too
  1228. # small any warping transform will make them go haywire
  1229. hp = mirror_brain(
  1230. x.points + x.vect * x.sampling_resolution * 2,
  1231. template=template,
  1232. mirror_axis=mirror_axis,
  1233. warp=warp,
  1234. )
  1235. x.points = mirror_brain(
  1236. x.points, template=template, mirror_axis=mirror_axis, warp=warp
  1237. )
  1238. if isinstance(x.k, type(None)) or x.k <= 0:
  1239. # Re-generate vectors
  1240. vect = x.points - hp
  1241. vect = vect / np.linalg.norm(vect, axis=1).reshape(-1, 1)
  1242. x._vect = vect
  1243. else:
  1244. # Set tangent vectors and alpha to None so they will be
  1245. # regenerated on demand
  1246. x._vect = x._alpha = None
  1247. elif isinstance(x, core.Mesh):
  1248. x.vertices = mirror_brain(
  1249. x.vertices, template=template, mirror_axis=mirror_axis, warp=warp
  1250. )
  1251. # We also need to flip the normals
  1252. x.faces = x.faces[:, ::-1]
  1253. else:
  1254. raise TypeError(f"Don't know how to transform neuron of type '{type(x)}'")
  1255. if x.has_connectors:
  1256. x.connectors = mirror_brain(
  1257. x.connectors, template=template, mirror_axis=mirror_axis, warp=warp
  1258. )
  1259. return x
  1260. elif isinstance(x, tm.Trimesh):
  1261. x = x.copy()
  1262. x.vertices = mirror_brain(
  1263. x.vertices, template=template, mirror_axis=mirror_axis, warp=warp
  1264. )
  1265. # We also need to flip the normals
  1266. x.faces = x.faces[:, ::-1]
  1267. return x
  1268. elif isinstance(x, pd.DataFrame):
  1269. if any([c not in x.columns for c in ["x", "y", "z"]]):
  1270. raise ValueError("DataFrame must have x, y and z columns.")
  1271. x = x.copy()
  1272. x[["x", "y", "z"]] = mirror_brain(
  1273. x[["x", "y", "z"]].values,
  1274. template=template,
  1275. mirror_axis=mirror_axis,
  1276. warp=warp,
  1277. )
  1278. return x
  1279. else:
  1280. try:
  1281. # At this point we expect numpy arrays
  1282. x = np.asarray(x)
  1283. except BaseException:
  1284. raise TypeError(f'Unable to transform data of type "{type(x)}"')
  1285. if not x.ndim == 2 or x.shape[1] != 3:
  1286. raise ValueError("Array must be of shape (N, 3).")
  1287. if not isinstance(template, str):
  1288. TypeError(f'Expected template of type str, got "{type(template)}"')
  1289. if isinstance(warp, (BaseTransform, TransformSequence)):
  1290. mirror_trans = warp
  1291. elif warp:
  1292. # See if there is a mirror registration
  1293. mirror_trans = registry.find_mirror_reg(template, non_found="ignore")
  1294. # Get actual transform from tuple
  1295. if mirror_trans:
  1296. mirror_trans = mirror_trans.transform
  1297. # If warp was not "auto" and we didn't find a registration, raise
  1298. elif warp != "auto" and not mirror_trans:
  1299. raise ValueError(f'No mirror transform found for "{template}"')
  1300. else:
  1301. mirror_trans = None
  1302. # Now find the meta info about the template brain
  1303. if isinstance(template, TemplateBrain):
  1304. tb = template
  1305. else:
  1306. tb = registry.find_template(template, non_found="raise")
  1307. # Get the bounding box
  1308. if not hasattr(tb, "boundingbox"):
  1309. raise ValueError(f'Template "{tb.label}" has no bounding box info.')
  1310. if not isinstance(tb.boundingbox, (list, tuple, np.ndarray)):
  1311. raise TypeError(
  1312. "Expected the template brain's bounding box to be a "
  1313. f"list, tuple or array - got '{type(tb.boundingbox)}'"
  1314. )
  1315. # Get bounding box of template brain
  1316. bbox = np.asarray(tb.boundingbox)
  1317. # Reshape if flat array
  1318. if bbox.ndim == 1:
  1319. bbox = bbox.reshape(3, 2)
  1320. if isinstance(mirror_axis, str) and mirror_axis == "auto":
  1321. # Try to get mirror axis from template brain meta data
  1322. if hasattr(tb, "mirror_axis"):
  1323. mirror_axis = tb.mirror_axis
  1324. else:
  1325. # Default to x axis
  1326. mirror_axis = "x"
  1327. if verbose:
  1328. print(
  1329. f'No mirror axis info found for template "{tb.label}", defaulting to "x"'
  1330. )
  1331. # Index of mirror axis
  1332. ix = {"x": 0, "y": 1, "z": 2}[mirror_axis]
  1333. if bbox.shape == (3, 2):
  1334. # In nat.templatebrains this is using the sum (min+max) but have a
  1335. # suspicion that this should be the difference (max-min)
  1336. mirror_axis_size = bbox[ix, :].sum()
  1337. elif bbox.shape == (2, 3):
  1338. mirror_axis_size = bbox[:, ix].sum()
  1339. else:
  1340. raise ValueError(
  1341. f"Expected bounding box to be of shape (3, 2) or (2, 3) got {bbox.shape}"
  1342. )
  1343. return mirror(
  1344. x, mirror_axis=mirror_axis, mirror_axis_size=mirror_axis_size, warp=mirror_trans
  1345. )
  1346. class TemplateBrain:
  1347. """Generic base class for template brains.
  1348. Minimally, a template should have a `name` and `label` property. For
  1349. mirroring, it also needs a `boundingbox`.
  1350. See [flybrains](https://github.com/navis-org/navis-flybrains) for
  1351. an example of how to use template brains.
  1352. """
  1353. def __init__(self, **properties):
  1354. """Initialize class."""
  1355. for k, v in properties.items():
  1356. setattr(self, k, v)
  1357. @property
  1358. def mesh(self):
  1359. """Mesh represenation of this brain."""
  1360. if not hasattr(self, "_mesh"):
  1361. name = getattr(self, "regName", getattr(self, "name", None))
  1362. raise ValueError(f"{name} does not appear to have a mesh")
  1363. return self._mesh
  1364. def render_template(
  1365. x: "core.NeuronObject",
  1366. template: TemplateBrain,
  1367. source: Optional[str] = None,
  1368. depth: bool = False,
  1369. smooth: int = 0,
  1370. ) -> np.ndarray:
  1371. """Render neurons into template space.
  1372. Parameters
  1373. ----------
  1374. x : Skeleton | Mesh | Dotprops | NeuronList
  1375. Neuron(s) to render. Uses each neuron's nodes, points or
  1376. (solid-voxelized) mesh, respectively. Multiple neurons are
  1377. accumulated into the same grid.
  1378. template : TemplateBrain
  1379. Template to use for bounds, shape and voxel sizes.
  1380. source : str, optional
  1381. If provided, will first transform the neuron(s) from
  1382. `source` template space into the `template` space.
  1383. If not provided, will assume that the neuron(s) are already
  1384. in the `template` space.
  1385. depth : bool
  1386. Only affects Meshes: if True, weigh each mesh voxel by
  1387. its distance to the surface (via
  1388. [`sparsecubes.measure.distance_transform`][]) instead of a
  1389. flat occupancy of 1. Thick regions (e.g. the soma) then
  1390. contribute more than thin neurites. Skeletons and
  1391. Dotprops are unaffected (still binned by point count).
  1392. smooth : int
  1393. If non-zero, will apply a Gaussian filter with `smooth`
  1394. as `sigma`.
  1395. Returns
  1396. -------
  1397. numpy array
  1398. 3D numpy array with rendered neuron(s) in template space.
  1399. The shape of the array is determined by the `template`
  1400. bounding box and voxel size.
  1401. Examples
  1402. --------
  1403. This example requires the
  1404. [flybrains](https://github.com/navis-org/navis-flybrains)
  1405. library to be installed: `pip3 install flybrains`
  1406. >>> import navis
  1407. >>> import flybrains
  1408. >>> # Neurons must be in - or transformed into - the template's space
  1409. >>> n = navis.example_neurons(5)
  1410. >>> # Render into the JRC2018F template grid
  1411. >>> img = navis.render_template(n, template=flybrains.JRC2018F, # doctest: +SKIP
  1412. ... source='JRCFIB2018Fraw')
  1413. """
  1414. if not isinstance(template, TemplateBrain):
  1415. raise TypeError(
  1416. f"Expected `template` to be of type TemplateBrain, got {type(template)}"
  1417. )
  1418. for attr in ("dims", "boundingbox", "voxdims"):
  1419. if not hasattr(template, attr):
  1420. raise ValueError(f'Template "{template.name}" must have `.{attr}` defined.')
  1421. pitch = np.asarray(template.voxdims, dtype=float)
  1422. bounds = np.asarray(template.boundingbox, dtype=float).reshape((3, 2))
  1423. shape = np.asarray(template.dims)
  1424. if (bounds[:, 0] >= bounds[:, 1]).any():
  1425. raise ValueError(
  1426. "Template bounding box must have lower bounds smaller than upper "
  1427. "bounds:",
  1428. bounds,
  1429. )
  1430. if shape.ndim != 1 or len(shape) != 3:
  1431. raise ValueError(
  1432. "Expected template `dims` to be a list, tuple or array of length "
  1433. f"3, got {template.dims}"
  1434. )
  1435. shape = tuple(int(d) for d in shape)
  1436. # Voxel index of the bounding box's lower corner. Both the binned points and
  1437. # the mesh voxels are mapped with the same round-to-nearest convention used
  1438. # by `neuron2voxels`/`sparsecubes`, so the two line up in the same grid.
  1439. offset = np.round(bounds[:, 0] / pitch).astype(int)
  1440. # Guard against templates whose (fixed-size) grid would exhaust memory
  1441. utils.check_grid_size(
  1442. shape, np.float32, hint="The template's voxel grid is very large."
  1443. )
  1444. # Transform the neuron(s) into template space
  1445. if source:
  1446. x = xform_brain(x, source=source, target=template.name)
  1447. from .. import sampling
  1448. image = np.zeros(shape, dtype=np.float32)
  1449. def _accumulate(voxels, weights=1.0):
  1450. """Add voxels (in template-grid indices) to the image, dropping any
  1451. that fall outside the grid."""
  1452. voxels = np.asarray(voxels)
  1453. if not len(voxels):
  1454. return
  1455. keep = np.all((voxels >= 0) & (voxels < shape), axis=1)
  1456. voxels = voxels[keep]
  1457. if not len(voxels):
  1458. return
  1459. if not np.isscalar(weights):
  1460. weights = np.asarray(weights)[keep]
  1461. image[voxels[:, 0], voxels[:, 1], voxels[:, 2]] += weights
  1462. # Iterate over neurons and fill the image grid
  1463. for neuron in core.NeuronList(x):
  1464. if isinstance(neuron, core.Mesh):
  1465. # Meshes are voxelized properly - walking the surface and filling
  1466. # the interior via `sparsecubes` - rather than just binning their
  1467. # vertices, which would miss any face larger than a voxel.
  1468. with warnings.catch_warnings(record=True) as caught:
  1469. warnings.simplefilter("always")
  1470. voxels = sparsecubes.voxelize(neuron.trimesh, spacing=pitch)
  1471. # Neuron meshes are routinely not watertight, so a `sparsecubes`
  1472. # warning about unfilled columns is expected rather than exceptional
  1473. # - demote it to a debug log and re-raise anything else.
  1474. for w in caught:
  1475. if "watertight" in str(w.message):
  1476. logger.debug(f"Voxelizing {neuron.id}: {w.message}")
  1477. else:
  1478. warnings.warn_explicit(
  1479. w.message, w.category, w.filename, w.lineno
  1480. )
  1481. # `sparsecubes` already returns unique voxels in the same
  1482. # convention. By default each occupied voxel contributes 1; with
  1483. # `depth` it instead contributes its distance to the surface, so
  1484. # thick regions weigh more than thin neurites.
  1485. if depth:
  1486. weights = sparsecubes.measure.distance_transform(
  1487. voxels, spacing=pitch
  1488. )
  1489. _accumulate(voxels - offset, weights)
  1490. else:
  1491. _accumulate(voxels - offset)
  1492. elif isinstance(neuron, (core.Skeleton, core.Dotprops)):
  1493. if isinstance(neuron, core.Skeleton):
  1494. # Resample first so that long edges don't leave gaps between
  1495. # nodes in the grid
  1496. neuron = sampling.resample_skeleton(
  1497. neuron, resample_to=pitch.min() / 2
  1498. )
  1499. pts = neuron.nodes[["x", "y", "z"]].values
  1500. else:
  1501. pts = neuron.points
  1502. # Bin the points and accumulate per-voxel counts
  1503. voxels = np.round(pts / pitch).astype(int) - offset
  1504. voxels, counts = np.unique(voxels, axis=0, return_counts=True)
  1505. _accumulate(voxels, counts.astype(np.float32))
  1506. else:
  1507. raise TypeError(
  1508. f"Don't know how to render neuron of type '{type(neuron)}' "
  1509. "into template space."
  1510. )
  1511. # Apply Gaussian filter
  1512. if smooth:
  1513. from scipy.ndimage import gaussian_filter
  1514. image = gaussian_filter(image, sigma=smooth)
  1515. return image
  1516. # Initialize the registry
  1517. registry = TemplateRegistry()

templates.py at commit 4fe56c8, under GPL-3.0 · at the source

Overview

Authors: Tian-Lun Chen1,2, Qiu-Sui Deng1,2, Kunzhang Lin3, Xiu-Dan Zheng1, Xin Wang1,2, Yong-Wei Zhong1, Xin-Yu Ning1, Ying Li1, Fu-Qiang Xu3, Jiu-Lin Du1,2,4, Xu-Fei Du1,2
  1. Institute of Neuroscience, Key Laboratory of Brain Cognition and Brain-Inspired Intelligence Technology, Center for Excellence in Brain Science and Intelligence Technology, Chinese Academy of Sciences Shanghai China
  2. University of Chinese Academy of Sciences Beijing China
  3. Shenzhen Key Laboratory of Viral Vectors for Biomedicine, Shenzhen-Hong Kong Institute of Brain Science, Shenzhen Institutes of Advanced Technology, Chinese Academy of Sciences Shenzhen China
  4. School of Life Science and Technology, ShanghaiTech University Shanghai China
Journal: eLife, volume 13, article RP100880
Dates: published online 18 September 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7554/elife.100880 · PMID 42758538 · PMCID PMC13588618 · OpenAlex W4401973586
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: zebrafish (organism), systems (subfield)
Methods: Statistics, Machine learning, fMRI & imaging, Single-unit activity, calcium imaging
Keywords: circuit tracing, rabies virus, zebrafish, trans-monosynaptic, cerebellum, reconstruction
MeSH: Neurons*, Zebrafish*, Animals, Larva, Rabies virus (* major topic)
Journal subjects: Neuroscience
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Brain Science and Brain-like Intelligence Technology - National Science and Technology Major Project (2021ZD0204500, 2021ZD0204502); National Natural Science Foundation of China (32321003, 62320106010); Youth Innovation Promotion Association of the Chinese Academy of Sciences
Citations: cited by 1 paper (Europe PMC); 71 references in the paper
Research resources: RRID:Addgene_240272, RRID:Addgene_240273, RRID:Addgene_240274, SADdG-mCherry[EnvA] RRID:Addgene_32636, CVSdG-tdTomato[EnvA] RRID:Addgene_73462, CVSdG-mCherry-P2A-Cre[EnvA] RRID:Addgene_73472, Fiji (ImageJ) RRID:SCR_002285, ParaView RRID:SCR_002516, GraphPad Prism v9.0 RRID:SCR_002798, Advanced Normalization Tools (ANTs) RRID:SCR_004757, 3D Slicer 5.4.0 RRID:SCR_005619, Python 3.11.5 RRID:SCR_008394, Clampex 10.6 RRID:SCR_011323, Simple Neurite Tracer (SNT) RRID:SCR_016566

Abstract

The larval zebrafish is a vertebrate model for in vivo monitoring and manipulation of whole-brain neuronal activity. Tracing its neural circuits remains challenging. Here, we report an applicable methodology tailored for larval zebrafish to achieve efficient retrograde trans-monosynaptic tracing from genetically defined neurons via EnvA-pseudotyped glycoprotein-deleted rabies viruses. By combinatorially optimizing multiple factors involved, we identified the CVS strain trans-complemented with advanced expression of N2cG at 36 °C as the optimal combination. It yielded a tracing efficiency of up to 20 inputs per starter cell. Its low cytotoxicity enabled the viable labeling and calcium imaging of infected neurons 10 days post-infection, spanning larval ages commonly used for functional examination. Cre-dependent labeling was further developed to enable cell-type-specific input tracing and circuit reconstruction. We mapped cerebellar circuits and uncovered the ipsilateral preference and subtype specificity of granule cell-to-Purkinje cell connections. Our method offers an efficient way for tracing neural circuits in larval zebrafish.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 1 match between paragraphs and lines of code.

navis-org/navis

License: GPL-3.0
State: the link answers, verified on 28 October 2026
Evidence: files inventoried
Commit: 4fe56c87fb8c2b0c9f89ceb3a4beb6dbf8f30839, 24 October 2026
Languages: Python (295), JavaScript (2), Shell (2), Jupyter (1), Java (1)
Size: 470 files, 301 scripts
Software Heritage: archived
Found in: the text, “Cerebellum delineation and neuron reconstruction”
Holds: README, license file, environment (pyproject.toml, requirements.txt, setup.py, binder/postBuild, binder/requirements.txt), tests, continuous integration, documentation, 1 notebook
Not found: CITATION.cff
Tools: NumPy (182 files), pandas (129 files), Matplotlib (57 files), SciPy (48 files), NetworkX (18 files), seaborn (12 files), Plotly (5 files), NEURON (4 files), scikit-image (4 files), h5py (3 files), igraph (2 files), scikit-learn (2 files), NiBabel (1 file), Numba (1 file), tifffile (1 file)
Availability: 2 checks, the latest on 28 October 2026: the link answers
  • 28 October 2026: the link answers
  • 26 September 2026: the link answers
303 files

soaringdu/Proj-RVCT

License: none: the authors keep all their rights
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: 3781c731b3196f0065720169750699bb6b658d7e, 23 June 2025
Languages: Jupyter (2)
Size: 4 files, 2 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: 2 notebooks
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (2 files), SciPy (2 files), Matplotlib (1 file), OpenCV (1 file), pandas (1 file), scikit-learn (1 file), seaborn (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 303 scripts, each with its path and the digest of its content;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

We have deposited the related zebrafish lines at CZRC (China Zebrafish Resource Center) and uploaded plasmid maps and sequences to Addgene. The viral vectors are available through BrainCase (Shenzhen, China). The neuron reconstructions can be downloaded at https://doi.org/10.12412/BSDC.1748509594.10001. All original code has been deposited at https://github.com/soaringdu/Proj-RVCT (copy archived at soaringdu, 2025). The source data files contain all numerical data used to generate the figures and figure supplements. Any additional information required to reanalyze the data reported in this paper is available upon request.

The following dataset was generated:

DuX ChenT 2025Purkinje Cell Input Atlas in Larval ZebrafishBrain Science Data Center10.12412/BSDC.1748509594.10001

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 11 authors, 6 keywords, 5 MeSH terms, 3 funders, 70 references, 14 RRIDs.

Cite

This paper

Chen, T.-L., Deng, Q.-S., Lin, K., Zheng, X.-D., Wang, X., Zhong, Y.-W., Ning, X.-Y., Li, Y., Xu, F.-Q., Du, J.-L., & Du, X.-F. (2026). An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish. eLife, 13, RP100880. https://doi.org/10.7554/elife.100880

BibTeX

@article{chen2026applicable,
author = {Chen, Tian-Lun and Deng, Qiu-Sui and Lin, Kunzhang and Zheng, Xiu-Dan and Wang, Xin and Zhong, Yong-Wei and Ning, Xin-Yu and Li, Ying and Xu, Fu-Qiang and Du, Jiu-Lin and Du, Xu-Fei},
title = {{An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish}},
journal = {eLife},
year = {2026},
month = sep,
volume = {13},
pages = {RP100880},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/elife.100880},
url = {https://doi.org/10.7554/elife.100880},
pmid = {42758538},
pmcid = {PMC13588618}
}

RIS

TY - JOUR
AU - Chen, Tian-Lun
AU - Deng, Qiu-Sui
AU - Lin, Kunzhang
AU - Zheng, Xiu-Dan
AU - Wang, Xin
AU - Zhong, Yong-Wei
AU - Ning, Xin-Yu
AU - Li, Ying
AU - Xu, Fu-Qiang
AU - Du, Jiu-Lin
AU - Du, Xu-Fei
TI - An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish
T2 - eLife
J2 - eLife
PY - 2026
DA - 2026/09/18
VL - 13
SP - RP100880
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/elife.100880
UR - https://doi.org/10.7554/elife.100880
LA - en
ER -

CSL-JSON

{
"id": "10.7554/elife.100880",
"type": "article-journal",
"title": "An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish",
"container-title": "eLife",
"author": [
{
"family": "Chen",
"given": "Tian-Lun"
},
{
"family": "Deng",
"given": "Qiu-Sui"
},
{
"family": "Lin",
"given": "Kunzhang"
},
{
"family": "Zheng",
"given": "Xiu-Dan"
},
{
"family": "Wang",
"given": "Xin"
},
{
"family": "Zhong",
"given": "Yong-Wei"
},
{
"family": "Ning",
"given": "Xin-Yu"
},
{
"family": "Li",
"given": "Ying"
},
{
"family": "Xu",
"given": "Fu-Qiang"
},
{
"family": "Du",
"given": "Jiu-Lin"
},
{
"family": "Du",
"given": "Xu-Fei"
}
],
"container-title-short": "eLife",
"volume": "13",
"page": "RP100880",
"DOI": "10.7554/elife.100880",
"PMID": "42758538",
"PMCID": "PMC13588618",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://doi.org/10.7554/elife.100880",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
18
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.7554/elife.77153
A viral toolbox for conditional and transneuronal gene expression in zebrafish.
Journal: eLife
In common: zebrafish, 14 references
[2] doi:10.1038/s41467-025-62938-w [code]
Designed optogenetic tool for bridging single-neuronal multimodal information in intact animals.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 2 other tools, zebrafish, systems, 7 references, author Jiu-Lin Du
[3] doi:10.1016/j.cell.2022.11.022 [code]
A brainstem integrator for self-location memory and positional homeostasis in zebrafish.
Journal: Cell
In common: tifffile, scikit-image, h5py, 6 other tools, zebrafish, 8 references
[4] doi:10.1016/j.cell.2024.03.016 [code]
Neurotransmitter classification from electron microscopy images at synaptic sites in Drosophila melanogaster.
Journal: Cell
In common: NEURON, tifffile, igraph, 12 other tools, 1 reference
[5] doi:10.1016/j.neuron.2024.04.035 [code]
Compensatory enhancement of input maintains aversive dopaminergic reinforcement in hungry Drosophila.
Journal: Neuron
In common: NEURON, tifffile, igraph, 12 other tools, 1 reference
[6] doi:10.1523/eneuro.0023-24.2024 [code]
Comparing the Representation of a Simple Visual Stimulus across the Cerebellar Network.
Journal: eNeuro
In common: Numba, scikit-image, seaborn, 5 other tools, zebrafish, 6 references
[7] doi:10.3389/fnsys.2026.1822122 [code]
Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.
Journal: Frontiers in systems neuroscience
In common: NEURON, tifffile, igraph, 12 other tools, systems
[8] doi:10.1038/s41586-025-09376-2 [code]
Network synchrony creates neural filters promoting quiescence in Drosophila.
Journal: Nature
In common: NEURON, tifffile, igraph, 12 other tools, systems
[9] doi:10.1038/s41592-022-01621-0 [code]
Automated synapse-level reconstruction of neural circuits in the larval zebrafish brain.
Journal: Nature methods
In common: Numba, NetworkX, OpenCV, 8 other tools, zebrafish, 4 references
[10] doi:10.1016/j.isci.2025.114192 [code]
Optogenetic interrogation of the zebrafish lateral line reveals brain-wide neural circuits involved in pattern separation.
Journal: iScience
In common: tifffile, NetworkX, Plotly, 8 other tools, zebrafish, systems, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.7554/elife.100880.svg)](https://openscicode.org/badge/doi/10.7554/elife.100880)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.7554/elife.100880.svg
   :target: https://openscicode.org/badge/doi/10.7554/elife.100880
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.7554/elife.100880"><img src="https://openscicode.org/badge/doi/10.7554/elife.100880.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.