Meshes¶
Routines that treat a triangle mesh as a graph over its vertices, with the edges taken from the faces.
Connected components¶
Labelling the connected components of a triangle mesh, i.e. finding which vertices are reachable from which through shared faces.
import navis_fastcore as fastcore
import numpy as np
# Two disconnected triangles
faces = np.array([[0, 1, 2], [3, 4, 5]], dtype=np.uint32)
fastcore.mesh_connected_components(faces, n_vertices=6)
# array([0, 0, 0, 3, 3, 3], dtype=uint32)
navis_fastcore.mesh_connected_components(faces, n_vertices)
¶
Find connected components of a triangle mesh.
Uses Union-Find (DSU) with path-halving. The only extra allocation is a
single integer array of length n_vertices — no adjacency list is built.
| PARAMETER | DESCRIPTION |
|---|---|
faces
|
TYPE:
|
n_vertices
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
components
|
For each vertex the index of the root vertex of its connected component. Vertices that share a component will have the same value (the smallest vertex index in that component).
TYPE:
|
Examples:
Two triangles sharing an edge — one component:
>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.uint32)
>>> fastcore.mesh_connected_components(faces, n_vertices=4)
array([0, 0, 0, 0], dtype=uint32)
Two disjoint triangles — two components:
Geodesic distances¶
The mesh counterpart to geodesic_matrix, which works on skeletons. A skeleton
is a tree, so distances there come from walking to the lowest common ancestor. A mesh is a
general cyclic graph, so this runs a Dijkstra per source instead — in parallel, which is
the whole point: scipy.sparse.csgraph.dijkstra holds the GIL, so you cannot get that
speedup from Python by threading it yourself.
import navis_fastcore as fastcore
import numpy as np
# Two triangles sharing the 1-2 edge, forming a unit square
faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.uint32)
vertices = np.array([[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0]], dtype=np.float64)
fastcore.geodesic_matrix_mesh(faces, vertices)
# array([[0. , 1. , 1. , 2. ],
# [1. , 0. , 1.4142135, 1. ],
# [1. , 1.4142135, 0. , 1. ],
# [2. , 1. , 1. , 0. ]], dtype=float32)
Mind the size of the output
A full V x V matrix is around 107 GB at V=164k, so for anything but a small mesh you
want sources and/or targets.
targets is worth calling out. scipy.sparse.csgraph.dijkstra has no notion of
targets: it always materialises all V columns and makes you slice afterwards. Passing
targets here means only those columns are ever allocated — for 200 sources and 100
targets on a 41k-vertex mesh that is 0.03 MB instead of 70 MB.
If you only need the nearest (or farthest) target, use
geodesic_nearest_mesh instead — its output is
O(sources) rather than O(sources x targets), and it is faster too, because the
search stops at the first target it settles.
This is the along-edge distance
Shortest paths are constrained to run along mesh edges, so on a coarse mesh they overshoot the true surface geodesic. This is the same approximation navis makes.
navis_fastcore.geodesic_matrix_mesh(faces, vertices=None, n_vertices=None, sources=None, targets=None, limit=None, threads=None)
¶
Calculate geodesic ("along-the-mesh-edge") distances on a triangle mesh.
This is the mesh counterpart to :func:~navis_fastcore.geodesic_matrix, which
works on skeletons. Where the skeleton version exploits the tree structure, a
mesh is a general cyclic graph, so this runs a parallel Dijkstra (or a BFS when
unweighted) over the vertex adjacency derived from faces.
Notes
This is the distance along mesh edges, not the exact surface geodesic: shortest paths are constrained to run along edges, so on a coarse mesh they overshoot the true surface distance.
Beware the size of the output. A full V x V matrix is ~107 GB at V=164k, so
for anything but a small mesh you want sources and/or targets. Unlike
scipy.sparse.csgraph.dijkstra — which has no notion of targets and always
materialises all V columns before you can slice them — targets here means
only those columns are ever allocated.
| PARAMETER | DESCRIPTION |
|---|---|
faces
|
TYPE:
|
vertices
|
TYPE:
|
n_vertices
|
TYPE:
|
sources
|
TYPE:
|
targets
|
TYPE:
|
limit
|
TYPE:
|
threads
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
matrix
|
Geodesic distances. Unreachable pairs — disconnected, or beyond
TYPE:
|
Examples:
Two triangles sharing the 1-2 edge, forming a unit square. Vertices 0 and 3 are the opposite corners, so they are not directly connected — the shortest path between them goes around, via 1 or 2:
>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.uint32)
>>> vertices = np.array([[0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [1, 1, 0]], dtype=np.float64)
>>> fastcore.geodesic_matrix_mesh(faces, vertices)
array([[0. , 1. , 1. , 2. ],
[1. , 0. , 1.4142135, 1. ],
[1. , 1.4142135, 0. , 1. ],
[2. , 1. , 1. , 0. ]], dtype=float32)
Without vertices every edge has weight 1, so you get hop counts instead — the
shared diagonal 1-2 is now a single hop rather than sqrt(2):
navis_fastcore.geodesic_nearest_mesh(faces, vertices=None, n_vertices=None, sources=None, targets=None, limit=None, threads=None)
¶
For each source vertex, find the nearest target vertex on a mesh.
A memory-efficient alternative to :func:~navis_fastcore.geodesic_matrix_mesh:
it keeps only the nearest target and the distance to it, so the output is
O(len(sources)) rather than O(len(sources) * len(targets)). It is also
faster, because the search stops at the first target it settles instead of
exploring the whole connected component.
| PARAMETER | DESCRIPTION |
|---|---|
faces
|
TYPE:
|
vertices
|
TYPE:
|
n_vertices
|
TYPE:
|
sources
|
TYPE:
|
targets
|
TYPE:
|
limit
|
TYPE:
|
threads
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
distances
|
Distance from each source to its nearest target;
TYPE:
|
nearest
|
Vertex index of that nearest target;
TYPE:
|
Notes
A source that is itself a target is matched to its nearest distinct target, never to itself (so the distance is never trivially 0). Ties break towards the lower vertex index.
Examples:
>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.uint32)
>>> vertices = np.array([[0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [1, 1, 0]], dtype=np.float64)
>>> dists, nearest = fastcore.geodesic_nearest_mesh(
... faces, vertices, sources=[0], targets=[2, 3]
... )
>>> dists
array([1.], dtype=float32)
>>> nearest
array([2], dtype=int32)
navis_fastcore.geodesic_farthest_mesh(faces, vertices=None, n_vertices=None, sources=None, targets=None, limit=None, threads=None)
¶
For each source vertex, find the farthest target vertex on a mesh.
The mirror image of :func:~navis_fastcore.geodesic_nearest_mesh, with the same
O(len(sources)) memory footprint. Unlike nearest, this cannot stop early
— it has to settle every target — but the farthest one then comes for free, since
the search settles vertices in increasing order of distance.
| PARAMETER | DESCRIPTION |
|---|---|
faces
|
TYPE:
|
vertices
|
TYPE:
|
n_vertices
|
TYPE:
|
sources
|
TYPE:
|
targets
|
TYPE:
|
limit
|
TYPE:
|
threads
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
distances
|
Distance from each source to its farthest target;
TYPE:
|
farthest
|
Vertex index of that farthest target;
TYPE:
|
Notes
As with nearest, a source that is itself a target is matched to a distinct
target, never to itself.
Examples:
>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.uint32)
>>> vertices = np.array([[0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [1, 1, 0]], dtype=np.float64)
>>> dists, farthest = fastcore.geodesic_farthest_mesh(
... faces, vertices, sources=[0], targets=[2, 3]
... )
>>> farthest
array([3], dtype=int32)
Arbitrary graphs¶
The same kernel, over an explicit edge list rather than a mesh. Unlike
geodesic_matrix, this makes no tree assumption, so cycles are fine.
import navis_fastcore as fastcore
import numpy as np
# A triangle. The direct 0-2 edge has weight 5, so the shortest
# path between them goes the long way round via 1.
edges = np.array([[0, 1], [1, 2], [2, 0]], dtype=np.uint32)
weights = np.array([1, 1, 5], dtype=np.float32)
fastcore.geodesic_matrix_graph(edges, 3, weights=weights)
# array([[0., 1., 2.],
# [1., 0., 1.],
# [2., 1., 0.]], dtype=float32)
navis_fastcore.geodesic_matrix_graph(edges, n_nodes, weights=None, directed=False, sources=None, targets=None, limit=None, threads=None)
¶
Calculate geodesic distances over an arbitrary graph.
The general form of :func:~navis_fastcore.geodesic_matrix_mesh. Unlike
:func:~navis_fastcore.geodesic_matrix, this makes no tree assumption — cycles
are fine.
| PARAMETER | DESCRIPTION |
|---|---|
edges
|
TYPE:
|
n_nodes
|
TYPE:
|
weights
|
TYPE:
|
directed
|
TYPE:
|
sources
|
TYPE:
|
targets
|
TYPE:
|
limit
|
TYPE:
|
threads
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
matrix
|
Geodesic distances;
TYPE:
|
Examples:
A triangle — a cycle, which the skeleton functions would reject. Note the direct 0-2 edge has weight 5, so the shortest path goes the long way round via 1:
>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> edges = np.array([[0, 1], [1, 2], [2, 0]], dtype=np.uint32)
>>> weights = np.array([1, 1, 5], dtype=np.float32)
>>> fastcore.geodesic_matrix_graph(edges, 3, weights=weights)
array([[0., 1., 2.],
[1., 0., 1.],
[2., 1., 0.]], dtype=float32)