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.