cdist and its pruned variants compute every (query × target)
score in one SIMD batch with the GIL released and per-row work
parallelised across threads. Use these when you have many queries
and many targets.
The four entry points differ in what they return — full matrix,
above-threshold pairs, top-k matches, or top-k per query. All
share the same scorer= argument and pruning machinery.
Output-processing variants
| Function | Returns | Use when |
|---|---|---|
cdist(queries, targets, *, scorer, …) |
ndarray shape (Q, T) |
you need the full score matrix |
cdist_above_threshold(queries, targets, *, scorer, threshold, …) |
iterator of (qi, ti, score) |
you only care about pairs above a similarity floor |
cdist_top_k(queries, targets, *, scorer, k, …) |
iterator of (qi, ti, score) of the global top-k matches |
global k-nearest-neighbours across the whole grid |
cdist_top_k_per_query(queries, targets, *, scorer, k, …) |
per-query top-k, list[list[(ti, score)]] |
every query gets its own top-k neighbour list |
cdist_top_k_per_query accepts cpu_count= (default 0 =
auto-detect, capped sensibly; 1 = single-threaded). Per-query
rows run on a worker pool sharing one byte-snapshot of the target
list under the released GIL.
The Scorer enum
scorer= accepts a stride_align.Scorer enum value (cheapest;
dispatches through the C++ batch kernel) or any Python callable
that takes two sequences and returns a score (slower fallback).
| Value | Algorithm |
|---|---|
Scorer.LEVENSHTEIN / LEVENSHTEIN_NORMALIZED |
edit distance |
Scorer.DAMERAU_LEVENSHTEIN / ..._NORMALIZED |
OSA |
Scorer.HAMMING / HAMMING_NORMALIZED |
equal-length only |
Scorer.JARO / JARO_WINKLER |
similarity |
Scorer.INDEL / INDEL_NORMALIZED |
insert + delete only |
Scorer.TRUE_DAMERAU_LEVENSHTEIN / ..._NORMALIZED |
unrestricted DL |
Scorer.SMITH_WATERMAN / ..._NORMALIZED |
local alignment |
Scorer.NEEDLEMAN_WUNSCH / ..._NORMALIZED |
global alignment |
SMITH_WATERMAN / NEEDLEMAN_WUNSCH row-loop through the SIMD
single-query kernels (they accept the same match_score /
mismatch_score / matrix / gap_* kwargs). Threading via
cpu_count still parallelises rows because the per-row kernels
release the GIL.
Pruning
All four entry points share three correctness-preserving optimisations:
- Length-difference pruning. Each
(q, t)pair is gated by a closed-form upper bound on achievable normalised similarity before any SIMD work runs. Bounds:min/maxfor Levenshtein / OSA / true-DL,(2 + min/max)/3for Jaro,2 * min / (q + t)for Indel,1.0if equal-length for Hamming. - Row-sort by query length, descending.
cdist_top_kprocesses the longest queries first so close-length high-scoring pairs surface early and the sharedglobal_min_boundatomic reaches a useful value before the short-query rows run. - Per-pair cutoff push-down into the SIMD kernel. Levenshtein
/ OSA / Hamming inner loops bail per lane once the running
score exceeds the per-pair cutoff plus the remaining-chars
allowance; bailed lanes return the
cutoff + 1sentinel.
The combination yields >100x speedup at threshold=0.99 on
random-ASCII benchmarks against an unpruned cdist — see
BENCHMARK for the cross-arch numbers.
Examples
import stride_align as sa
# Full matrix
M = sa.cdist(queries, targets, scorer=sa.Scorer.LEVENSHTEIN_NORMALIZED)
# M.shape == (len(queries), len(targets)), dtype float64
# Threshold filter — get every pair within 90% Jaro-Winkler similarity
for qi, ti, score in sa.cdist_above_threshold(
queries, targets,
scorer=sa.Scorer.JARO_WINKLER, threshold=0.9,
):
...
# Top-100 most-similar pairs across the whole grid
matches = list(sa.cdist_top_k(
queries, targets,
scorer=sa.Scorer.INDEL_NORMALIZED, k=100,
))
# Per-query 5-nearest-neighbours, 8-way parallel
nn = sa.cdist_top_k_per_query(
queries, targets,
scorer=sa.Scorer.JARO_WINKLER, k=5, cpu_count=8,
)
# nn[i] is a list of up to 5 (target_index, score) tuples for queries[i]
Choosing the right entry point
| You want… | Use |
|---|---|
| every pair's score | cdist |
| only pairs above a similarity floor | cdist_above_threshold |
| the global k most-similar pairs | cdist_top_k |
| each query's own k nearest neighbours | cdist_top_k_per_query |
Single-query workloads (one query × many targets) should use the
per-algorithm _scores / _best / _normalized_* / _top_k
entry points instead — they skip the cross-product setup. See
edit-distance and
similarity.
