Skip to content

Releases: adamdeprince/stride-align

v0.5.0

Choose a tag to compare

@adamdeprince adamdeprince released this 10 Jun 03:45

rapidfuzz shim full-surface competitive across architectures. See CHANGELOG.md [0.5.0] for details. LoongArch wheels (old-world + new-world) attached below; x86_64 / aarch64 / macOS arm64 wheels on PyPI.

v0.4.0

Choose a tag to compare

@adamdeprince adamdeprince released this 31 May 22:49

Added

  • Phonetic encoders (Phase D.2). Full standard family with the
    same byte-extraction + variant pattern as the rest of the library:

    • Soundex (Russell & Odell, 1918). soundex, soundex_equal.
    • Metaphone (Lawrence Philips, 1990) with MetaphoneVariant
      enum — PHILIPS (spec-correct, Apache Commons Codec branch)
      and JELLYFISH (matches the popular Python library). metaphone,
      metaphone_equal.
    • Double Metaphone (Lawrence Philips, 2000) with
      DoubleMetaphoneVariant enum — COMMONS (faithful Apache
      Commons Codec / doublemetaphone PyPI port) and PYTHON
      (bug-compat with the metaphone PyPI package's missing-else GH
      leak). Returns (primary, alternate). double_metaphone.
    • NYSIIS (Taft, 1970). nysiis, nysiis_equal.
    • Match Rating Approach (Moore, Western Airlines, 1977).
      match_rating_codex, match_rating_compare.
    • Caverphone 2.0 (Hood, 2004). caverphone.

    All encoders dispatch through the same peel_to_ascii_string
    helper, accept str/bytes interchangeably, and skip non-letter /
    non-ASCII codepoints. Cross-checked against Apache Commons Codec
    reference data and the jellyfish, metaphone, doublemetaphone,
    and pyphonetics PyPI packages.

  • Dynamic Time Warping (dtw, dtw_distances). Sakoe-Chiba band
    via window= (absolute integer radius or fractional 0 < r <= 1);
    L1 vs L2-squared distance auto-picked by dtype (int16 -> L1,
    float32/float64 -> L2-squared) with explicit distance=
    override. Inputs are NumPy ndarray; matching dtype enforced.

  • cdist_top_k_per_query. Generator API:
    for query, [(score, target), ...] in cdist_top_k_per_query(...).
    Per-query top-k heap rather than the global top-k that
    cdist_top_k returns. pruning=False by default; pruning=True
    enables adaptive length-difference pruning using the same
    max_normalized_similarity closed-form bound as
    cdist_above_threshold. Worst-in-heap tightens as scoring
    progresses, so targets whose bound can't beat the current heap
    minimum skip the kernel entirely. ~12× faster than the unpruned
    baseline on wide-length workloads in benchmarks; flat on tight-
    length workloads. Hamming length-mismatch pairs are skipped via
    the bound rather than raising ValueError.

  • Wide-token Farrar batch (Phase B.2.x). Full SW + NW Farrar
    dispatch through the wide-token kernel for NumPy ndarray with
    uint16 dtype and str inputs with codepoints > 255 (UCS-2).
    Auto-promotes at the binding layer; 8-bit byte kernel stays
    byte-stable for ASCII / Latin-1 / bytes workloads.

  • Wider PyPI metadata. [project.urls] (Homepage / Documentation /
    Repository / Source / Download / Issues / Changelog / Benchmarks),
    maintainers, license-files = ["LICENSE"] (PEP 639),
    Typing :: Typed classifier, Natural Language classifiers for
    English and Chinese (Simplified), Operating System classifiers for
    Linux and macOS, and named extras for bench (rapidfuzz,
    Levenshtein, editdistance, parasail) and phonetic-compat
    (jellyfish, metaphone, doublemetaphone, pyphonetics).

Changed

  • Python 3.12+ floor. requires-python = ">=3.12", dropping
    3.9 / 3.10 / 3.11. The nanobind 2.x vectorcall fast path is
    unconditional from this version; the dispatch layer no longer
    needs the dict-lookup workarounds that replaced 3.10's match
    statements.

  • Vectorcall: skip the Python wrapper frame for every cheap
    function.
    Phonetic encoders, Jaro / Jaro-Winkler scalar entry
    points, the scalar Levenshtein / OSA / Hamming / Indel / true-DL /
    DTW entry points, *_top_k, extract, and extract_best are now
    direct re-exports of the C++ binding rather than Python forwarder
    wrappers. Tight-loop savings measured at 22-54% per call on the
    encoders and 22-26% on Levenshtein-family scoring. Docstrings
    moved to the C++ binding side so help(sa.soundex) etc. still
    works.

  • str / bytes rejection on batch inputs moved to C++. The
    reject_str_or_bytes_targets helper fires from a single shared
    point in preprocess.hpp; the per-binding _coerce_targets Python
    helper is gone. Same TypeError, fewer Python frames.

Fixed

  • Generator double-iteration in *_top_k and extract. The
    bindings called PySequence_Fast to grab items for make_top_k,
    then passed the original handle to the score dispatcher — which
    re-materialised it. Generators were getting consumed by the first
    pass and the second returned empty. The dispatch calls now take
    the already-materialised owner. Surfaced by the wrapper-removal
    pass that exposed the underlying behaviour.

  • Double Metaphone alternate cleanup. Empty alternate (matching
    the metaphone and doublemetaphone PyPI conventions) when the
    primary and alternate codes are identical, so if alt: works.

Notes

  • No abi3 wheel. The Stable ABI excludes the
    cpython/unicodeobject.h fast macros (PyUnicode_KIND,
    PyUnicode_*_DATA, PyUnicode_GET_LENGTH) that the zero-copy
    UCS-1/UCS-2/UCS-4 path relies on for the CJK / wide-token SIMD
    kernels. Their function-form equivalents either allocate a copy or
    are one call per codepoint, both fatal to the SIMD path. We ship
    one wheel per minor Python version (3.12 / 3.13 / 3.14) instead.
    Rationale recorded in CMakeLists.txt.

stride-align 0.3.0

Choose a tag to compare

@adamdeprince adamdeprince released this 24 May 15:44

stride-align 0.3.0

Highlights (full details in CHANGELOG.md):

New scorers

  • Indel distance (Scorer.INDEL / Scorer.INDEL_NORMALIZED) — Levenshtein restricted to insertions and deletions; bit-parallel Allison-Dix single-word kernel.
  • True (unrestricted) Damerau-Levenshtein (Scorer.TRUE_DAMERAU_LEVENSHTEIN / Scorer.TRUE_DAMERAU_LEVENSHTEIN_NORMALIZED) — the unrestricted form where a character may participate in multiple edits. Existing Scorer.DAMERAU_LEVENSHTEIN continues to refer to OSA.

cdist pruning + cutoff push-down

  • Length-difference pruning for cdist_above_threshold and cdist_top_k.
  • Row-sort by query length in cdist_top_k.
  • Per-pair cutoff push-down into the Myers (Lev), OSA, and Hamming SIMD inner loops.
  • Cross-arch geomean 426x speedup at threshold 0.99 vs the unpruned baseline (24 scorer×host cells, median 512x, range 145x–1408x). See BENCHMARK.md and the Loongson vs Tiger Lake report (简体中文).

Fixes

  • macOS / LoongArch64 cdist_above_threshold iteratornb::stop_iteration across DSO boundaries was surfacing as RuntimeError instead of StopIteration. Now uses the Python C API (PyErr_SetNone(PyExc_StopIteration) + null nb::object return). Fixes 91 macOS test failures.

Build / packaging

  • Python 3.9 support (was 3.10+). The three match blocks became dict lookups; everything else was already 3.9-compatible.
  • C++23 → C++20 build requirement. The codebase didn't actually use any C++23 stdlib feature; lowering lets older toolchains (gcc 10) build the project. Two libstdc++-10 gaps (std::bit_cast, std::make_unique_for_overwrite) bridged with feature-test-gated fallbacks. See docs/power8-gcc10-workarounds.md.

Distribution

  • PyPI (pip install stride-align): sdist + 23 wheels —

    • 6 manylinux_2_27 x86_64 (cp39–cp314)
    • 6 musllinux_1_2 x86_64 (cp39–cp314)
    • 5 manylinux_2_39 aarch64 (cp310–cp314)
    • 6 macOS arm64 (cp39–cp314)
  • This GitHub release: only the Linux LoongArch64 wheel (cp313), which PyPI's platform-tag list doesn't yet accept. Install with:

    pip install \
      https://github.com/adamdeprince/stride-align/releases/download/v0.3.0/stride_align-0.3.0-cp313-cp313-linux_loongarch64.whl

    See README §安装 for the full Loongson install recipe.

docs

v0.2.0

Choose a tag to compare

@adamdeprince adamdeprince released this 19 May 11:45

Levenshtein + Damerau-Levenshtein, multi-arch SIMD batch.

What's new since v0.1.0

  • Multi-word Levenshtein SIMD for patterns up to 256 chars (W = 2/3/4).
  • score_cutoff parameter on every levenshtein_* function. Per-lane done masks + all-lanes early-exit; up to 6x rapidfuzz on cutoff workloads.
  • Zero-copy singular dispatch for bytes / 1-byte unicode (levenshtein_score(q, t) skips the prepare_alignment vector copy).
  • Damerau-Levenshtein (OSA-restricted, Hyyrö 2002) — scalar bit-parallel + SIMD batch. New API: damerau_levenshtein_score, _normalized_score, _scores, _normalized_scores.
  • NeonOps / LsxOps / LasxOps / VsxOps Ops bundles. Combined with the existing x86 SSE/AVX2/AVX-512/AVX10 bundles, every SIMD ISA we support now has a multi-target Levenshtein and Damerau-Levenshtein kernel:
    • Intel SSE4.1 / AVX2 / AVX-512 / AVX10-256 / AVX10-512
    • Apple Silicon NEON (macOS arm64)
    • AArch64 NEON / SVE / SVE2 (Linux)
    • LoongArch LSX / LASX
    • PowerPC VSX (Power8+)

Highlight benchmarks vs rapidfuzz / python-Levenshtein

arch workload ratio
Intel AVX-512 Lev medium q=100, 1-vs-200 3.03x rapidfuzz
Intel AVX-512 Damerau short q=20, 1-vs-1000 4.22x rapidfuzz OSA
Intel AVX-512 Lev cutoff q=50, K=12 6.03x rapidfuzz
Mac M4 NEON Lev short q=30, 1-vs-1000 8.54x python-Levenshtein
Mac M4 NEON Damerau short q=30, 1-vs-1000 7.45x rapidfuzz OSA
Graviton4 (NEON/SVE/SVE2) Lev short q=30, 1-vs-1000 4.05x python-Levenshtein
Power8 VSX Lev multi-word q=200, 1-vs-200 3.03x vs generic
Loongson LASX Lev multi-word q=100, 1-vs-200 3.34x vs generic

Full benchmarks in BENCHMARK.md.

Installation

```bash
pip install stride-align==0.2.0
```

PyPI has wheels for Python 3.10-3.14 on Linux x86_64 (manylinux_2_28), Linux aarch64 (manylinux_2_39), Linux ppc64le (manylinux_2_34), and macOS arm64. LoongArch64 wheels are attached to this release (PyPI doesn't accept the linux_loongarch64 platform tag); install with:

```bash
PY=$(python3 -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")')
pip install https://github.com/adamdeprince/stride-align/releases/download/v0.2.0/stride_align-0.2.0-\${PY}-\${PY}-linux_loongarch64.whl
```

stride-align 0.1.0

Choose a tag to compare

@adamdeprince adamdeprince released this 18 May 09:28

stride-align v0.1.0 — first PyPI release.

Most wheels are on PyPI: https://pypi.org/project/stride-align/0.1.0/

LoongArch64 wheels live here on GitHub Releases because PyPI does not
yet accept the linux_loongarch64 or manylinux_2_38_loongarch64
platform tags. Install on Loongson with:

PY=$(python3 -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")')
pip install https://github.com/adamdeprince/stride-align/releases/download/v0.1.0/stride_align-0.1.0-${PY}-${PY}-linux_loongarch64.whl

Available wheels (all five SIMD backends inside — LSX + LASX + generic + SWAR):

  • Python 3.10 / LoongArch64
  • Python 3.11 / LoongArch64
  • Python 3.12 / LoongArch64
  • Python 3.13 / LoongArch64
  • Python 3.14 / LoongArch64

Built on Loongson 3A6000-class hardware (Kylin V10 SP1, GCC 15.2.0,
CMake 4.3.2). See BENCHMARK.md for performance numbers.