Skip to content

fastcore-rs

Fast implementations of the algorithms that navis and nat need most: traversal and geometry on rooted trees (i.e. neuron skeletons), plus NBLAST neuron similarity.

The project is a single Rust core with bindings on top:

Package Language
fastcore Rust the core crate
navis-fastcore Python (PyO3) docs
nat.fastcore R (extendr) docs

The bindings are deliberately generalized: they know nothing about navis or nat themselves, so third-party libraries can use them without those rather heavy dependencies. navis-fastcore only requires numpy.

Install

pip install navis-fastcore

Pre-compiled wheels are on PyPI. navis will use fastcore automatically where appropriate; see the Python docs if you want to call it directly.

install.packages(
  "nat.fastcore",
  repos = c("https://schlegelp.r-universe.dev", "https://cloud.r-project.org")
)

Pre-compiled binaries for Windows and macOS come from R-universe, so no Rust toolchain is needed. On Linux this builds from source. See the R docs.

[dependencies]
fastcore = { git = "https://github.com/schlegelp/fastcore-rs" }

The crate is not published to crates.io — depend on it via git. See the Rust docs.

What's available where

Every capability lives in the Rust core; the bindings differ only in what they surface and how they spell it. — means that surface doesn't expose it directly.

Rooted trees (skeletons)

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Geodesic distance matrix dag::geodesic_distances_all_by_all, dag::geodesic_distances_partial geodesic_matrix geodesic_distances
Geodesic distances for given pairs dag::geodesic_pairs geodesic_pairs geodesic_pairs
Distance to the nearest target dag::geodesic_nearest geodesic_nearest geodesic_nearest
Distance to the farthest target dag::geodesic_farthest geodesic_farthest geodesic_farthest
Distances to the root dag::all_dists_to_root, dag::dist_to_root dist_to_root all_dists_to_root, dist_to_root
Subtree height (distance down to the farthest leaf) dag::subtree_height subtree_height subtree_height
Connected components dag::connected_components connected_components connected_components
Break into linear segments dag::generate_segments, dag::break_segments generate_segments, break_segments, segment_coords generate_segments, break_segments
Strahler index dag::strahler_index strahler_index strahler_index
Prune twigs dag::prune_twigs prune_twigs prune_twigs1
Classify nodes (root/leaf/branch/slab) dag::classify_nodes classify_nodes classify_nodes
Sub-tree below a node dag::descendants descendants descendants
Path from a node to its root dag::paths_to_root paths_to_root paths_to_root
Re-root dag::reroot reroot reroot
Contract nodes dag::contract_nodes contract_nodes contract_nodes
Simplify to roots/leafs/branches dag::simplify_skeleton simplify_skeleton simplify_skeleton
Downsample (keep every Nth node) downsample::downsample_skeleton downsample_skeleton downsample_skeleton
Simplify by geometry (RDP / Visvalingam-Whyatt) downsample::simplify_rdp, downsample::simplify_vw simplify_rdp, simplify_vw simplify_rdp, simplify_vw
Resample to a fixed spacing downsample::resample_skeleton resample_skeleton resample_skeleton
Smooth node coordinates, or any per-node column downsample::smooth_skeleton, downsample::smooth_skeleton_gaussian smooth_skeleton, smooth_skeleton_gaussian smooth_skeleton, smooth_skeleton_gaussian
Adjacency matrix (CSR) dag::adjacency adjacency adjacency
Longest path(s) dag::longest_path, dag::longest_paths longest_path, longest_paths longest_path, longest_paths
Betweenness centrality dag::betweenness betweenness betweenness
Descendant counts dag::descendant_counts descendant_counts descendant_counts
Synapse flow centrality dag::synapse_flow_centrality synapse_flow_centrality synapse_flow_centrality
Cycle detection dag::has_cycles has_cycles has_cycles
Node ID → index mapping — — (done internally) node_indices
Child → parent distances — parent_dist child_to_parent_dists

Healing fragmented skeletons

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Reconnect a broken skeleton (composed in the bindings) heal_skeleton heal_skeleton
Minimal reconnecting edges topo::stitch_fragments stitch_fragments stitch_fragments
Regenerate the parent vector topo::reroot_rewire reroot_rewire reroot_rewire

Meshes

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Connected components of a triangle mesh, by vertex, face or manifold edge mesh::mesh_connected_components, mesh::mesh_face_components mesh_connected_components mesh_connected_components
Geodesic distances across a mesh mesh::geodesic_matrix_mesh geodesic_matrix_mesh geodesic_matrix_mesh
Nearest / farthest target vertex mesh::geodesic_nearest_mesh, mesh::geodesic_farthest_mesh geodesic_nearest_mesh, geodesic_farthest_mesh geodesic_nearest_mesh, geodesic_farthest_mesh
Geodesic distances across any graph mesh::geodesic_matrix_graph geodesic_matrix_graph geodesic_matrix_graph
Unique undirected edges of a mesh mesh::unique_edges unique_edges unique_edges
Connected components of any graph mesh::connected_components_graph connected_components_graph connected_components_graph
Components of every level set at once mesh::level_set_components level_set_components level_set_components
Contract nodes and simplify the edge list mesh::contract_vertices contract_vertices contract_vertices
Minimum / maximum spanning forest mesh::minimum_spanning_tree minimum_spanning_tree minimum_spanning_tree
Orient an edge list into a rooted spanning forest mesh::parents_from_edges parents_from_edges parents_from_edges
Which edges are bridges (may not be dropped) mesh::bridges bridges bridges
Spanning tree over a subset, by geodesic distance, without the k x k matrix mesh::geodesic_mst_mesh, mesh::geodesic_mst_graph geodesic_mst_mesh, geodesic_mst_graph geodesic_mst_mesh, geodesic_mst_graph
Shortest paths with the route, not just the length mesh::geodesic_predecessors_graph, mesh::geodesic_path_graph geodesic_predecessors, geodesic_path geodesic_predecessors, geodesic_path
Greedy clustering by geodesic radius mesh::geodesic_clusters geodesic_clusters geodesic_clusters
Simplify a mesh, reporting where every original vertex went simplify::simplify_mesh, simplify::simplify_mesh_lossless simplify_mesh, simplify_mesh_lossless simplify_mesh, simplify_mesh_lossless
A graph prepared once for many geodesic queries (all of the above as methods, minus the per-call index build) mesh::GeodesicGraph GeodesicGraph —
Induced subgraph, carved from a prepared graph mesh::GeodesicGraph::subset GeodesicGraph.subset —
Grow a connected region of a fixed number of nodes/points, with each one's distance to the seed mesh::GeodesicGraph::grow GeodesicGraph.grow —
Farthest-point sampling: evenly-spread seeds along the graph mesh::GeodesicGraph::farthest_seed GeodesicGraph.farthest_seed —
Everything within a radius of a set of nodes, and which of them is nearest mesh::GeodesicGraph::ball GeodesicGraph.ball —
Re-weight edges of a prepared graph in place mesh::GeodesicGraph::set_weights GeodesicGraph.set_weights —

Neuron similarity

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Tangent vectors + alpha from a point cloud points::dotprops dotprops, Dotprop.from_points —
NBLAST, query vs target nblast::nblast_query_target nblast nblast
NBLAST, all-by-all nblast::nblast_allbyall nblast_allbyall nblast_allbyall
NBLAST for explicit index pairs nblast::nblast_pairs nblast_pairs nblast_pairs
k nearest neighbours, no score matrix nblast_knn::nblast_knn, nblast_knn::nblast_knn_query_target nblast_knn nblast_knn
Two-pass approximate NBLAST — nblast_smart —
synNBLAST (synapse-based) synblast::synblast_query_target, synblast::synblast_allbyall synblast, Synapses synblast, synblast_allbyall
limit_dist heuristic for a scoring matrix nblast::Smat — (via limit_dist="auto") smat_auto_limit

Match extraction

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Top-N matches per query matches::top_matches top_matches —
Matches above a threshold / percentage band matches::matches_above matches_above —
Count matches without allocating them matches::count_matches count_matches —

Clustering

The other thing you do with a score matrix. All three surfaces produce the same merge tree; what differs is the object it arrives in — a SciPy-compatible (n-1, 4) linkage matrix in Rust and Python, an hclust in R.

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Hierarchical clustering, from a score matrix or condensed distances linkage::linkage_from_scores, linkage::linkage linkage nblast_hclust, fast_hclust
Condensed distances from a score matrix (fused symmetrise + transform + condense) linkage::condense condensed_distances nblast_dist
Symmetrise a score matrix against its transpose linkage::symmetrize symmetrize symmetrize2
Dendrogram leaf order (SciPy's leaves_list) linkage::leaf_order leaf_order leaf_order3

Spatial transforms

Capability Rust (fastcore) Python (navis-fastcore) R (nat.fastcore)
Read a CMTK *.list registration cmtk::Registration::from_path, cmtk::Chain CmtkRegistration, load_cmtk_registration cmtk_read
Apply it to points (forward / inverse) cmtk::transform_points, cmtk::inverse_transform_points CmtkRegistration.xform, .xform_inv cmtk_xform, cmtk_xform_inv
CMTK registration properties fields on Registration / SplineWarp .affine, .domain, .dims, .spacing cmtk_affine, cmtk_domain, cmtk_dims, cmtk_spacing
Read an Elastix TransformParameters file elastix::ElastixTransform::from_path, elastix::Chain ElastixTransform, load_elastix_transform elastix_read
Apply it to points (forward / inverse) elastix::transform_points, elastix::inverse_transform_points ElastixTransform.xform, .xform_inv elastix_xform, elastix_xform_inv
Elastix transform properties fields on Linear / BSpline .affine, .kinds, .grid_size, .grid_spacing, .grid_origin elastix_affine, elastix_kinds, elastix_grid_size, elastix_grid_spacing, elastix_grid_origin
Is an Elastix file invertible? (without parsing it) elastix::probe_invertible probe_elastix_invertible elastix_probe_invertible
Fit a thin-plate spline to landmarks tps::TpsTransform::fit, ::from_coefs TpsTransform, .from_coefs tps_transform, tps_coefs
Moving least squares from landmarks mls::MlsTransform::new MlsTransform mls_transform
Apply a landmark transform to points TpsTransform::xform, MlsTransform::xform .xform tps_xform, mls_xform
Its global affine .matrix_affine .matrix_affine tps_affine, mls_affine

Neither CMTK nor Elastix needs to be installed — see CMTK transforms and Elastix transforms. The two differ in one important respect: outside the region it was fitted on, CMTK fails a point (we return NaN) while Elastix returns it unchanged. Each binding follows its own tool. Elastix additionally gains an inverse, which Elastix itself cannot compute.

Both work the same way: a transform object holds only the parse, and direction is chosen per call (invert=, or the _inv method), so one object serves every direction rather than re-reading the file to walk it the other way.

When there is no registration at all — only matched landmarks — the thin-plate spline and moving least squares transforms take over; see Landmark transforms. Neither materialises the n_points x n_landmarks matrix its reference implementation does, so there is no batch size to tune, and moving least squares becomes usable at landmark counts (~3,400) that would otherwise need tens of GB.

Python only

navis_fastcore.wrappers.csgraph provides drop-in replacements for some scipy.sparse.csgraph routines (dijkstra, connected_components) that are faster when your graph happens to be a rooted tree — see Scipy wrappers.

A note on input conventions

The three surfaces take the same tree in different shapes:

  • Rust is index-based: a parent-index vector in which roots are negative.
  • Python takes node_ids and parent_ids arrays with arbitrary IDs and maps them to indices for you.
  • R takes a precomputed parents index vector (build one with node_indices) plus separate x/y/z vectors.

So what reads geodesic_matrix(node_ids, parent_ids) in Python reads geodesic_distances(parents, ...) in R, with node_indices called first.


  1. R's prune_twigs has no mask argument — extendr cannot take a Vec<bool>. ↩

  2. Rust and Python symmetrise in place and allocate nothing. R's value semantics forbid writing to the caller's matrix, so nat.fastcore copies once and returns the copy. ↩

  3. R's takes an hclust (or its merge matrix) and returns a 1-based ordering; Rust and Python take the linkage matrix and return 0-based indices. An untouched hclust already carries the same ordering in $order — the R function is for a merge matrix you built or edited yourself. ↩