Skip to content

NBLAST

See Concepts › NBLAST for how the scoring works and what the Rust pipeline does under the hood. This page covers the Python API.

A "dotprop" is any object exposing points (an (N, 3) array of coordinates) and vect (an (N, 3) array of unit tangent vectors). When use_alpha is enabled it must additionally expose alpha (an (N,) array).

Building dotprops

dotprops derives vect and alpha from a bare point cloud, so nothing outside fastcore is needed to get to a scoreable neuron:

import navis_fastcore as fastcore

dp = fastcore.Dotprop.from_points(points, k=20)      # or:
vect, alpha = fastcore.dotprops(points, k=20)

k counts the point itself, matching scipy.spatial.cKDTree.query and what navis does. This replaces the usual cKDTree.query plus N 3x3 SVDs — a single-threaded step that on 100,000 points takes ~510 ms against ~33 ms here, and which was the only reason navis-fastcore would have needed scipy at all.

The sign of each tangent vector is arbitrary (NBLAST scores on |dot|) but deterministic. Degenerate neighbourhoods — coincident points — give alpha = 0 and a unit vector rather than the NaNs that would silently poison every score they touch.

Compute tangent vectors and alpha values for a point cloud.

For each point, takes its k nearest neighbours (the point itself included), forms the scatter matrix of that neighbourhood about its centroid, and returns the principal direction plus a measure of how elongated the neighbourhood is. Together with the points themselves those are the dotprops representation :func:~navis_fastcore.nblast consumes - see :meth:navis_fastcore.Dotprop.from_points for the one-call form.

This replaces the usual scipy.spatial.cKDTree.query plus N 3x3 SVDs, which is single-threaded and was the only reason navis-fastcore would have needed scipy at all.

PARAMETER DESCRIPTION
points
  Point coordinates. Converted to float64. Must be finite.

TYPE: (N, 3) array

k
  Number of nearest neighbours, *including the point itself* - i.e.
  ``k=20`` uses 19 other points. This matches
  ``scipy.spatial.cKDTree.query(x, k=k)``, which returns the query point
  as its own first neighbour. Clamped to ``N``.

TYPE: int DEFAULT: 20

threads
  Number of threads. If ``None`` uses all available cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
vect

Principal direction of each neighbourhood, unit length.

TYPE: (N, 3) float64 array

alpha

(l1 - l2) / (l1 + l2 + l3) for scatter matrix eigenvalues l1 >= l2 >= l3. 0 for an isotropic neighbourhood, 1 for a perfectly collinear one.

TYPE: (N, ) float64 array

Notes

The sign of each vector is arbitrary - an eigenvector is only defined up to sign, and NBLAST scores on |dot| so it never observes the choice. It is nonetheless deterministic here (the largest-magnitude component is forced positive), so results are reproducible run to run and across thread counts.

Degenerate neighbourhoods - all-coincident points, or a single point - have no defined direction and an alpha of 0/0. They come back as alpha = 0 with the unit vector [1, 0, 0], rather than the NaNs that would silently poison every downstream NBLAST score.

Examples:

Points on a line are perfectly anisotropic:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> pts = np.zeros((10, 3))
>>> pts[:, 0] = np.arange(10)
>>> vect, alpha = fastcore.dotprops(pts, k=5)
>>> alpha.round(6)
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
>>> vect[0]
array([1., 0., 0.])

All-by-all

import navis_fastcore as fastcore

import numpy as np
from collections import namedtuple

Dotprop = namedtuple("Dotprop", ["points", "vect"])

# 10 example dotprops (use real tangent vectors in practice)
dps = [Dotprop(np.random.rand(100, 3), np.random.rand(100, 3)) for _ in range(10)]

# (10, 10) float32 matrix; row = query, column = target, diagonal = 1.0
scores = fastcore.nblast_allbyall(dps)

# Symmetric scores (also 'min' / 'max')
scores_sym = fastcore.nblast_allbyall(dps, symmetry="mean")

Query vs target

# (len(query), len(target)) score matrix
scores = fastcore.nblast(query_dps, target_dps)

# 'mean' / 'min' / 'max' also combine with the reverse (target-vs-query) NBLAST
scores = fastcore.nblast(query_dps, target_dps, symmetry="mean")

Selected pairs only

When you already know which comparisons you want — a candidate list from a cheaper filter, the non-zero cells of a connectivity matrix, a set of putative matches to verify — nblast_pairs scores exactly those and hands back one value per pair instead of a matrix:

pairs = np.array([(0, 5), (0, 7), (3, 5)])      # positions, not IDs
scores = fastcore.nblast_pairs(query_dps, target_dps, pairs)

k pairs cost k comparisons rather than n_query x n_target. Each neuron is still indexed once and the pairs are grouped by target, so a target whose whole column you request reproduces that column of nblast exactly — this is the same primitive Smart NBLAST uses for its second pass. Pass target=None to compare a set against itself without preparing it twice.

k nearest neighbours

nblast_knn returns each neuron's k nearest neighbours without ever building the score matrix — the usual reason being a UMAP embedding, which needs a k-NN graph rather than every pairwise score. See Concepts › NBLAST for why the shortlist it uses is sound rather than merely plausible.

# (n, 20) neighbour indices and their exact NBLAST scores
idx, scores = fastcore.nblast_knn(dotprops, k=20)

# straight into UMAP: it wants distances, not similarities
import umap
emb = umap.UMAP(precomputed_knn=(idx, 1.0 - scores)).fit_transform(
    np.zeros((len(dotprops), 2))
)

# query vs target: `idx` then indexes `target`
idx, scores = fastcore.nblast_knn(query_dps, target=target_dps, k=5)

Only which neurons make the shortlist is approximate; every returned score is an exact NBLAST value. On 163,976 real neurons at the default n_candidates=200, recall@20 was 0.99 while scoring 0.16% of the pairs — about 5 minutes against an estimated 35 hours, and 39 MB against 107 GB.

Beyond the shared options below, it takes:

  • k (default 20): neighbours per neuron. Rows with fewer available neighbours are padded with -1 in idx and -inf in scores.
  • target (default None): search among these instead of among dotprops, making idx index them. Note the saving is on pair scoring, so it grows with the size of the query set — for a handful of queries against a large library both paths pay the same target-indexing cost and plain nblast is simpler.
  • symmetry (default "mean"): unlike the matrix functions this defaults to symmetrising, because the combine has to happen before the top-k cut — once only k neighbours per row survive there is no transpose left to symmetrise against. See the concepts page.
  • n_candidates (default 200): shortlist size, and the one recall/cost knob. Measured recall@20 on 163,976 neurons: 0.91 at 50, 0.97 at 100, 0.99 at 200, 0.996 at 400.
  • voxel (default 20.0), n_dirs (default 3), splat (default True): signature resolution. 10–20 µm voxels measured equivalently; splat is worth about 0.05 recall@20.

Measuring recall against a ground truth

Compare neighbour sets, not positions. An order-sensitive (idx == truth).mean() counts a swap between two near-tied neighbours as two misses and will read several points lower than the set recall — on real data 0.957 against 0.988 for the same result. UMAP consumes the set and the distances, and is invariant to ordering within a row.

Smart NBLAST

nblast_smart is a two-pass approximation for large comparisons. It first runs a cheap "pre-NBLAST" on downsampled dotprops, then keeps only the best-scoring targets per query and recomputes those pairs at full resolution. Unselected cells keep their coarse pre-pass score. This mirrors navis' nblast_smart (its scores argument is spelled symmetry here, matching the other functions).

# Keep the top 10% of targets per query (percentile 90) for the full pass
scores = fastcore.nblast_smart(query_dps, target_dps, t=90, criterion="percentile")

# All-by-all; also return the boolean mask of cells recomputed at full resolution
scores, mask = fastcore.nblast_smart(dps, t=90, return_mask=True)

# Other selection criteria: an absolute score threshold, or a fixed number per query
scores = fastcore.nblast_smart(dps, t=0.3, criterion="score")
scores = fastcore.nblast_smart(dps, t=10, criterion="N")

Extra options beyond the shared ones below: t / criterion select the candidate targets ("percentile", "score" or "N"), downsample (default 10) sets the pre-pass point stride, and return_mask additionally returns which cells were recomputed. Because fastcore's dense NBLAST is already fast, nblast_smart pays off mainly for large all-by-all comparisons where the full-resolution scoring dominates; on small inputs the extra pre-pass can make it a wash.

syNBLAST (synapse-based)

synblast compares neurons by their synapses (connectors) instead of their skeleton points: for every query connector it finds the nearest target connector of the same type and scores that distance through the same lookup matrix with the dot product fixed at 1 (synapses carry no tangent vector). This mirrors navis' synblast (its scores argument is spelled symmetry here).

A "synapse cloud" is any object exposing connectors — an (N, 3) or (N, 4) array of [x, y, z, (type)], where the optional 4th column is a numeric connector type (e.g. 0 = presynapse, 1 = postsynapse). The Synapses namedtuple is a minimal container for one.

import navis_fastcore as fastcore
from navis_fastcore import Synapses

import numpy as np

# 5 neurons; each connector is [x, y, z, type] with type in {0, 1}
neurons = [
    Synapses(np.hstack([np.random.rand(200, 3) * 10, np.random.randint(0, 2, (200, 1))]))
    for _ in range(5)
]

# (5, 5) all-by-all matrix; diagonal = 1.0
scores = fastcore.synblast(neurons)

# Only compare like-typed synapses (pre-vs-pre, post-vs-post)
scores = fastcore.synblast(neurons, by_type=True)

# Query vs target, symmetric, restricted to presynapses
scores = fastcore.synblast(neurons[:2], neurons, symmetry="mean", cn_types=[0])

synblast shares smat, normalize, symmetry, n_cores, precision and progress with nblast (see below), plus two synapse-specific options: by_type (default False) restricts matches to same-type connectors, and cn_types keeps only connectors whose type is in the given set before scoring. It does not take use_alpha or limit_dist (neither applies to synapses).

Options

Both nblast_allbyall and nblast accept the same options:

  • smat: the scoring matrix. None (default) uses the embedded FCWB matrix. You may also pass a navis Lookup2d, or a (values, dist_edges, dot_edges) tuple where the edges are the ascending left bin boundaries.
  • normalize (default True): divide each score by the query's self-hit so a perfect self-match scores 1.0.
  • symmetry (default None): None / "forward" returns the raw forward (asymmetric) matrix; "mean", "min" or "max" combine it with the reverse direction (its transpose for nblast_allbyall, an explicit reverse NBLAST for the rectangular nblast). Symmetrising is done in place and allocates nothing — the numpy spelling (M + M.T) / 2 would build two full n x n temporaries, and even np.add(M, M.T, out=M) still builds one, since numpy sees the output overlapping M.T and defensively copies. At 100k neurons that is 80 GB of peak that no longer exists.
  • use_alpha (default False): weight each point's dot product by sqrt(alpha_query * alpha_target), emphasising locally linear (backbone) regions. Requires each dotprop to expose a per-point alpha. With no explicit smat, this auto-selects the alpha-calibrated FCWB matrix (as navis does); an explicit smat is used as given.
  • limit_dist (default None): a distance upper bound. A query point whose nearest neighbour is farther than this is scored at the matrix's "far + orthogonal" corner (aann prunes such searches). Pass a number, or "auto" for 1.05 × the last distance-bin edge (as in navis).
  • n_cores (default None): cap the number of worker threads. None uses all available cores.
  • precision (default 32): dtype of the returned matrix — 16, 32 or 64 (or "half" / "single" / "double"). The scoring math always runs in float64; precision only sets the storage width of the result.
  • progress (default False): show a progress bar over the scoring pairs (drawn from Rust to stderr; plain reprinted text under Jupyter).

API

NBLAST every query neuron against every target neuron.

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button; it stops promptly and raises KeyboardInterrupt.

PARAMETER DESCRIPTION
query
        Each must expose `points` (N, 3) and unit tangent `vect`
        (N, 3); also `alpha` (N,) when ``use_alpha`` is set.

TYPE: iterable of dotprop-likes

target
        Each must expose `points` (N, 3) and unit tangent `vect`
        (N, 3); also `alpha` (N,) when ``use_alpha`` is set.

TYPE: iterable of dotprop-likes

smat
        Scoring matrix. ``None`` uses the embedded FCWB matrix.

TYPE: None | navis Lookup2d | (values, dist_edges, dot_edges) DEFAULT: None

normalize
        Divide each score by the query's self-hit.

TYPE: bool DEFAULT: True

symmetry
        ``None`` / ``'forward'`` returns the raw forward matrix.
        The others combine it with the reverse (target-vs-query)
        NBLAST, matching navis' ``scores`` argument.

TYPE: None | 'forward' | 'mean' | 'min' | 'max' DEFAULT: None

use_alpha
        Weight each dot product by ``sqrt(alpha_query * alpha_target)``.

TYPE: bool DEFAULT: False

limit_dist
        Distance upper bound (see ``nblast_allbyall``).

TYPE: None | float | 'auto' DEFAULT: None

n_cores
        Cap the number of worker threads. ``None`` uses all cores.

TYPE: int | None DEFAULT: None

precision
        Dtype of the returned matrix. Math is always float64.

TYPE: 16 | 32 | 64 DEFAULT: 32

progress
        Show progress bars over index building and scoring.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
np.ndarray

(n_query, n_target) score matrix; row = query, column = target.

All-by-all NBLAST.

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button; it stops promptly and raises KeyboardInterrupt.

PARAMETER DESCRIPTION
dotprops
     Each must expose `points` (N, 3) and unit tangent `vect` (N, 3).
     When ``use_alpha`` is set, each must also expose `alpha` (N,).
     Coordinates are used at the dtype they arrive as: float32
     `points`/`vect` build a float32 spatial index and roughly halve
     peak memory. Anything else is taken as float64.

TYPE: iterable of dotprop-likes

smat
     Scoring matrix. ``None`` uses the embedded FCWB matrix.

TYPE: None | navis Lookup2d | (values, dist_edges, dot_edges) DEFAULT: None

normalize
     Divide each score by the query's self-hit (self-match == 1.0).

TYPE: bool DEFAULT: True

symmetry
     Combine the forward matrix with its transpose. ``None`` /
     ``'forward'`` returns the raw forward (asymmetric) matrix.

TYPE: None | 'forward' | 'mean' | 'min' | 'max' DEFAULT: None

use_alpha
     Weight each dot product by ``sqrt(alpha_query * alpha_target)``,
     emphasising locally linear (backbone) regions.

TYPE: bool DEFAULT: False

limit_dist
     Distance upper bound: a query point whose nearest neighbour is
     farther than this is scored at the "far + orthogonal" corner of
     the matrix. ``'auto'`` uses ``1.05 *`` the last distance bin edge.

TYPE: None | float | auto DEFAULT: None

n_cores
     Cap the number of worker threads. ``None`` uses all cores.

TYPE: int | None DEFAULT: None

precision
     Dtype of the returned matrix. The scoring math is always float64.

TYPE: 16 | 32 | 64 DEFAULT: 32

progress
     Show progress bars (drawn from Rust to stderr): first over
     building the neuron indices, then over the scoring pairs.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
np.ndarray

(n, n) score matrix; row = query, column = target.

NBLAST only the given (query, target) pairs.

The sparse counterpart to :func:~navis_fastcore.nblast: scoring k pairs costs k comparisons rather than the full n_query x n_target grid, and the result is a k-vector rather than a matrix. Use it when you already know which comparisons you want — a candidate list from a cheaper filter, the non-zero cells of a connectivity matrix, or a set of putative matches to verify.

Each neuron is still indexed once and pairs are grouped by target, so a target whose whole query column is requested reproduces that column of :func:~navis_fastcore.nblast exactly.

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button; it stops promptly and raises KeyboardInterrupt.

PARAMETER DESCRIPTION
query
        Each must expose `points` (N, 3) and unit tangent `vect`
        (N, 3); also `alpha` (N,) when ``use_alpha`` is set. Pass
        ``target=None`` to compare `query` against itself without
        preparing it twice.

TYPE: iterable of dotprop-likes

target
        Each must expose `points` (N, 3) and unit tangent `vect`
        (N, 3); also `alpha` (N,) when ``use_alpha`` is set. Pass
        ``target=None`` to compare `query` against itself without
        preparing it twice.

TYPE: iterable of dotprop-likes

pairs
        One ``(query index, target index)`` per row — **positions** in
        `query` and `target`, not IDs. Duplicates are allowed and each
        is scored once per row.

TYPE: (k, 2) int array

smat
        Scoring matrix. ``None`` uses the embedded FCWB matrix.

TYPE: None | navis Lookup2d | (values, dist_edges, dot_edges) DEFAULT: None

normalize
        Divide each score by the query's self-hit.

TYPE: bool DEFAULT: True

symmetry
        ``None`` / ``'forward'`` returns the raw forward score. The
        others combine it with the reverse score of the *same* pair
        (target as query), which costs a second pass.

TYPE: None | 'forward' | 'mean' | 'min' | 'max' DEFAULT: None

use_alpha
        Weight each dot product by ``sqrt(alpha_query * alpha_target)``.

TYPE: bool DEFAULT: False

limit_dist
        Distance upper bound (see ``nblast_allbyall``).

TYPE: None | float | 'auto' DEFAULT: None

n_cores
        Cap the number of worker threads. ``None`` uses all cores.

TYPE: int | None DEFAULT: None

precision
        Dtype of the returned scores. Math is always float64.

TYPE: 16 | 32 | 64 DEFAULT: 32

progress
        Show progress bars over index building and scoring.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
scores

One score per row of pairs, in the order given.

TYPE: (k, ) array

Examples:

>>> pairs = np.array([(0, 5), (0, 7), (3, 5)])
>>> scores = nblast_pairs(queries, library, pairs)

Score selected pairs within one set, without preparing it twice:

>>> scores = nblast_pairs(neurons, None, pairs)

The k nearest neighbours of every neuron, without the n x n matrix.

An all-by-all is the wrong shape for a k-NN question at scale: 164k neurons is 2.7e10 pairs and a 107 GB matrix, when the answer wanted from it is a 26 MB k-NN graph (typically to feed a UMAP embedding). This computes that graph directly, in three stages:

  1. each neuron becomes a coarse voxel-occupancy signature;
  2. the n_candidates most similar neurons per row are shortlisted from those signatures;
  3. the exact NBLAST score is computed for the shortlisted pairs only.

The pre-filter is sound because the FCWB scoring matrix has finite support — beyond its last distance bin (40 um) every cell is ~ -10, so neurons that do not overlap in space score at that floor regardless of shape. Only which neurons make the shortlist is approximate; every returned score is an exact NBLAST value, because a neuron that belongs in the true top-k outranks the global k-th and so cannot be dropped by the rerank once shortlisted.

Measured on 163,976 zebrafish neurons: recall@20 = 0.990 at the default n_candidates=200, scoring 0.16% of pairs.

.. note::

Scores agree with :func:`~navis_fastcore.nblast` but are not guaranteed
bit-identical to it. Where a query point is *exactly* equidistant from two
target points the nearest neighbour is ambiguous, and the two functions
can pick different (equally valid) tied tangents, shifting the score in
its last digits. ``aann-graph >= 0.2.1`` resolves such ties to the lowest
vertex index, which makes the two agree in all but a rare case where the
tied points are not adjacent in the neighbourhood graph. Ties are common
on real neurons, whose coordinates sit on a quantised grid, and
essentially absent on continuous synthetic data. Compare the two with a
small tolerance rather than for exact equality.

.. note::

Coordinates are held at the dtype they arrive as, so passing float32
``points`` / ``vect`` builds a float32 spatial index and roughly halves peak
memory — worth doing at this scale, where every neuron's index is alive at
once and they dominate the resident set. Scores shift by ~1e-4 relative
(float32 resolves ~1e-4 at millimetre-scale coordinates, which is four
orders below the finest distance bin, but it can flip a near-tied nearest
neighbour). Both sides of a query/target call must agree; a mix runs at
float64. Do not narrow if you need to reproduce an earlier float64 run.

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button.

PARAMETER DESCRIPTION
dotprops
       The queries. Each must expose `points` (N, 3) and unit tangent
       `vect` (N, 3); also `alpha` (N,) when ``use_alpha`` is set.
       float32 arrays select the float32 index (see the note above).

TYPE: iterable of dotprop-likes

target
       If given, neighbours are searched among these instead of among
       `dotprops`, and the returned `idx` indexes **`target`** — the
       k-NN counterpart of ``nblast(query, target)``. Two things
       differ from the ``None`` (all-by-all) case: nothing is excluded
       from a row, so a neuron appearing in both sets matches itself
       at 1.0; and the candidate shortlist is not symmetrically closed
       (there is no "target proposes the query" side to close over),
       which makes a given `n_candidates` slightly less generous than
       in the all-by-all case — raise it by ~40% to match.

TYPE: iterable of dotprop-likes | None DEFAULT: None

k
       Neighbours to return per neuron.

TYPE: int DEFAULT: 20

symmetry
       How the two directions of a pair are combined **before** the
       top-`k` cut. This matters more here than for a full matrix:
       with a matrix you can symmetrise afterwards against the
       transpose, but once only `k` neighbours per row are kept the
       transpose is gone. The asymmetry is real — a small neuron
       contained in a large one scores high one way and low the other
       — so ``'mean'`` is the default. ``None`` / ``'forward'`` keeps
       each row's own forward score.

TYPE: 'mean' | 'forward' | 'min' | 'max' | None DEFAULT: 'mean'

n_candidates
       Shortlist size per neuron; the one recall/cost knob. Measured
       recall@20 on 163,976 real neurons: 0.911 at 50, 0.969 at 100,
       0.990 at 200, 0.996 at 400. The budget needed for a given
       recall grows only about logarithmically with the number of
       neurons.

TYPE: int DEFAULT: 200

voxel
       Signature voxel edge, in the units of `points` (um for the
       FCWB matrix). 10-20 measured equivalently.

TYPE: float DEFAULT: 20.0

n_dirs
       Tangent-direction bins for the signature; 1 disables them.

TYPE: int DEFAULT: 3

splat
       Trilinearly spread each point over its 8 surrounding voxels
       (worth ~0.05 recall@20).

TYPE: bool DEFAULT: True

smat
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

normalize
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

use_alpha
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

limit_dist
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

n_cores
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

precision
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

progress
       As in `nblast` / `nblast_allbyall`.

DEFAULT: None

RETURNS DESCRIPTION
idx

(n_query, k) int64 neighbour indices, descending by score — into target if given, else into dotprops. Rows with fewer than k candidates are padded with -1.

TYPE: np.ndarray

scores

(n_query, k) NBLAST similarities aligned to idx; padding is -inf. For UMAP's precomputed_knn pass 1.0 - scores as the distances.

TYPE: np.ndarray

Examples:

>>> idx, scores = nblast_knn(dotprops, k=20)
>>> reducer = umap.UMAP(precomputed_knn=(idx, 1.0 - scores))

Search a small query set against a large reference set:

>>> idx, scores = nblast_knn(queries, target=library, k=5)

Smart(er) NBLAST: a fast two-pass approximation of an all-by-all / query-target.

A cheap "pre-NBLAST" is run on downsampled dotprops; for each query the best-scoring targets are then kept (per criterion) and the full-resolution NBLAST is computed only for those query-target pairs. Unselected cells keep their coarse pre-pass score. This matches navis' nblast_smart (its scores argument is spelled symmetry here, as in nblast/nblast_allbyall).

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button (during either pass); it stops promptly and raises KeyboardInterrupt.

PARAMETER DESCRIPTION
query
     Each must expose `points` (N, 3) and unit tangent `vect` (N, 3);
     also `alpha` (N,) when ``use_alpha`` is set.

TYPE: iterable of dotprop-likes

target
     Targets to compare against. ``None`` runs an all-by-all
     (``target = query``).

TYPE: iterable of dotprop-likes | None DEFAULT: None

t
     Threshold for ``criterion``: the percentile (default, ``90`` keeps
     the top 10% per query), an absolute score, or the number of targets.

TYPE: int | float DEFAULT: 90

criterion
     How ``t`` selects the candidate targets kept for the full pass.

TYPE: 'percentile' | 'score' | 'N' DEFAULT: 'percentile'

downsample
     Pre-pass keeps every ``downsample``-th point (default ``10``).

TYPE: int DEFAULT: 10

smat
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

normalize
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

symmetry
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

use_alpha
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

limit_dist
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

n_cores
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

precision
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

progress
     As in `nblast` / `nblast_allbyall`.

DEFAULT: None

return_mask
     If ``True``, also return the boolean mask of cells that were
     recomputed at full resolution.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
np.ndarray | (np.ndarray, np.ndarray)

The (n_query, n_target) score matrix, or (scores, mask) when return_mask is set.

Synapse-based NBLAST (syNBLAST).

Compares neurons by their synapses (connectors) rather than their skeleton points: for every query connector the nearest target connector of the same type is found, and the euclidean distance is scored through the NBLAST lookup matrix with the dot product fixed at 1 (synapses have no tangent vector). This matches navis' synblast (navis' scores argument is spelled symmetry here, as in nblast / nblast_allbyall).

A long run can be interrupted with Ctrl-C / the Jupyter interrupt button; it stops promptly and raises KeyboardInterrupt.

PARAMETER DESCRIPTION
query
     Each item exposes ``connectors`` (or is itself an array) of shape
     ``(N, 3)`` or ``(N, 4)`` — ``[x, y, z, (type)]``. The 4th column is
     a numeric connector type, required when ``by_type`` / ``cn_types``
     are set. See `Synapses`.

TYPE: iterable of synapse-bearing neurons

target
     Targets to compare against. ``None`` runs an all-by-all
     (``target = query``).

TYPE: iterable of synapse-bearing neurons | None DEFAULT: None

by_type
     If ``True``, only connectors of the same type compare against each
     other (navis' ``by_type``); requires a type column. Default
     ``False`` treats all connectors as one group.

TYPE: bool DEFAULT: False

cn_types
     If given, keep only connectors whose type is in this set before
     scoring (navis' ``cn_types``); requires a type column.

TYPE: iterable | None DEFAULT: None

smat
     Scoring matrix. ``None`` uses the embedded FCWB matrix (navis'
     ``smat="auto"``); only its last (aligned) dot-product column is
     used.

TYPE: None | navis Lookup2d | (values, dist_edges, dot_edges) DEFAULT: None

normalize
     Divide each score by the query's self-hit (self-match == 1.0).

TYPE: bool DEFAULT: True

symmetry
     ``None`` / ``'forward'`` returns the raw forward matrix; the others
     combine it with the reverse (target-vs-query) syNBLAST.

TYPE: None | 'forward' | 'mean' | 'min' | 'max' DEFAULT: None

n_cores
     Cap the number of worker threads. ``None`` uses all cores.

TYPE: int | None DEFAULT: None

precision
     Dtype of the returned matrix. The scoring math is always float64.

TYPE: 16 | 32 | 64 DEFAULT: 32

progress
     Show progress bars over index building and scoring.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
np.ndarray

(n_query, n_target) score matrix; row = query, column = target.

Extracting matches

Once you have a score matrix, pulling the best matches back out of it is its own problem — the matrices are big (a few 100k on a side is tens of GB), so the extraction must not copy or transpose them. Three criteria, all reading the matrix at its native width (float16, float32 or float64) and returning plain numpy arrays:

import numpy as np
import navis_fastcore as fastcore

scores = fastcore.nblast_allbyall(dps)          # (n, n) float32

# The 5 best targets per query, best first. `skip_self` drops the diagonal, which
# would otherwise be every neuron's own top hit.
indices, values = fastcore.top_matches(scores, 5, skip_self=True)

# Everything above an absolute cutoff, or within 5% of each query's own best match.
# Ragged, so returned CSR-style: query `q` owns indices[offsets[q]:offsets[q + 1]].
offsets, indices, values = fastcore.matches_above(scores, threshold=0.5)
offsets, indices, values = fastcore.matches_above(scores, percentage=0.05)

Expand the ragged result into a long-format table without a Python loop:

counts = np.diff(offsets)
query = np.repeat(np.arange(len(counts)), counts)
rank = np.arange(len(indices)) - np.repeat(offsets[:-1], counts)

Notes:

  • axis=1 gives matches per target instead of per query. It costs no more than axis=0 and does not transpose: the kernel walks column stripes of the row-major buffer, so the matrix is read exactly once either way. Passing a transposed view (scores.T) is likewise free.
  • The matrix is borrowed, never copied — including a np.memmap. Nothing here will cast your dtype behind your back, so a strided view or an unsupported dtype raises rather than silently materialising tens of GB.
  • distances=True if lower is better.
  • NaN is never a match. A query with no valid scores yields -1/NaN slots (top_matches) or an empty group (matches_above).
  • Ties break toward the lower index, so results don't depend on the thread count.
  • matches_above counts before it allocates, so max_matches can refuse an over-broad cutoff instead of exhausting the machine. Use count_matches to size a result first.

Extract the n best matches for each query (or target).

The matrix is neither copied nor transposed, whichever axis you ask for - matches along axis=1 are served by striding the row-major buffer in column blocks, so it is read exactly once either way. That matters: these matrices run to tens of GB.

PARAMETER DESCRIPTION
scores
        Score matrix, float16/32/64. Must be C- or F-contiguous. Not copied.

TYPE: (n_query, n_target) np.ndarray

n
        How many matches to extract per group. Must be <= the length of the
        scanned axis.

TYPE: int

axis
        0 = one row of matches per query (per row of `scores`); 1 = per target.

TYPE: 0 | 1 DEFAULT: 0

distances
        If True, *lower* is better (a distance matrix rather than similarity).

TYPE: bool DEFAULT: False

skip_self
        Exclude each group's self-match. True uses the diagonal (requires a
        square matrix); an array gives the index to skip per group, -1 for none.

TYPE: bool | (n_groups,) array DEFAULT: False

n_cores
        Cap the worker count. Default: all cores.

TYPE: int DEFAULT: None

progress

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
indices

Index along the other axis, best match first. -1 where the group had fewer than n valid (non-NaN) cells.

TYPE: (n_groups, n) int64

values

The matching scores, in the dtype of scores. NaN where the paired index is -1.

TYPE: (n_groups, n)

Examples:

>>> import numpy as np
>>> import navis_fastcore as fastcore
>>> scores = np.array([[1.0, 0.2, 0.9],
...                    [0.2, 1.0, 0.4],
...                    [0.9, 0.4, 1.0]], dtype=np.float32)
>>> idx, val = fastcore.top_matches(scores, 2)
>>> idx
array([[0, 2],
       [1, 2],
       [2, 0]])

Ignore the self-hits on the diagonal:

>>> idx, val = fastcore.top_matches(scores, 1, skip_self=True)
>>> idx.ravel()
array([2, 2, 0])
>>> val.ravel()
array([0.9, 0.4, 0.9], dtype=float32)

Extract every match clearing a cutoff. Ragged, so returned CSR-style.

Give exactly one of:

  • threshold - an absolute cutoff: keep every cell >= threshold (<= if distances).
  • percentage - a band around each group's own best value: percentage=0.05 keeps everything within 5% of that group's top match. Note this is "within X% of the best", not "the top X%".
PARAMETER DESCRIPTION
scores
        Score matrix, float16/32/64. Must be C- or F-contiguous. Not copied.

TYPE: (n_query, n_target) np.ndarray

threshold

TYPE: float DEFAULT: None

percentage
        In [0, 1].

TYPE: float DEFAULT: None

axis

TYPE: 0 | 1 DEFAULT: 0

distances
        If True, *lower* is better.

TYPE: bool DEFAULT: False

skip_self
        See [`top_matches`][navis_fastcore.top_matches].

TYPE: bool | (n_groups,) array DEFAULT: False

max_matches
        Refuse to allocate more than this many matches. The count is known
        before anything is allocated, so an over-broad cutoff raises instead of
        taking the machine down with it. See
        [`count_matches`][navis_fastcore.count_matches] to size a result first.

TYPE: int DEFAULT: None

n_cores

TYPE: int DEFAULT: None

progress

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
offsets

Group g's matches are indices[offsets[g]:offsets[g + 1]].

TYPE: (n_groups + 1,) int64

indices

Index along the other axis, best first within each group. Same quantity, and same dtype, as top_matches' indices.

TYPE: (total,) int64

values

The matching scores, in the dtype of scores.

TYPE: (total,)

Examples:

>>> import numpy as np
>>> import navis_fastcore as fastcore
>>> scores = np.array([[1.0, 0.2, 0.9],
...                    [0.2, 1.0, 0.4],
...                    [0.9, 0.4, 1.0]], dtype=np.float32)
>>> offsets, indices, values = fastcore.matches_above(scores, threshold=0.5)
>>> offsets
array([0, 2, 3, 5])
>>> indices[offsets[0]:offsets[1]]   # query 0's matches, best first
array([0, 2])

Expand to a long-format table without a Python loop:

>>> counts = np.diff(offsets)
>>> query = np.repeat(np.arange(len(counts)), counts)
>>> rank = np.arange(len(indices)) - np.repeat(offsets[:-1], counts)
>>> query
array([0, 0, 1, 2, 2])
>>> rank
array([0, 1, 0, 0, 1])

Count the matches each group would yield, without materialising them.

The counting half of matches_above on its own. Use it to size a result - or to pick a cutoff - on a matrix you cannot afford to guess wrong about.

PARAMETER DESCRIPTION
scores
        Score matrix, float16/32/64. Must be C- or F-contiguous. Not copied.

TYPE: (n_query, n_target) np.ndarray

threshold

TYPE: float DEFAULT: None

percentage
        Exactly one of the two, as for `matches_above`.

TYPE: float DEFAULT: None

axis

TYPE: 0 | 1 DEFAULT: 0

distances

TYPE: bool DEFAULT: False

skip_self

TYPE: bool | (n_groups,) array DEFAULT: False

n_cores

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
counts

TYPE: (n_groups,) int64

Examples:

>>> import numpy as np
>>> import navis_fastcore as fastcore
>>> scores = np.array([[1.0, 0.2, 0.9],
...                    [0.2, 1.0, 0.4],
...                    [0.9, 0.4, 1.0]], dtype=np.float32)
>>> fastcore.count_matches(scores, threshold=0.5)
array([2, 1, 2])

Clustering

The other thing you do with a score matrix is cluster it. The textbook route — symmetrise, convert similarity to distance, condense, then scipy.cluster.hierarchy.linkage — allocates another n x n array at almost every step, and at 100k neurons that, not the clustering, is what exhausts the machine:

# What this replaces. Each line materialises a fresh n x n array.
m = (scores + scores.T) / 2
m = 1 - m
Z = linkage(squareform(m, checks=False), method="ward")

fastcore.linkage fuses the first three steps into a single pass that writes the condensed distance vector directly, then clusters that buffer in place:

import navis_fastcore as fastcore
from scipy.cluster.hierarchy import fcluster, dendrogram

scores = fastcore.nblast_allbyall(dps)          # (n, n) float32

# Symmetrise, 1 - score, condense and cluster - no n x n temporary anywhere.
Z = fastcore.linkage(scores, method="ward")

# Z is a SciPy linkage matrix, so the rest of the ecosystem just works.
labels = fcluster(Z, 10, criterion="maxclust")

The condensed distances are available on their own if you want them:

cond = fastcore.condensed_distances(scores, symmetry="mean", transform="one_minus")
Z = fastcore.linkage(cond, method="average", copy=False)   # clusters in place

Notes:

  • float32 stays float32. scipy.cluster.hierarchy.linkage up-casts its input to float64 unconditionally, so handing it a float32 matrix to save memory instead costs you a second, doubled copy of the condensed matrix — plus a bool temporary the size of the input for its finiteness check. Here the condensed matrix is the only allocation, and the finiteness check rides along on the fused pass for free.
  • Measured on a 14-core machine at n = 40,000, method="ward", float32 input: 38.2 s / 12.5 GB peak for the numpy+SciPy pipeline versus 10.8 s / 9.6 GB here. The output is the same dendrogram.
  • Z matches SciPy's layout exactly — (n-1, 4), float64, singletons labelled 0..n and the cluster formed at step i labelled n + i, rows ordered by increasing distance — so fcluster, dendrogram and cut_tree all take it directly.
  • symmetry mirrors nblast_allbyall's: use "none" if the matrix is already symmetric, which is also the fastest path since it reads the buffer strictly sequentially.
  • Clustering consumes its input as scratch. From a square matrix that never matters (the buffer is its own); from a condensed vector, copy=False clusters in place and halves peak memory at the cost of your array.
  • The linkage itself is single-threaded and cannot be interrupted — Ctrl-C is honoured up to the end of the condensing pass only.

leaf_order is SciPy's leaves_list, for callers who would rather not add scipy just to draw the thing:

order = fastcore.leaf_order(Z)      # (n,) observation indices, left to right
labels[order]                       # your labels, in drawing order

symmetrize is the standalone version of the combine linkage and condensed_distances fold into their own pass. You do not need it before either of them — reach for it when something else has to read the matrix, and note that it rewrites your array in place rather than handing back a copy:

fastcore.symmetrize(scores, symmetry="mean")   # no n x n temporary

Hierarchical clustering of a score matrix (e.g. NBLAST output).

Turn a square score matrix into a condensed distance vector.

Symmetrisation, the similarity-to-distance transform and the condensing all happen in a single fused pass, so the only allocation is the n(n-1)/2 output itself. The numpy equivalent::

cond = squareform(1 - (M + M.T) / 2, checks=False)

materialises three more n x n arrays on the way, which at 100k neurons is where the memory actually goes.

The diagonal is never read, so a matrix carrying self-scores rather than zeros needs no fixing up first.

PARAMETER DESCRIPTION
scores
    Score matrix, typically from :func:`~navis_fastcore.nblast_allbyall`.
    Must be C- or F-contiguous; it is borrowed, never copied or cast.

TYPE: (n, n) float32 or float64 array

symmetry
    How to combine ``M[i, j]`` with ``M[j, i]``, since NBLAST is not
    symmetric. Mirrors the ``symmetry`` argument of
    :func:`~navis_fastcore.nblast_allbyall`. Use ``"none"`` when the
    matrix is already symmetric — it is also the fastest, since it reads
    the buffer strictly sequentially.

TYPE: "mean" | "min" | "max" | "none" DEFAULT: 'mean'

transform
    ``"one_minus"`` gives ``1 - score``, the usual NBLAST convention;
    ``"none"`` passes values through as distances unchanged.

TYPE: one_minus | none DEFAULT: 'one_minus'

n_cores
    Thread cap. ``None`` uses all available cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
condensed

Upper triangle in row-major order, i.e. the layout scipy.spatial.distance.squareform produces. Same dtype as scores.

TYPE: (n * (n - 1) / 2, ) array

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.00, 0.75],
...                    [0.25, 1.00]], dtype=np.float32)
>>> fastcore.condensed_distances(scores)
array([0.5], dtype=float32)

Already-symmetric distances can be passed straight through:

>>> d = np.array([[0.0, 0.25],
...               [0.25, 0.0]], dtype=np.float32)
>>> fastcore.condensed_distances(d, symmetry="none", transform="none")
array([0.25], dtype=float32)

The order to place the leaves in so a dendrogram draws without crossings.

The equivalent of scipy.cluster.hierarchy.leaves_list, for callers who have no scipy: a depth-first walk of the merge tree from the root, emitting each observation as it is reached. Iterative, so a 200k-observation chain does not need 200k stack frames.

PARAMETER DESCRIPTION
Z
    A linkage matrix, from :func:`~navis_fastcore.linkage` or from
    SciPy. Only the first two columns are read.

TYPE: (n - 1, 4) array

RETURNS DESCRIPTION
order

Observation indices, left to right. A permutation of 0..n, so it indexes the observations you clustered: labels[order] puts your labels in drawing order.

TYPE: (n, ) int64 array

Notes

Any consumer that draws the tree must use the same child order as the linkage matrix it came from, which is why this reads Z rather than a pre-built tree.

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.00, 0.90, 0.20, 0.10],
...                    [0.70, 1.00, 0.30, 0.15],
...                    [0.25, 0.35, 1.00, 0.80],
...                    [0.05, 0.10, 0.60, 1.00]], dtype=np.float32)
>>> Z = fastcore.linkage(scores, method="average")
>>> fastcore.leaf_order(Z)
array([0, 1, 2, 3])

Hierarchical clustering, returning a SciPy-compatible linkage matrix.

Accepts either a square score matrix — in which case the whole pipeline (symmetrise, convert to distance, condense, cluster) runs fused, with no n x n temporary ever materialised — or an existing condensed distance vector.

The result is interchangeable with scipy.cluster.hierarchy.linkage output and can be handed straight to fcluster, dendrogram or cut_tree.

Two things make this cheaper than the SciPy pipeline at scale:

  • float32 stays float32. scipy.cluster.hierarchy.linkage up-casts its input to float64 unconditionally, so handing it a float32 matrix to save memory instead costs you a second, doubled copy of it.
  • Nothing is copied. The condensed matrix is clustered in place.
PARAMETER DESCRIPTION
x
    Either a square score matrix (``symmetry`` and ``transform`` apply)
    or a 1-D condensed distance vector (they are ignored).

TYPE: (n, n) or (n * (n - 1) / 2, ) float32 or float64 array

method
    Linkage method: "single", "complete", "average", "weighted", "ward",
    "centroid" or "median". Same meanings as in SciPy.

TYPE: str DEFAULT: 'ward'

symmetry
    How to combine ``M[i, j]`` with ``M[j, i]``. Square input only.

TYPE: "mean" | "min" | "max" | "none" DEFAULT: 'mean'

transform
    ``"one_minus"`` gives ``1 - score``. Square input only.

TYPE: one_minus | none DEFAULT: 'one_minus'

copy
    Condensed input only. Linkage consumes its input as scratch; with
    ``copy=True`` (the default) a copy is taken so yours survives. Pass
    ``False`` to cluster in place and halve peak memory — your array is
    left in an arbitrary state afterwards. Square input never copies,
    because the condensed buffer it builds is its own.

TYPE: bool DEFAULT: True

n_cores
    Thread cap for the condensing / validation passes. The linkage
    itself is single-threaded.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
Z

One merge per row as [cluster1, cluster2, distance, size], ordered by increasing distance. Singletons are labelled 0..n and the cluster formed at step i is labelled n + i.

TYPE: (n - 1, 4) float64 array

Notes

Clustering cannot be interrupted once it starts: it exposes no per-merge hook, so Ctrl-C is only honoured up to the end of the condensing pass. At 100k observations the linkage is the part that takes minutes.

On float32 input the merge heights match a float64 run to about 1e-7 relative. Where distances are very nearly tied, that rounding can also swap the order of two otherwise-equivalent merges; on structured data (what a real NBLAST matrix looks like) this does not happen, but on near-degenerate input it affects a small fraction of merges. Pass float64 if you need merge order to be reproducible against SciPy down to the last tie.

Examples:

Two obvious pairs, from a score matrix:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.00, 0.90, 0.20, 0.10],
...                    [0.70, 1.00, 0.30, 0.15],
...                    [0.25, 0.35, 1.00, 0.80],
...                    [0.05, 0.10, 0.60, 1.00]], dtype=np.float32)
>>> Z = fastcore.linkage(scores, method="average")
>>> Z[:, :2].astype(int)
array([[0, 1],
       [2, 3],
       [4, 5]])

Cut it into two clusters, using SciPy as usual:

>>> from scipy.cluster.hierarchy import fcluster
>>> fcluster(Z, 2, criterion="maxclust")
array([1, 1, 2, 2], dtype=int32)

From an existing condensed distance vector:

>>> cond = fastcore.condensed_distances(scores)
>>> Z = fastcore.linkage(cond, method="average")
>>> Z.shape
(3, 4)

Symmetrise a square score matrix against its own transpose, in place.

This is the case numpy cannot do cheaply. (M + M.T) / 2 builds two full n x n temporaries, and even np.add(M, M.T, out=M) still builds one, because numpy sees the output overlapping M.T and defensively copies the input first. The kernel here writes both triangles as it walks the upper one, so it allocates nothing — at 100k neurons that is 80 GB of peak that stops existing.

You do not need this before :func:~navis_fastcore.linkage or :func:~navis_fastcore.condensed_distances: both take a symmetry argument and fold the same combine into their own pass. Use it when something else is going to read the matrix — plotting it, writing it out, or feeding it to a library that assumes symmetry.

PARAMETER DESCRIPTION
scores
    Score matrix, modified in place. Must be C- or F-contiguous and
    writeable; it is borrowed, never copied or cast.

TYPE: (n, n) float32 or float64 array

symmetry
    How to combine ``M[i, j]`` with ``M[j, i]``. ``"none"`` mirrors the
    upper triangle onto the lower instead of combining.

TYPE: "mean" | "min" | "max" | "none" DEFAULT: 'mean'

n_cores
    Thread cap. ``None`` uses all available cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
scores

original name refers to the symmetrised matrix either way.

TYPE: the same array, for convenience. It was modified in place, so the

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.0, 0.8],
...                    [0.4, 1.0]], dtype=np.float32)
>>> fastcore.symmetrize(scores)
array([[1. , 0.6],
       [0.6, 1. ]], dtype=float32)

Turn a square score matrix into a condensed distance vector.

Symmetrisation, the similarity-to-distance transform and the condensing all happen in a single fused pass, so the only allocation is the n(n-1)/2 output itself. The numpy equivalent::

cond = squareform(1 - (M + M.T) / 2, checks=False)

materialises three more n x n arrays on the way, which at 100k neurons is where the memory actually goes.

The diagonal is never read, so a matrix carrying self-scores rather than zeros needs no fixing up first.

PARAMETER DESCRIPTION
scores
    Score matrix, typically from :func:`~navis_fastcore.nblast_allbyall`.
    Must be C- or F-contiguous; it is borrowed, never copied or cast.

TYPE: (n, n) float32 or float64 array

symmetry
    How to combine ``M[i, j]`` with ``M[j, i]``, since NBLAST is not
    symmetric. Mirrors the ``symmetry`` argument of
    :func:`~navis_fastcore.nblast_allbyall`. Use ``"none"`` when the
    matrix is already symmetric — it is also the fastest, since it reads
    the buffer strictly sequentially.

TYPE: "mean" | "min" | "max" | "none" DEFAULT: 'mean'

transform
    ``"one_minus"`` gives ``1 - score``, the usual NBLAST convention;
    ``"none"`` passes values through as distances unchanged.

TYPE: one_minus | none DEFAULT: 'one_minus'

n_cores
    Thread cap. ``None`` uses all available cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
condensed

Upper triangle in row-major order, i.e. the layout scipy.spatial.distance.squareform produces. Same dtype as scores.

TYPE: (n * (n - 1) / 2, ) array

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.00, 0.75],
...                    [0.25, 1.00]], dtype=np.float32)
>>> fastcore.condensed_distances(scores)
array([0.5], dtype=float32)

Already-symmetric distances can be passed straight through:

>>> d = np.array([[0.0, 0.25],
...               [0.25, 0.0]], dtype=np.float32)
>>> fastcore.condensed_distances(d, symmetry="none", transform="none")
array([0.25], dtype=float32)

The order to place the leaves in so a dendrogram draws without crossings.

The equivalent of scipy.cluster.hierarchy.leaves_list, for callers who have no scipy: a depth-first walk of the merge tree from the root, emitting each observation as it is reached. Iterative, so a 200k-observation chain does not need 200k stack frames.

PARAMETER DESCRIPTION
Z
    A linkage matrix, from :func:`~navis_fastcore.linkage` or from
    SciPy. Only the first two columns are read.

TYPE: (n - 1, 4) array

RETURNS DESCRIPTION
order

Observation indices, left to right. A permutation of 0..n, so it indexes the observations you clustered: labels[order] puts your labels in drawing order.

TYPE: (n, ) int64 array

Notes

Any consumer that draws the tree must use the same child order as the linkage matrix it came from, which is why this reads Z rather than a pre-built tree.

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.00, 0.90, 0.20, 0.10],
...                    [0.70, 1.00, 0.30, 0.15],
...                    [0.25, 0.35, 1.00, 0.80],
...                    [0.05, 0.10, 0.60, 1.00]], dtype=np.float32)
>>> Z = fastcore.linkage(scores, method="average")
>>> fastcore.leaf_order(Z)
array([0, 1, 2, 3])

Symmetrise a square score matrix against its own transpose, in place.

This is the case numpy cannot do cheaply. (M + M.T) / 2 builds two full n x n temporaries, and even np.add(M, M.T, out=M) still builds one, because numpy sees the output overlapping M.T and defensively copies the input first. The kernel here writes both triangles as it walks the upper one, so it allocates nothing — at 100k neurons that is 80 GB of peak that stops existing.

You do not need this before :func:~navis_fastcore.linkage or :func:~navis_fastcore.condensed_distances: both take a symmetry argument and fold the same combine into their own pass. Use it when something else is going to read the matrix — plotting it, writing it out, or feeding it to a library that assumes symmetry.

PARAMETER DESCRIPTION
scores
    Score matrix, modified in place. Must be C- or F-contiguous and
    writeable; it is borrowed, never copied or cast.

TYPE: (n, n) float32 or float64 array

symmetry
    How to combine ``M[i, j]`` with ``M[j, i]``. ``"none"`` mirrors the
    upper triangle onto the lower instead of combining.

TYPE: "mean" | "min" | "max" | "none" DEFAULT: 'mean'

n_cores
    Thread cap. ``None`` uses all available cores.

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
scores

original name refers to the symmetrised matrix either way.

TYPE: the same array, for convenience. It was modified in place, so the

Examples:

>>> import navis_fastcore as fastcore
>>> import numpy as np
>>> scores = np.array([[1.0, 0.8],
...                    [0.4, 1.0]], dtype=np.float32)
>>> fastcore.symmetrize(scores)
array([[1. , 0.6],
       [0.6, 1. ]], dtype=float32)