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–1170 · score 0.53 · Janelia Research Campus, sequence

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,823 lines · 68 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).
  449. avoid : str | list thereof, optional
  450. Avoid going through specific intermediate template(s).
  451. inverse_weight : bool | float
  452. What to charge for traversing a transform backwards. See
  453. `TemplateRegistry.bridging_graph`. Lower = more likely to
  454. be used.
  455. prefer_forward : bool
  456. Where two templates are connected by both a purpose-built
  457. registration and the inverse of its counterpart, use the
  458. purpose-built one - regardless of weight. Set to False to
  459. pick on weight alone, i.e. to take your graph's weights
  460. entirely at face value.
  461. reciprocal : bool | float
  462. Deprecated alias for `inverse_weight`.
  463. Returns
  464. -------
  465. path : list
  466. Path from source to target: [source, ..., target]
  467. transforms : list
  468. Transforms as [[path_to_transform, inverse], ...]
  469. """
  470. inverse_weight = _deprecate_reciprocal(reciprocal, inverse_weight)
  471. # Generate (or get cached) bridging graph
  472. G = self.bridging_graph(inverse_weight=inverse_weight)
  473. if len(G) == 0:
  474. raise ValueError("No bridging registrations available")
  475. # Do not remove the conversion to list - fuzzy matching does act up
  476. # otherwise
  477. nodes = list(G.nodes)
  478. if source not in nodes:
  479. best_match = fw.process.extractOne(
  480. source, nodes, scorer=fw.fuzz.token_sort_ratio
  481. )
  482. raise ValueError(
  483. f'Source "{source}" has no known bridging '
  484. f'registrations. Did you mean "{best_match[0]}" '
  485. "instead?"
  486. )
  487. if target not in G.nodes:
  488. best_match = fw.process.extractOne(
  489. target, nodes, scorer=fw.fuzz.token_sort_ratio
  490. )
  491. raise ValueError(
  492. f'Target "{target}" has no known bridging '
  493. f'registrations. Did you mean "{best_match[0]}" '
  494. "instead?"
  495. )
  496. if via:
  497. via = list(utils.make_iterable(via)) # do not remove the list() here
  498. for v in via:
  499. if v not in G.nodes:
  500. best_match = fw.process.extractOne(
  501. v, nodes, scorer=fw.fuzz.token_sort_ratio
  502. )
  503. raise ValueError(
  504. f'Via "{v}" has no known bridging '
  505. f'registrations. Did you mean "{best_match[0]}" '
  506. "instead?"
  507. )
  508. if avoid:
  509. avoid = list(utils.make_iterable(avoid))
  510. # This will raise a error message if no path is found
  511. if not via and not avoid:
  512. try:
  513. path = nx.shortest_path(G, source, target, weight="weight")
  514. except nx.NetworkXNoPath:
  515. raise nx.NetworkXNoPath(
  516. f"No bridging path connecting {source} and {target} found."
  517. )
  518. else:
  519. # Go through all possible paths and find one that...
  520. found_any = False # track if we found any path
  521. found_good = False # track if we found a path matching the criteria
  522. for path in nx.all_simple_paths(G, source, target):
  523. found_any = True
  524. # ... has all `via`s...
  525. if via and all([v in path for v in via]):
  526. # ... and none of the `avoid`
  527. if avoid:
  528. if not any([v in path for v in avoid]):
  529. found_good = True
  530. break
  531. else:
  532. found_good = True
  533. break
  534. # If we only have `avoid` but no `via`
  535. elif avoid and not any([v in path for v in avoid]):
  536. found_good = True
  537. break
  538. if not found_any:
  539. raise nx.NetworkXNoPath(
  540. f"No bridging path connecting {source} and {target} found."
  541. )
  542. elif not found_good:
  543. if via and avoid:
  544. raise nx.NetworkXNoPath(
  545. f"No bridging path connecting {source}"
  546. f'and {target} via "{via}" and '
  547. f'avoiding "{avoid}" found'
  548. )
  549. elif via:
  550. raise nx.NetworkXNoPath(
  551. f"No bridging path connecting {source}"
  552. f'and {target} via "{via}" found.'
  553. )
  554. else:
  555. raise nx.NetworkXNoPath(
  556. f"No bridging path connecting {source}"
  557. f'and {target} avoiding "{avoid}" found.'
  558. )
  559. # `path` holds the sequence of nodes we are traversing but not which
  560. # transforms (i.e. edges) to use
  561. transforms = [
  562. _pick_edge(G, n1, n2, prefer_forward=prefer_forward)
  563. for n1, n2 in zip(path[:-1], path[1:])
  564. ]
  565. return path, transforms
  566. def find_all_bridging_paths(
  567. self,
  568. source: str,
  569. target: str,
  570. via: Optional[str] = None,
  571. avoid: Optional[str] = None,
  572. inverse_weight=1,
  573. prefer_forward: bool = True,
  574. cutoff: int = None,
  575. reciprocal=None,
  576. ) -> tuple:
  577. """Find all bridging paths from source to target.
  578. Parameters
  579. ----------
  580. source : str
  581. Source from which to transform to `target`.
  582. target : str
  583. Target to which to transform to.
  584. via : str | list thereof, optional
  585. Force specific intermediate template(s).
  586. avoid : str | list thereof, optional
  587. Avoid specific intermediate template(s).
  588. inverse_weight : bool | float
  589. What to charge for traversing a transform backwards. See
  590. `TemplateRegistry.bridging_graph`. Lower = more likely to
  591. be used.
  592. prefer_forward : bool
  593. Where two templates are connected by both a purpose-built
  594. registration and the inverse of its counterpart, use the
  595. purpose-built one - regardless of weight. See
  596. `TemplateRegistry.find_bridging_path`.
  597. cutoff : int, optional
  598. Depth to stop the search. Only paths of length
  599. <= cutoff are returned.
  600. reciprocal : bool | float
  601. Deprecated alias for `inverse_weight`.
  602. Returns
  603. -------
  604. path : list
  605. Path from source to target: [source, ..., target]
  606. transforms : list
  607. Transforms as [[path_to_transform, inverse], ...]
  608. """
  609. inverse_weight = _deprecate_reciprocal(reciprocal, inverse_weight)
  610. # Generate (or get cached) bridging graph
  611. G = self.bridging_graph(inverse_weight=inverse_weight)
  612. if len(G) == 0:
  613. raise ValueError("No bridging registrations available")
  614. # Do not remove the conversion to list - fuzzy matching does act up
  615. # otherwise
  616. nodes = list(G.nodes)
  617. if source not in nodes:
  618. best_match = fw.process.extractOne(
  619. source, nodes, scorer=fw.fuzz.token_sort_ratio
  620. )
  621. raise ValueError(
  622. f'Source "{source}" has no known bridging '
  623. f'registrations. Did you mean "{best_match[0]}" '
  624. "instead?"
  625. )
  626. if target not in G.nodes:
  627. best_match = fw.process.extractOne(
  628. target, nodes, scorer=fw.fuzz.token_sort_ratio
  629. )
  630. raise ValueError(
  631. f'Target "{target}" has no known bridging '
  632. f'registrations. Did you mean "{best_match[0]}" '
  633. "instead?"
  634. )
  635. if via and via not in G.nodes:
  636. best_match = fw.process.extractOne(
  637. via, nodes, scorer=fw.fuzz.token_sort_ratio
  638. )
  639. raise ValueError(
  640. f'Via "{via}" has no known bridging '
  641. f'registrations. Did you mean "{best_match[0]}" '
  642. "instead?"
  643. )
  644. # This will raise a error message if no path is found
  645. for path in nx.all_simple_paths(G, source, target, cutoff=cutoff):
  646. # Skip paths that don't contain `via`
  647. if isinstance(via, str) and (via not in path):
  648. continue
  649. elif isinstance(via, (list, tuple, np.ndarray)) and not all(
  650. [v in path for v in via]
  651. ):
  652. continue
  653. # Skip paths that contain `avoid`
  654. if isinstance(avoid, str) and (avoid in path):
  655. continue
  656. elif isinstance(avoid, (list, tuple, np.ndarray)) and any(
  657. [v in path for v in avoid]
  658. ):
  659. continue
  660. # `path` holds the sequence of nodes we are traversing but not which
  661. # transforms (i.e. edges) to use
  662. transforms = [
  663. _pick_edge(G, n1, n2, prefer_forward=prefer_forward)
  664. for n1, n2 in zip(path[:-1], path[1:])
  665. ]
  666. yield path, transforms
  667. @functools.lru_cache()
  668. def shortest_bridging_seq(
  669. self,
  670. source: str,
  671. target: str,
  672. via: Optional[str] = None,
  673. inverse_weight: float = 1,
  674. prefer_forward: bool = True,
  675. ) -> tuple:
  676. """Find shortest bridging sequence to get from source to target.
  677. Parameters
  678. ----------
  679. source : str
  680. Source from which to transform to `target`.
  681. target : str
  682. Target to which to transform to.
  683. via : str | list of str
  684. Waystations to traverse on the way from source to
  685. target.
  686. inverse_weight : float
  687. Scales the cost of traversing a transform backwards.
  688. The default of `1` takes the graph's weights at face
  689. value: each transform already declares how expensive it
  690. is to invert (see `register_transform`). Raise it to
  691. make navis detour further to avoid going backwards at
  692. all. Remember lower weight = more likely to be used.
  693. prefer_forward : bool
  694. Where two templates are connected by both a
  695. purpose-built registration and the inverse of its
  696. counterpart, use the purpose-built one - regardless of
  697. weight. Set to False to pick on weight alone.
  698. Returns
  699. -------
  700. sequence : (N, ) array
  701. Sequence of registrations that will be traversed.
  702. transform_seq : TransformSequence
  703. Class that collates the required transforms to get
  704. from source to target.
  705. """
  706. # Generate sequence of nodes we need to find a path for
  707. # Minimally it's just from source to target
  708. nodes = np.array([source, target])
  709. if via:
  710. nodes = np.insert(nodes, 1, via)
  711. seq = [nodes[0]]
  712. transforms = []
  713. for n1, n2 in zip(nodes[:-1], nodes[1:]):
  714. path, tr = self.find_bridging_path(
  715. n1,
  716. n2,
  717. inverse_weight=inverse_weight,
  718. prefer_forward=prefer_forward,
  719. )
  720. seq = np.append(seq, path[1:])
  721. transforms = np.append(transforms, tr)
  722. if any(np.unique(seq, return_counts=True)[1] > 1):
  723. logger.warning(f"Bridging sequence contains loop: {'->'.join(seq)}")
  724. # Generate the transform sequence
  725. transform_seq = TransformSequence(*transforms)
  726. return seq, transform_seq
  727. def find_mirror_reg(self, template: str, non_found: str = "raise") -> tuple:
  728. """Search for a mirror transformation for given template.
  729. Typically a mirror transformation specifies a non-rigid transformation
  730. to correct asymmetries in an image.
  731. Parameters
  732. ----------
  733. template : str
  734. Name of the template to find a mirror transformation for.
  735. non_found : "raise" | "ignore"
  736. What to do if no mirror transformation is found. If "ignore"
  737. and no mirror transformation found, will silently return
  738. `None`.
  739. Returns
  740. -------
  741. tuple
  742. Named tuple containing a mirror transformation. Will only
  743. ever return one - even if multiple are available.
  744. """
  745. for tr in self.mirrors:
  746. if tr.source == template:
  747. return tr
  748. if non_found == "raise":
  749. raise ValueError(f"No mirror transformation found for {template}")
  750. return None
  751. def find_closest_mirror_reg(self, template: str, non_found: str = "raise") -> str:
  752. """Search for the closest mirror transformation for given template.
  753. Typically a mirror transformation specifies a non-rigid transformation
  754. to correct asymmetries in an image.
  755. Parameters
  756. ----------
  757. template : str
  758. Name of the template to find a mirror transformation for.
  759. non_found : "raise" | "ignore"
  760. What to do if there is no path to a mirror transformation.
  761. If "ignore" and no path is found, will silently return
  762. `None`.
  763. Returns
  764. -------
  765. str
  766. Name of the closest template with a mirror transform.
  767. """
  768. # Templates with mirror registrations
  769. temps_w_mirrors = [t.source for t in self.mirrors]
  770. # Add symmetrical template brains
  771. temps_w_mirrors += [
  772. t.label for t in self.templates if getattr(t, "symmetrical", False) == True
  773. ]
  774. if not temps_w_mirrors:
  775. raise ValueError("No mirror transformations registered")
  776. # If this template has a mirror registration:
  777. if template in temps_w_mirrors:
  778. return template
  779. # Get bridging graph
  780. G = self.bridging_graph()
  781. if template not in G.nodes:
  782. raise ValueError(
  783. f'"{template}" does not appear to be a registered template'
  784. )
  785. # Get path lengths from template to all other nodes
  786. pl = nx.single_source_dijkstra_path_length(G, template)
  787. # Subset to targets that have a mirror reg
  788. pl = {k: v for k, v in pl.items() if k in temps_w_mirrors}
  789. # Find the closest mirror
  790. cl = sorted(pl.keys(), key=lambda x: pl[x])
  791. # If any, return the closests
  792. if cl:
  793. return cl[0]
  794. if non_found == "raise":
  795. raise ValueError(
  796. f'No path to a mirror transformation found for "{template}"'
  797. )
  798. return None
  799. def find_template(self, name: str, non_found: str = "raise") -> "TemplateBrain":
  800. """Search for a given template (brain).
  801. Parameters
  802. ----------
  803. name : str
  804. Name of the template to find a mirror transformation for.
  805. Searches against `name` and `label` (short name) properties
  806. of registered templates.
  807. non_found : "raise" | "ignore"
  808. What to do if no mirror transformation is found. If "ignore"
  809. and no mirror transformation found, will silently return
  810. `None`.
  811. Returns
  812. -------
  813. TemplateBrain
  814. """
  815. for tmp in self.templates:
  816. if getattr(tmp, "label", None) == name:
  817. return tmp
  818. if getattr(tmp, "name", None) == name:
  819. return tmp
  820. if non_found == "raise":
  821. raise ValueError(f'No template brain registered that matches "{name}"')
  822. return None
  823. def plot_bridging_graph(self, **kwargs):
  824. """Draw bridging graph using networkX.
  825. Parameters
  826. ----------
  827. **kwargs
  828. Keyword arguments are passed to `networkx.draw_networkx`.
  829. Returns
  830. -------
  831. None
  832. """
  833. # Get graph
  834. G = self.bridging_graph(inverse_weight=False)
  835. # Draw nodes and edges
  836. node_labels = {n: n for n in G.nodes}
  837. pos = nx.kamada_kawai_layout(G)
  838. # Draw all nodes
  839. nx.draw_networkx_nodes(
  840. G, pos=pos, node_color="lightgrey", node_shape="o", node_size=300
  841. )
  842. nx.draw_networkx_labels(
  843. G, pos=pos, labels=node_labels, font_color="k", font_size=10
  844. )
  845. # Draw edges by type of transform
  846. edge_types = set([e[2]["type"] for e in G.edges(data=True)])
  847. lines = []
  848. labels = []
  849. for t, c in zip(edge_types, sns.color_palette("muted", len(edge_types))):
  850. subset = [e for e in G.edges(data=True) if e[2]["type"] == t]
  851. nx.draw_networkx_edges(
  852. G, pos=pos, edgelist=subset, edge_color=mcl.to_hex(c), width=1.5
  853. )
  854. lines.append(Line2D([0], [0], color=c, linewidth=2, linestyle="-"))
  855. labels.append(t)
  856. plt.legend(lines, labels)
  857. def xform_brain(
  858. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  859. source: str,
  860. target: str,
  861. via: Optional[str] = None,
  862. avoid: Optional[str] = None,
  863. affine_fallback: bool = True,
  864. caching: bool = True,
  865. verbose: bool = True,
  866. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  867. """Transform 3D data between template brains.
  868. This requires the appropriate transforms to be registered with `navis`.
  869. See the docs/tutorials for details.
  870. Notes
  871. -----
  872. For Neurons only: transforms can introduce a change in the units (e.g. if
  873. the transform goes from micron to nanometer space). Some template brains have
  874. their units hard-coded in their meta data (as `_navis_units`). If that's
  875. not the case we fall-back to trying to infer any change in units by comparing
  876. distances between x/y/z coordinate before and after the transform. That
  877. approach works reasonably well with base 10 increments (e.g. nm -> um) but
  878. may be off with odd changes in units (e.g. physical -> voxel space).
  879. Regardless of whether hard-coded or inferred, any change in units is used to
  880. update the `.units` property and node/soma radii for Skeletons.
  881. Parameters
  882. ----------
  883. x : Neuron/List | numpy.ndarray | pandas.DataFrame
  884. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  885. columns. Numpy array must be shape `(N, 3)`.
  886. source : str
  887. Source template brain that the data currently is in.
  888. target : str
  889. Target template brain that the data should be
  890. transformed into.
  891. via : str | list thereof, optional
  892. Optionally set intermediate template(s). This can be
  893. helpful to force a specific transformation sequence.
  894. avoid : str | list thereof, optional
  895. Prohibit going through specific intermediate template(s).
  896. affine_fallback : bool
  897. In some cases the non-rigid transformation of points
  898. can fail - for example if points are outside the
  899. deformation field. If that happens, they will be
  900. returned as `NaN`. If `affine_fallback=True`
  901. we will apply only the rigid affine part of the
  902. transformation to those points to get as close as
  903. possible to the correct coordinates.
  904. caching : bool
  905. If True, will (pre-)cache data for transforms whenever
  906. possible. Depending on the data and the type of
  907. transforms this can speed things up significantly at the
  908. cost of increased memory usage:
  909. - `False` = no upfront cost, lower memory footprint
  910. - `True` = higher upfront cost, most definitely faster
  911. Only applies if input is NeuronList and if transforms
  912. include H5 transform.
  913. verbose : bool
  914. If True, will print some useful info on transform.
  915. Returns
  916. -------
  917. same type as `x`
  918. Copy of input with transformed coordinates.
  919. Examples
  920. --------
  921. This example requires the
  922. [flybrains](https://github.com/navis-org/navis-flybrains)
  923. library to be installed: `pip3 install flybrains`
  924. Also, if you haven't already, you will need to have the optional Saalfeld
  925. lab (Janelia Research Campus) transforms installed (this is a one-off):
  926. >>> import flybrains # doctest: +SKIP
  927. >>> flybrains.download_jrc_transforms() # doctest: +SKIP
  928. Once `flybrains` is installed and you have downloaded the registrations,
  929. you can run this:
  930. >>> import navis
  931. >>> import flybrains
  932. >>> # navis example neurons are in raw (8nm voxel) hemibrain (JRCFIB2018Fraw) space
  933. >>> n = navis.example_neurons(1)
  934. >>> # Transform to FAFB14 space
  935. >>> xf = navis.xform_brain(n, source='JRCFIB2018Fraw', target='FAFB14') # doctest: +SKIP
  936. See Also
  937. --------
  938. [`navis.xform`][]
  939. Lower level entry point that takes data and applies a given
  940. transform or sequence thereof.
  941. [`navis.mirror_brain`][]
  942. Uses non-rigid transforms to mirror neurons from the left
  943. to the right side of given template brain and vice versa.
  944. """
  945. if not isinstance(source, str):
  946. TypeError(f'Expected source of type str, got "{type(source)}"')
  947. if not isinstance(target, str):
  948. TypeError(f'Expected target of type str, got "{type(target)}"')
  949. # Get the transformation sequence
  950. path, transforms = registry.find_bridging_path(source, target, via=via, avoid=avoid)
  951. if verbose:
  952. path_str = path[0]
  953. for p, tr in zip(path[1:], transforms):
  954. if isinstance(tr, AliasTransform):
  955. link = "="
  956. else:
  957. link = "->"
  958. path_str += f" {link} {p}"
  959. print("Transform path:", path_str)
  960. # Combine into transform sequence
  961. trans_seq = TransformSequence(*transforms)
  962. # Apply transform and returned xformed points
  963. xf = xform(x, transform=trans_seq, caching=caching, affine_fallback=affine_fallback)
  964. # We might be able to set the correct units based on the target template's
  965. # meta data (the "guessed" new units can be off if the transform is
  966. # not base 10 which happens for e.g. voxels -> physical space)
  967. if isinstance(xf, (core.NeuronList, core.BaseNeuron)):
  968. # First we need to find the last non-alias template space
  969. for tmp, tr in zip(path[::-1], transforms[::-1]):
  970. if not isinstance(tr, AliasTransform):
  971. # There is a chance that there is no meta data for this template
  972. try:
  973. last_temp = registry.find_template(tmp)
  974. except ValueError:
  975. break
  976. except BaseException:
  977. raise
  978. # If this template brain has a property for navis units
  979. if hasattr(last_temp, "_navis_units"):
  980. for n in core.NeuronList(xf):
  981. n.units = last_temp._navis_units
  982. break
  983. return xf
  984. def symmetrize_brain(
  985. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  986. template: Union[str, "TemplateBrain"],
  987. via: Optional[str] = "auto",
  988. progress: bool = True,
  989. verbose: bool = False,
  990. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  991. """Symmetrize 3D object (neuron, coordinates).
  992. The way this works is by:
  993. 1. Finding the closest mirror transform (unless provided)
  994. 2. Mirror data on the left-hand-side to the right-hand-side using the
  995. proper (warp) mirror transform to offset deformations
  996. 3. Simply flip that data back to the left-hand-side
  997. This works reasonably well but may produce odd results around the midline.
  998. For high quality symmetrization you are better off generating dedicated
  999. transform (see `navis-flybrains` for an example).
  1000. Parameters
  1001. ----------
  1002. x : Neuron/List | Volume/trimesh | numpy.ndarray | pandas.DataFrame
  1003. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  1004. columns. Numpy array must be shape `(N, 3)`.
  1005. template : str | TemplateBrain
  1006. Source template brain space that the data is in. If string
  1007. will be searched against registered template brains.
  1008. via : "auto" | str
  1009. By default ("auto") it will find and apply the closest
  1010. mirror transform. You can also specify a template that
  1011. should be used. That template must have a mirror transform!
  1012. progress : bool
  1013. Whether to show a progress bar when symmetrizing multiple
  1014. neurons.
  1015. verbose : bool
  1016. If True, will print some useful info on the transform(s).
  1017. Returns
  1018. -------
  1019. xs
  1020. Same object type as input (array, neurons, etc) but
  1021. hopefully symmetrical.
  1022. Examples
  1023. --------
  1024. This example requires the
  1025. [flybrains](https://github.com/navis-org/navis-flybrains)
  1026. library to be installed: `pip3 install flybrains`
  1027. >>> import navis
  1028. >>> import flybrains
  1029. >>> # Get the FAFB14 neuropil mesh
  1030. >>> m = flybrains.FAFB14.mesh
  1031. >>> # Symmetrize the mesh
  1032. >>> s = navis.symmetrize_brain(m, template='FAFB14')
  1033. >>> # Plot side-by-side for comparison
  1034. >>> m.plot3d() # doctest: +SKIP
  1035. >>> s.plot3d(color=(1, 0, 0)) # doctest: +SKIP
  1036. """
  1037. if not isinstance(template, str):
  1038. TypeError(f'Expected template of type str, got "{type(template)}"')
  1039. if via == "auto":
  1040. # Find closest mirror transform
  1041. via = registry.find_closest_mirror_reg(template)
  1042. if isinstance(x, core.NeuronList):
  1043. if len(x) == 1:
  1044. x = x[0]
  1045. else:
  1046. xf = []
  1047. for n in config.tqdm(
  1048. x,
  1049. desc="Mirroring",
  1050. disable=config.pbar_hide or not progress,
  1051. leave=config.pbar_leave,
  1052. ):
  1053. xf.append(symmetrize_brain(n, template=template, via=via))
  1054. return core.NeuronList(xf)
  1055. if isinstance(x, core.BaseNeuron):
  1056. x = x.copy()
  1057. if isinstance(x, core.Skeleton):
  1058. x.nodes = symmetrize_brain(x.nodes, template=template, via=via)
  1059. elif isinstance(x, core.Dotprops):
  1060. x.points = symmetrize_brain(x.points, template=template, via=via)
  1061. # Set tangent vectors and alpha to None so they will be regenerated
  1062. x._vect = x._alpha = None
  1063. elif isinstance(x, core.Mesh):
  1064. x.vertices = symmetrize_brain(x.vertices, template=template, via=via)
  1065. else:
  1066. raise TypeError(f"Don't know how to transform neuron of type '{type(x)}'")
  1067. if x.has_connectors:
  1068. x.connectors = symmetrize_brain(x.connectors, template=template, via=via)
  1069. return x
  1070. elif isinstance(x, tm.Trimesh):
  1071. x = x.copy()
  1072. x.vertices = symmetrize_brain(x.vertices, template=template, via=via)
  1073. return x
  1074. elif isinstance(x, pd.DataFrame):
  1075. if any([c not in x.columns for c in ["x", "y", "z"]]):
  1076. raise ValueError("DataFrame must have x, y and z columns.")
  1077. x = x.copy()
  1078. x[["x", "y", "z"]] = symmetrize_brain(
  1079. x[["x", "y", "z"]].values.astype(float), template=template, via=via
  1080. )
  1081. return x
  1082. else:
  1083. try:
  1084. # At this point we expect numpy arrays
  1085. x = np.asarray(x)
  1086. except BaseException:
  1087. raise TypeError(f'Unable to transform data of type "{type(x)}"')
  1088. if not x.ndim == 2 or x.shape[1] != 3:
  1089. raise ValueError("Array must be of shape (N, 3).")
  1090. # Now find the meta info for this template brain
  1091. if isinstance(template, TemplateBrain):
  1092. tb = template
  1093. else:
  1094. tb = registry.find_template(template, non_found="raise")
  1095. # Get the bounding box
  1096. if not hasattr(tb, "boundingbox"):
  1097. raise ValueError(f'Template "{tb.label}" has no bounding box info.')
  1098. if not isinstance(tb.boundingbox, (list, tuple, np.ndarray)):
  1099. raise TypeError(
  1100. "Expected the template brain's bounding box to be a "
  1101. f"list, tuple or array - got '{type(tb.boundingbox)}'"
  1102. )
  1103. # Get bounding box of template brain
  1104. bbox = np.asarray(tb.boundingbox)
  1105. # Reshape if flat array
  1106. if bbox.ndim == 1:
  1107. bbox = bbox.reshape(3, 2)
  1108. # Find points on the left
  1109. center = bbox[0][0] + (bbox[0][1] - bbox[0][0]) / 2
  1110. is_left = x[:, 0] > center
  1111. # Make a copy of the original data
  1112. x = x.copy()
  1113. # If nothing to symmetrize - return
  1114. if is_left.sum() == 0:
  1115. return x
  1116. # Mirror with compensation for deformations
  1117. xm = mirror_brain(
  1118. x[is_left], template=template, via=via, mirror_axis="x", verbose=verbose
  1119. )
  1120. # And now flip them back without compensation for deformations
  1121. xmf = mirror_brain(xm, template=template, warp=False, mirror_axis="x")
  1122. # Replace values
  1123. x[is_left] = xmf
  1124. return x
  1125. def mirror_brain(
  1126. x: Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"],
  1127. template: Union[str, "TemplateBrain"],
  1128. mirror_axis: Union[Literal["x"], Literal["y"], Literal["z"]] = "auto",
  1129. warp: Union[Literal["auto"], bool] = "auto",
  1130. via: Optional[str] = None,
  1131. verbose: bool = False,
  1132. progress: bool = True,
  1133. ) -> Union["core.NeuronObject", "pd.DataFrame", "np.ndarray"]:
  1134. """Mirror 3D object (neuron, coordinates) about given axis.
  1135. The way this works is:
  1136. 1. Look up the length of the template space along the given axis. For this,
  1137. the template space has to be registered (see docs for details).
  1138. 2. Flip object along midpoint of axis using a affine transformation.
  1139. 3. (Optional) Apply a warp transform that corrects asymmetries.
  1140. Parameters
  1141. ----------
  1142. x : Neuron/List | Volume/trimesh | numpy.ndarray | pandas.DataFrame
  1143. Data to transform. Dataframe must contain `['x', 'y', 'z']`
  1144. columns. Numpy array must be shape `(N, 3)`.
  1145. template : str | TemplateBrain
  1146. Source template brain space that the data is in. If string
  1147. will be searched against registered template brains.
  1148. Alternatively check out [`navis.transforms.mirror`][]
  1149. for a lower level interface.
  1150. mirror_axis : 'auto' | 'x' | 'y' | 'z', optional
  1151. Axis to mirror. If "auto" (default), will try get the correct
  1152. mirror axis from the template brain's meta data. If that is
  1153. not available, will default to "x".
  1154. warp : bool | "auto" | Transform, optional
  1155. If 'auto', will check if a non-rigid mirror transformation
  1156. exists for the given `template` and apply it after the
  1157. flipping. Alternatively, you can also pass a Transform or
  1158. TransformSequence directly.
  1159. via : str | None
  1160. If provided, (e.g. "FCWB") will first transform coordinates
  1161. into that space, then mirror and transform back.
  1162. Use this if there is no mirror registration for the original
  1163. template, or to transform to a symmetrical template in which
  1164. flipping is sufficient. Note that `mirror_axis` must match
  1165. the mirror axis of the "via" template!
  1166. verbose : bool
  1167. If True, will print some useful info on the transform(s).
  1168. progress : bool
  1169. Whether to show a progress bar when mirroring multiple
  1170. neurons.
  1171. Returns
  1172. -------
  1173. xf
  1174. Same object type as input (array, neurons, etc) but with
  1175. transformed coordinates.
  1176. Examples
  1177. --------
  1178. This example requires the
  1179. [flybrains](https://github.com/navis-org/navis-flybrains)
  1180. library to be installed: `pip3 install flybrains`
  1181. Also, if you haven't already, you will need to have the optional Saalfeld
  1182. lab (Janelia Research Campus) transforms installed (this is a one-off):
  1183. >>> import flybrains # doctest: +SKIP
  1184. >>> flybrains.download_jrc_transforms() # doctest: +SKIP
  1185. Once `flybrains` is installed and you have downloaded the registrations,
  1186. you can run this:
  1187. >>> import navis
  1188. >>> import flybrains
  1189. >>> # navis example neurons are in raw hemibrain (JRCFIB2018Fraw) space
  1190. >>> n = navis.example_neurons(1)
  1191. >>> # Mirror about x axis (this is a simple flip in this case)
  1192. >>> mirrored = navis.mirror_brain(n * 8 / 1000, tem plate='JRCFIB2018F', via='JRC2018F') # doctest: +SKIP
  1193. >>> # We also need to get back to raw coordinates
  1194. >>> mirrored = mirrored / 8 * 1000 # doctest: +SKIP
  1195. See Also
  1196. --------
  1197. [`navis.mirror`][]
  1198. Lower level function for mirroring. You can use this if
  1199. you want to mirror data without having a registered
  1200. template for it.
  1201. """
  1202. utils.eval_param(
  1203. mirror_axis,
  1204. name="mirror_axis",
  1205. allowed_values=("x", "y", "z", "auto"),
  1206. on_error="raise",
  1207. )
  1208. if not isinstance(warp, (BaseTransform, TransformSequence)):
  1209. utils.eval_param(
  1210. warp, name="warp", allowed_values=("auto", True, False), on_error="raise"
  1211. )
  1212. # If we go via another brain space
  1213. if via and via != template:
  1214. # Xform to "via" space
  1215. xf = xform_brain(x, source=template, target=via, verbose=verbose)
  1216. # Mirror
  1217. xfm = mirror_brain(
  1218. xf,
  1219. template=via,
  1220. mirror_axis=mirror_axis,
  1221. warp=warp,
  1222. progress=progress,
  1223. via=None,
  1224. )
  1225. # Xform back to original template space
  1226. xfm_inv = xform_brain(xfm, source=via, target=template, verbose=verbose)
  1227. return xfm_inv
  1228. if isinstance(x, core.NeuronList):
  1229. if len(x) == 1:
  1230. x = x[0]
  1231. else:
  1232. xf = []
  1233. for n in config.tqdm(
  1234. x,
  1235. desc="Mirroring",
  1236. disable=config.pbar_hide or not progress,
  1237. leave=config.pbar_leave,
  1238. ):
  1239. xf.append(
  1240. mirror_brain(
  1241. n, template=template, mirror_axis=mirror_axis, warp=warp
  1242. )
  1243. )
  1244. return core.NeuronList(xf)
  1245. if isinstance(x, core.BaseNeuron):
  1246. x = x.copy()
  1247. if isinstance(x, core.Skeleton):
  1248. x.nodes = mirror_brain(
  1249. x.nodes, template=template, mirror_axis=mirror_axis, warp=warp
  1250. )
  1251. elif isinstance(x, core.Dotprops):
  1252. if isinstance(x.k, type(None)) or x.k <= 0:
  1253. # If no k, we need to mirror vectors too. Note that this is less
  1254. # than ideal though! Here, we are scaling the vector by the
  1255. # dotprop's sampling resolution (i.e. ideally a representative
  1256. # distance between the points) because if the vectors are too
  1257. # small any warping transform will make them go haywire
  1258. hp = mirror_brain(
  1259. x.points + x.vect * x.sampling_resolution * 2,
  1260. template=template,
  1261. mirror_axis=mirror_axis,
  1262. warp=warp,
  1263. )
  1264. x.points = mirror_brain(
  1265. x.points, template=template, mirror_axis=mirror_axis, warp=warp
  1266. )
  1267. if isinstance(x.k, type(None)) or x.k <= 0:
  1268. # Re-generate vectors
  1269. vect = x.points - hp
  1270. vect = vect / np.linalg.norm(vect, axis=1).reshape(-1, 1)
  1271. x._vect = vect
  1272. else:
  1273. # Set tangent vectors and alpha to None so they will be
  1274. # regenerated on demand
  1275. x._vect = x._alpha = None
  1276. elif isinstance(x, core.Mesh):
  1277. x.vertices = mirror_brain(
  1278. x.vertices, template=template, mirror_axis=mirror_axis, warp=warp
  1279. )
  1280. # We also need to flip the normals
  1281. x.faces = x.faces[:, ::-1]
  1282. else:
  1283. raise TypeError(f"Don't know how to transform neuron of type '{type(x)}'")
  1284. if x.has_connectors:
  1285. x.connectors = mirror_brain(
  1286. x.connectors, template=template, mirror_axis=mirror_axis, warp=warp
  1287. )
  1288. return x
  1289. elif isinstance(x, tm.Trimesh):
  1290. x = x.copy()
  1291. x.vertices = mirror_brain(
  1292. x.vertices, template=template, mirror_axis=mirror_axis, warp=warp
  1293. )
  1294. # We also need to flip the normals
  1295. x.faces = x.faces[:, ::-1]
  1296. return x
  1297. elif isinstance(x, pd.DataFrame):
  1298. if any([c not in x.columns for c in ["x", "y", "z"]]):
  1299. raise ValueError("DataFrame must have x, y and z columns.")
  1300. x = x.copy()
  1301. x[["x", "y", "z"]] = mirror_brain(
  1302. x[["x", "y", "z"]].values,
  1303. template=template,
  1304. mirror_axis=mirror_axis,
  1305. warp=warp,
  1306. )
  1307. return x
  1308. else:
  1309. try:
  1310. # At this point we expect numpy arrays
  1311. x = np.asarray(x)
  1312. except BaseException:
  1313. raise TypeError(f'Unable to transform data of type "{type(x)}"')
  1314. if not x.ndim == 2 or x.shape[1] != 3:
  1315. raise ValueError("Array must be of shape (N, 3).")
  1316. if not isinstance(template, str):
  1317. TypeError(f'Expected template of type str, got "{type(template)}"')
  1318. if isinstance(warp, (BaseTransform, TransformSequence)):
  1319. mirror_trans = warp
  1320. elif warp:
  1321. # See if there is a mirror registration
  1322. mirror_trans = registry.find_mirror_reg(template, non_found="ignore")
  1323. # Get actual transform from tuple
  1324. if mirror_trans:
  1325. mirror_trans = mirror_trans.transform
  1326. # If warp was not "auto" and we didn't find a registration, raise
  1327. elif warp != "auto" and not mirror_trans:
  1328. raise ValueError(f'No mirror transform found for "{template}"')
  1329. else:
  1330. mirror_trans = None
  1331. # Now find the meta info about the template brain
  1332. if isinstance(template, TemplateBrain):
  1333. tb = template
  1334. else:
  1335. tb = registry.find_template(template, non_found="raise")
  1336. # Get the bounding box
  1337. if not hasattr(tb, "boundingbox"):
  1338. raise ValueError(f'Template "{tb.label}" has no bounding box info.')
  1339. if not isinstance(tb.boundingbox, (list, tuple, np.ndarray)):
  1340. raise TypeError(
  1341. "Expected the template brain's bounding box to be a "
  1342. f"list, tuple or array - got '{type(tb.boundingbox)}'"
  1343. )
  1344. # Get bounding box of template brain
  1345. bbox = np.asarray(tb.boundingbox)
  1346. # Reshape if flat array
  1347. if bbox.ndim == 1:
  1348. bbox = bbox.reshape(3, 2)
  1349. if isinstance(mirror_axis, str) and mirror_axis == "auto":
  1350. # Try to get mirror axis from template brain meta data
  1351. if hasattr(tb, "mirror_axis"):
  1352. mirror_axis = tb.mirror_axis
  1353. else:
  1354. # Default to x axis
  1355. mirror_axis = "x"
  1356. if verbose:
  1357. print(
  1358. f'No mirror axis info found for template "{tb.label}", defaulting to "x"'
  1359. )
  1360. # Index of mirror axis
  1361. ix = {"x": 0, "y": 1, "z": 2}[mirror_axis]
  1362. if bbox.shape == (3, 2):
  1363. # In nat.templatebrains this is using the sum (min+max) but have a
  1364. # suspicion that this should be the difference (max-min)
  1365. mirror_axis_size = bbox[ix, :].sum()
  1366. elif bbox.shape == (2, 3):
  1367. mirror_axis_size = bbox[:, ix].sum()
  1368. else:
  1369. raise ValueError(
  1370. f"Expected bounding box to be of shape (3, 2) or (2, 3) got {bbox.shape}"
  1371. )
  1372. return mirror(
  1373. x, mirror_axis=mirror_axis, mirror_axis_size=mirror_axis_size, warp=mirror_trans
  1374. )
  1375. class TemplateBrain:
  1376. """Generic base class for template brains.
  1377. Minimally, a template should have a `name` and `label` property. For
  1378. mirroring, it also needs a `boundingbox`.
  1379. See [flybrains](https://github.com/navis-org/navis-flybrains) for
  1380. an example of how to use template brains.
  1381. """
  1382. def __init__(self, **properties):
  1383. """Initialize class."""
  1384. for k, v in properties.items():
  1385. setattr(self, k, v)
  1386. @property
  1387. def mesh(self):
  1388. """Mesh represenation of this brain."""
  1389. if not hasattr(self, "_mesh"):
  1390. name = getattr(self, "regName", getattr(self, "name", None))
  1391. raise ValueError(f"{name} does not appear to have a mesh")
  1392. return self._mesh
  1393. def render_template(
  1394. x: "core.NeuronObject",
  1395. template: TemplateBrain,
  1396. source: Optional[str] = None,
  1397. depth: bool = False,
  1398. smooth: int = 0,
  1399. ) -> np.ndarray:
  1400. """Render neurons into template space.
  1401. Parameters
  1402. ----------
  1403. x : Skeleton | Mesh | Dotprops | NeuronList
  1404. Neuron(s) to render. Uses each neuron's nodes, points or
  1405. (solid-voxelized) mesh, respectively. Multiple neurons are
  1406. accumulated into the same grid.
  1407. template : TemplateBrain
  1408. Template to use for bounds, shape and voxel sizes.
  1409. source : str, optional
  1410. If provided, will first transform the neuron(s) from
  1411. `source` template space into the `template` space.
  1412. If not provided, will assume that the neuron(s) are already
  1413. in the `template` space.
  1414. depth : bool
  1415. Only affects Meshes: if True, weigh each mesh voxel by
  1416. its distance to the surface (via
  1417. [`sparsecubes.measure.distance_transform`][]) instead of a
  1418. flat occupancy of 1. Thick regions (e.g. the soma) then
  1419. contribute more than thin neurites. Skeletons and
  1420. Dotprops are unaffected (still binned by point count).
  1421. smooth : int
  1422. If non-zero, will apply a Gaussian filter with `smooth`
  1423. as `sigma`.
  1424. Returns
  1425. -------
  1426. numpy array
  1427. 3D numpy array with rendered neuron(s) in template space.
  1428. The shape of the array is determined by the `template`
  1429. bounding box and voxel size.
  1430. Examples
  1431. --------
  1432. This example requires the
  1433. [flybrains](https://github.com/navis-org/navis-flybrains)
  1434. library to be installed: `pip3 install flybrains`
  1435. >>> import navis
  1436. >>> import flybrains
  1437. >>> # Neurons must be in - or transformed into - the template's space
  1438. >>> n = navis.example_neurons(5)
  1439. >>> # Render into the JRC2018F template grid
  1440. >>> img = navis.render_template(n, template=flybrains.JRC2018F, # doctest: +SKIP
  1441. ... source='JRCFIB2018Fraw')
  1442. """
  1443. if not isinstance(template, TemplateBrain):
  1444. raise TypeError(
  1445. f"Expected `template` to be of type TemplateBrain, got {type(template)}"
  1446. )
  1447. for attr in ("dims", "boundingbox", "voxdims"):
  1448. if not hasattr(template, attr):
  1449. raise ValueError(f'Template "{template.name}" must have `.{attr}` defined.')
  1450. pitch = np.asarray(template.voxdims, dtype=float)
  1451. bounds = np.asarray(template.boundingbox, dtype=float).reshape((3, 2))
  1452. shape = np.asarray(template.dims)
  1453. if (bounds[:, 0] >= bounds[:, 1]).any():
  1454. raise ValueError(
  1455. "Template bounding box must have lower bounds smaller than upper "
  1456. "bounds:",
  1457. bounds,
  1458. )
  1459. if shape.ndim != 1 or len(shape) != 3:
  1460. raise ValueError(
  1461. "Expected template `dims` to be a list, tuple or array of length "
  1462. f"3, got {template.dims}"
  1463. )
  1464. shape = tuple(int(d) for d in shape)
  1465. # Voxel index of the bounding box's lower corner. Both the binned points and
  1466. # the mesh voxels are mapped with the same round-to-nearest convention used
  1467. # by `neuron2voxels`/`sparsecubes`, so the two line up in the same grid.
  1468. offset = np.round(bounds[:, 0] / pitch).astype(int)
  1469. # Guard against templates whose (fixed-size) grid would exhaust memory
  1470. utils.check_grid_size(
  1471. shape, np.float32, hint="The template's voxel grid is very large."
  1472. )
  1473. # Transform the neuron(s) into template space
  1474. if source:
  1475. x = xform_brain(x, source=source, target=template.name)
  1476. from .. import sampling
  1477. image = np.zeros(shape, dtype=np.float32)
  1478. def _accumulate(voxels, weights=1.0):
  1479. """Add voxels (in template-grid indices) to the image, dropping any
  1480. that fall outside the grid."""
  1481. voxels = np.asarray(voxels)
  1482. if not len(voxels):
  1483. return
  1484. keep = np.all((voxels >= 0) & (voxels < shape), axis=1)
  1485. voxels = voxels[keep]
  1486. if not len(voxels):
  1487. return
  1488. if not np.isscalar(weights):
  1489. weights = np.asarray(weights)[keep]
  1490. image[voxels[:, 0], voxels[:, 1], voxels[:, 2]] += weights
  1491. # Iterate over neurons and fill the image grid
  1492. for neuron in core.NeuronList(x):
  1493. if isinstance(neuron, core.Mesh):
  1494. # Meshes are voxelized properly - walking the surface and filling
  1495. # the interior via `sparsecubes` - rather than just binning their
  1496. # vertices, which would miss any face larger than a voxel.
  1497. with warnings.catch_warnings(record=True) as caught:
  1498. warnings.simplefilter("always")
  1499. voxels = sparsecubes.voxelize(neuron.trimesh, spacing=pitch)
  1500. # Neuron meshes are routinely not watertight, so a `sparsecubes`
  1501. # warning about unfilled columns is expected rather than exceptional
  1502. # - demote it to a debug log and re-raise anything else.
  1503. for w in caught:
  1504. if "watertight" in str(w.message):
  1505. logger.debug(f"Voxelizing {neuron.id}: {w.message}")
  1506. else:
  1507. warnings.warn_explicit(
  1508. w.message, w.category, w.filename, w.lineno
  1509. )
  1510. # `sparsecubes` already returns unique voxels in the same
  1511. # convention. By default each occupied voxel contributes 1; with
  1512. # `depth` it instead contributes its distance to the surface, so
  1513. # thick regions weigh more than thin neurites.
  1514. if depth:
  1515. weights = sparsecubes.measure.distance_transform(
  1516. voxels, spacing=pitch
  1517. )
  1518. _accumulate(voxels - offset, weights)
  1519. else:
  1520. _accumulate(voxels - offset)
  1521. elif isinstance(neuron, (core.Skeleton, core.Dotprops)):
  1522. if isinstance(neuron, core.Skeleton):
  1523. # Resample first so that long edges don't leave gaps between
  1524. # nodes in the grid
  1525. neuron = sampling.resample_skeleton(
  1526. neuron, resample_to=pitch.min() / 2
  1527. )
  1528. pts = neuron.nodes[["x", "y", "z"]].values
  1529. else:
  1530. pts = neuron.points
  1531. # Bin the points and accumulate per-voxel counts
  1532. voxels = np.round(pts / pitch).astype(int) - offset
  1533. voxels, counts = np.unique(voxels, axis=0, return_counts=True)
  1534. _accumulate(voxels, counts.astype(np.float32))
  1535. else:
  1536. raise TypeError(
  1537. f"Don't know how to render neuron of type '{type(neuron)}' "
  1538. "into template space."
  1539. )
  1540. # Apply Gaussian filter
  1541. if smooth:
  1542. from scipy.ndimage import gaussian_filter
  1543. image = gaussian_filter(image, sigma=smooth)
  1544. return image
  1545. # Initialize the registry
  1546. registry = TemplateRegistry()

templates.py at commit cb9a591, 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
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 26 September 2026
Evidence: files inventoried
Commit: cb9a5915b6b3587cb81154f4f77ffc62fe12b03a, 6 September 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 (181 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: 1 check, the latest on 26 September 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.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
[2] doi:10.1038/s41467-026-69633-4 [code]
Visuomotor decision-making through multifeature convergence in the larval zebrafish hindbrain.
Journal: Nature communications
In common: h5py, seaborn, scikit-learn, 4 other tools, zebrafish, 7 references
[3] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: tifffile, igraph, Numba, 11 other tools
[4] doi:10.1038/s41467-026-76242-8 [code]
Whole-brain, all-optical interrogation of neuronal dynamics underlying gut and vascular interoception in zebrafish.
Journal: Nature communications
In common: tifffile, NetworkX, OpenCV, 8 other tools, zebrafish, systems, 2 references
[5] doi:10.1038/s41598-026-57519-w [code]
Automated segmentation of neurons and spinal cord structures in immunofluorescence images using SpineDL.
Journal: Scientific reports
In common: tifffile, Numba, NetworkX, 10 other tools
[6] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: igraph, Numba, NetworkX, 9 other tools, zebrafish
[7] doi:10.1038/s41593-026-02267-3 [code]
Spatial proteomic analysis in human Alzheimer's disease brains enables identification of microenvironment-dependent microglial cell states.
Journal: Nature neuroscience
In common: tifffile, igraph, Numba, 9 other tools
[8] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: Numba, NetworkX, Plotly, 9 other tools, systems
[9] doi:10.1016/j.stemcr.2026.103015 [code]
Brain injury reactivates a developmental program driving genesis and integration of transient LGE-class interneurons.
Journal: Stem cell reports
In common: tifffile, Numba, NetworkX, 9 other tools
[10] doi:10.1016/j.isci.2026.116206 [code]
Gut distension evokes rapid neural dynamics in vagal and hindbrain populations of larval zebrafish.
Journal: iScience
In common: tifffile, NetworkX, OpenCV, 7 other tools, zebrafish, systems, 1 reference

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.

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: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

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