{"id":50817216,"url":"https://github.com/adamdeprince/stride-align","last_synced_at":"2026-06-13T10:33:15.431Z","repository":{"id":358592739,"uuid":"1211094672","full_name":"adamdeprince/stride-align","owner":"adamdeprince","description":"High-performance Python/C++ string and sequence alignment: Smith-Waterman, Needleman-Wunsch, Levenshtein, Damerau-Levenshtein, Jaro, and batch fuzzy matching.","archived":false,"fork":false,"pushed_at":"2026-06-05T03:28:32.000Z","size":1878,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T04:12:52.276Z","etag":null,"topics":["bioinformatics","damerau-levenshtein","fuzzy-matching","jaro","levenshtein","needleman-wunsch","sequence-alignment","smith-waterman","string-alignment"],"latest_commit_sha":null,"homepage":"https://stride-align.com","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adamdeprince.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.bib","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-15T04:06:37.000Z","updated_at":"2026-06-01T09:03:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/adamdeprince/stride-align","commit_stats":null,"previous_names":["adamdeprince/stride-align"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/adamdeprince/stride-align","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamdeprince%2Fstride-align","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamdeprince%2Fstride-align/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamdeprince%2Fstride-align/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamdeprince%2Fstride-align/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamdeprince","download_url":"https://codeload.github.com/adamdeprince/stride-align/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamdeprince%2Fstride-align/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34281700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bioinformatics","damerau-levenshtein","fuzzy-matching","jaro","levenshtein","needleman-wunsch","sequence-alignment","smith-waterman","string-alignment"],"created_at":"2026-06-13T10:33:13.988Z","updated_at":"2026-06-13T10:33:15.390Z","avatar_url":"https://github.com/adamdeprince.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# stride-align\n\n**Languages:** [English](README.md) · [简体中文](README.zh-CN.md)\n\n`stride-align` is a SIMD-accelerated Python library for fuzzy string\nmatching, sequence alignment, phonetic encoding, and time-series\ndistance. It ships Smith-Waterman and Needleman-Wunsch alignment,\nLevenshtein and Damerau-Levenshtein edit distance, Jaro and\nJaro-Winkler similarity, Hamming and Indel distance, Dynamic Time\nWarping (`int16` / `float32` / `float64`), and the standard phonetic\nencoders — Soundex, Metaphone (Philips and jellyfish variants),\nDouble Metaphone (Apache Commons and Python-package variants),\nNYSIIS, Match Rating Approach, Caverphone 2, Cologne Phonetic,\nDaitch-Mokotoff Soundex, and Beider-Morse Phonetic Matching\n(GENERIC). Scoring kernels are\nhand-vectorised behind a runtime CPU dispatcher: x86 SSE4.1 / AVX2 /\nAVX-512BW+VL / AVX10-256 / AVX10-512, ARM NEON and SVE/SVE2,\nLoongArch LSX and LASX, PowerPC VSX, with a scalar fallback. Python\nbindings use nanobind with vectorcall on every entry point, target\nPython 3.12+, and accept `bytes`, `str` (UCS-1/UCS-2/UCS-4\nzero-copy), and NumPy `ndarray`.\n\nBuilt for high-throughput fuzzy-match workloads — record linkage,\ndeduplication, search-as-you-type, NLP token similarity,\nbioinformatics local alignment — with first-class CJK: UCS-2 inputs\nroute to a 16-bit-token Farrar kernel rather than being downconverted\nto bytes, so Chinese, Japanese and Korean strings hit the same SIMD\npath as ASCII. The all-pairs surface (`cdist`,\n`cdist_above_threshold`, `cdist_top_k`, `cdist_top_k_per_query`)\ncombines per-target SIMD scoring with closed-form length-difference\npruning that skips work when a target's max possible similarity\nprovably can't clear the running heap minimum or threshold.\nSubstitution matrices (BLOSUM, PAM) and affine gaps are supported on\nthe alignment path. Correctness is validated on real x86, Apple\nSilicon, Graviton ARM, Loongson LoongArch, and POWER8 hardware —\nbenchmarks at\n[stride-align.com/BENCHMARK.html](https://stride-align.com/BENCHMARK.html).\n\nInstead of giving you a lecture, we're going to learn by doing.\nLet's dive right into how it works.\n\n## Installation\n\n```bash\npip install stride-align\n```\n\nPrebuilt wheels cover Linux x86_64 (glibc and musl), macOS arm64,\nLinux aarch64, and Linux ppc64le on CPython 3.12 / 3.13 / 3.14.\nOther (platform, Python) pairs fall back to the PyPI source\ndistribution and compile locally; you'll need a C++20 compiler and\nCMake ≥ 3.26.\n\n**Loongson / LoongArch64 users:** wheels live on GitHub Releases\nrather than PyPI, and you pick between the old-world and new-world\nbinary worlds — see\n[LoongArch installation](#loongarch-installation) further down.\n\nFirst, just a disclaimer: I'm not using religious texts here to push\nan agenda - for this demo I need multiple largish public domain\ndocuments that have the same meaning but are phrased differently. The\nBible just happens to fit that demo requirement freakishly well.\n\nImagine we have two sentences - let's use the first sentence in\nGenesis for this:\n\nIn the American Standard Version we have: \"In the beginning God\ncreated the heavens and the earth.\"\n\nIn the King James Version we have: \"In the beginning God created the\nheaven and the earth.\"\n\nWe can see with our eyes there's a difference - heavens vs heaven.\nBut how do we quantify this difference? We'd use this little bit of\ncode:\n\n```python\nimport stride_align as sa\n\nprint(sa.smith_waterman_normalized_score(\n      \"In the beginning God created the heavens and the earth.\",\n      \"In the beginning God created the heaven and the earth.\"))\n```\n\nWhen we run this it prints:\n\n```python\n0.9907407407407407\n```\n\nNormalized scores are between `0` and `1`. A score of `1` means the\ninputs are an exact match under the default scoring model. Scores near\n`0` mean the inputs have little in common, though Smith-Waterman may\nstill find small local matches inside otherwise unrelated strings.\n\nNow let's change the text and see what happens to the score.\n\n```python\nimport stride_align as sa\n\nprint(sa.smith_waterman_normalized_score(\n      \"In the beginning God created the heavens and the earth.\",\n      \"The quick brown fox jumped over the lazy dog.\"))\n```\n\nand Python prints\n\n```\n0.12222222222222222\n```\n\nStarting to get the idea? The more similar the strings, the higher the score.\n\nLet's build a bigger example, something that gives us a feel for the\nlibrary's performance. You'll probably notice that we switch between\nSmith-Waterman and Needleman-Wunsch and may be wondering which to use\nwhen. Use Needleman-Wunsch when you want to compare the whole input\nagainst the whole input. Use Smith-Waterman when you want to find the\nbest matching region inside larger inputs.\n\nOkay, let's move on to the demo code. You need `requests` for this\npart of the demo:\n\n```bash\npip install requests\n```\n\n```python\nimport os, time, requests\nimport stride_align as sa\n\nif not os.path.exists(\"kjv.txt\"):\n    response = requests.get(\"https://openbible.com/textfiles/kjv.txt\")\n    response.raise_for_status()\n    response.encoding = \"utf-8-sig\"\n    open(\"kjv.txt\", \"w\", encoding=\"utf-8\").write(response.text)\n\nlines = [line.strip().lower() for line in open(\"kjv.txt\")][2:]\n\nwhile True:\n    if not (query := input(\"Enter a snippet to match.  Press enter to end.\\n\")):\n        break\n    t = time.perf_counter()\n    scores = sa.needleman_wunsch_normalized_scores(query.lower(), lines)\n    best = int(scores.argmax())\n    print()\n    print(\"Score:\", float(scores[best]))\n    print(lines[best])\n    print(\"Search time: %0.2fms\" % ((time.perf_counter() - t) * 1000))\n    print()\n    print()\n```\n\nNow how can we use this? Suppose we have a random Bible verse and\nwant to know what chapter and verse it comes from. `grep` you say?\nOh, heavens, no, we made a mistake. The verse we have is from a\ndifferent translation, say the Catholic Public Domain, and what we\nhave on our computer is the King James Bible. `grep`'s exact string\nmatching won't work here. How do we find the chapter and verse? We\nsearch for the \"closest\" or \"most similar\" string using `stride-align`,\nof course.\n\nIn our demo the first part concerns itself with downloading and\ncaching. The good folks at [Open Bible](https://openbible.com) put\nthis text where it's HTTP-reachable, but we want to be respectful of\ntheir IT budget so we cache what we download. It's just good\ncitizenship.\n\nIn the next part we load all of the lines into a list. We remove\nnewlines and make everything lower case because we don't want to get\nall fiddly about whether we're holding the shift key.\n\nLastly that `while True:` loop collects a line of text, presumably the\nBible verse from the Catholic version of the Bible we want to look up\nthe chapter and verse for, and matches it against all of the lines in\nthe King James Bible using the batch form of Needleman-Wunsch. It\nreturns an array of scores. We use `argmax()` to find the best-scoring\nline and then print the line associated with that index. Let's try it.\n\nI'm going to use Jeremiah 4:28 from the Catholic Bible - it's actually\nquite different from the same verse in the King James Bible. Let's see\nwhat happens ...\n\n```\n$ python3 demo2.py\nEnter a snippet to match.  Press enter to end.\nThe earth will mourn, and the heavens will lament from above. For I have spoken, I have decided, and I have not regretted. Neither will I be turned away from it.\n\nScore: 0.3598901098901099\njeremiah 4:28\tfor this shall the earth mourn, and the heavens above be black: because i have spoken [it], i have purposed [it], and will not repent, neither will i turn back from it.\nSearch time: 206.51ms\n\n```\n\n... and we found it! And pretty quickly too.\n\n\nNow let's do another demo: spell checking.\n\nThis is a toy spell checker, not a production one. It ignores punctuation,\ncapitalization, word frequency, proper nouns, and context. The point is to\nshow the same one-query-against-many-candidates pattern on a familiar task.\n\n```python\nimport os, sys\nimport stride_align as sa\n\npaths = ['/usr/share/dict/words',\n         '/usr/dict/words',\n         '/var/lib/dict/words',\n         '/etc/dictionaries-common/words']\n\nfor path in paths:\n    if os.path.exists(path):\n        break\nelse:\n    print(\"Sorry, I can't find your dictionary\", file=sys.stderr)\n    exit(1)\n\n\nwords = [line.strip().lower() for line in open(path)]\n\n\nfor line in sys.stdin:\n    new_line = []\n    for word in line.split():\n        scores = sa.needleman_wunsch_normalized_scores(word.lower(), words)\n        word = words[int(scores.argmax())]\n        new_line.append(word)\n    print(' '.join(new_line), flush=True)\n```\n\nThe first thing this script does is try to find our operating system's\nlist of correctly spelled words. Its location can vary from\ndistribution to distribution. Once we've found it, we load it, strip\noff newlines and start the act of spell checking.\n\nThe spell checking looks a lot like the matching we did before. For\neach candidate word, we match it against all of the words in our list\nof correctly spelled words, use `argmax()` to find the highest-scoring\ncandidate, and replace the word with that candidate. We could speed\nthings up with some optimizations, like not searching for a match for\ncorrectly spelled words, but this is a demo and that optimization is\nleft as an exercise for the reader.\n\nLet's see how it works!\n\n```bash\n$ cat - | python3 demo3.py\nthis is a demonstrtion of a spel checker\nit doesn't matter that I can't spell corectly\n\nthis is a demonstration of a spell checker\nit doesn't matter that i can't spell correctly\n```\n\n\n## Details\n\nThe current scaffold provides:\n\n- Needleman-Wunsch score-only alignment\n- Needleman-Wunsch alignment with traceback\n- Smith-Waterman score-only alignment\n- Smith-Waterman alignment with traceback\n- A backend layout that matches the specialization pattern used in `massive-speedup`\n- CPU/backend detection and Python-side backend dispatch\n\nThe native boundary accepts:\n\n- `bytes` against `bytes`\n- `str` against `str`\n- sequences of immutable hashable Python objects\n- mixed sequence/object inputs where a `str` or `bytes` side is treated as a sequence\n\nDirect `bytes` versus `str` pairs raise `TypeError`.\n\nThe current implementations are generic dynamic-programming kernels with preprocessing\nthat serializes Python inputs into 8, 16, 32, or 64-bit token streams. SIMD-specialized\nbackends can replace the backend translation units later without changing the Python API.\n\nScore-only functions return numeric scores. The normalized variants return\nscores between `0` and `1`. Path functions return alignment result objects\ncontaining the score, aligned values, operations, and CIGAR-style summaries\nwhere available.\n\n## API\n\n```python\nimport stride_align\n\nscore = stride_align.needleman_wunsch_score(\"ACGT\", \"ACCT\")\nscores = stride_align.Scores(\"ACGT\", variant=\"needleman_wunsch\").compare([\"ACCT\", \"AGGT\"])\nresult = stride_align.smith_waterman_path(\"ACCGT\", \"CCG\")\nwide_result = stride_align.smith_waterman_path(\"ACCGT\", \"CCG\", width=64)\nobject_result = stride_align.needleman_wunsch_path(\n    [frozenset({1}), frozenset({2})],\n    [frozenset({1}), frozenset({3})],\n)\n\nprint(score)\nprint(scores)\nprint(result.score, result.aligned_query, result.aligned_target, result.operations)\nprint(wide_result.score)\nprint(object_result.aligned_query, object_result.aligned_target)\n```\n\nUse `Scores(...).compare([...])` or the `*_scores()` functions for one-query\nagainst many-target score workloads. That path prepares the query/profile once\nand is the preferred performance API for repeated English/Chinese text\ncomparisons.\n\nTraceback outputs preserve the paired fast-path type:\n\n- `str` inputs return aligned `str`\n- `bytes` inputs return aligned `bytes`\n- sequence/object inputs return aligned `tuple` values with `None` gaps\n\nPass `width=8`, `16`, `32`, or `64` to force the internal token/scoring width\ninstead of using automatic selection.\n\nSome functions expose CIGAR strings, short for \"Concise Idiosyncratic\nGapped Alignment Report\". CIGAR is the compact alignment-operation notation\nused by SAM/BAM tooling. If you want the full formal version, see the\n[SAM specification](https://samtools.github.io/hts-specs/SAMv1.pdf).\n\n### Substitution matrices (BLOSUM, PAM)\n\nFor protein alignment, `stride_align.matrices` ships the canonical\nBLOSUM and PAM substitution matrices. Pass any of them via the\n`matrix=` kwarg on `smith_waterman_score`, `needleman_wunsch_score`,\nor their `_scores` batch counterparts:\n\n```python\nimport stride_align\nfrom stride_align.matrices import blosum62, pam250\n\n# Local alignment, NCBI standard BLOSUM62 with affine gaps (open=-11,\n# extend=-1). matrix= is mutually exclusive with match_score / mismatch_score.\nstride_align.smith_waterman_score(\n    \"HEAGAWGHEE\", \"PAWHEAE\",\n    matrix=blosum62,\n    gap_open_score=-11, gap_extend_score=-1,\n)\n\n# Batch (1 query × N targets) with profile reuse — the recommended\n# path for \"score one query against a library\".\nstride_align.smith_waterman_scores(\n    \"HEAGAWGHEE\",\n    [\"PAWHEAE\", \"HEAGAWGHEE\", \"MEEPS\"],\n    matrix=pam250, gap_open_score=-14, gap_extend_score=-2,\n)\n\n# Custom matrices: parse any NCBI-format text file\ncustom = stride_align.matrices.SubstitutionMatrix.from_ncbi_text(\n    open(\"/path/to/BLOSUM62\").read(),\n    name=\"BLOSUM62\",\n    gap_open=-11, gap_extend=-1,\n)\n```\n\nEach built-in `SubstitutionMatrix` exposes its alphabet, matrix data\n(`int8` ndarray), and recommended gap defaults via `.gap_score`\n(linear), `.gap_open`, and `.gap_extend`. Both linear gaps (`gap_score=`)\nand affine gaps (`gap_open_score=` + `gap_extend_score=`) are\nsupported on the AVX-512 backend; other SIMD backends currently fall\nback to the scalar generic kernel for matrix-mode.\n\nThe shipped matrix values come from the NCBI BLAST distribution\n[`ftp.ncbi.nih.gov/blast/matrices/`](https://ftp.ncbi.nih.gov/blast/matrices/),\nwhich carries the canonical reference scores. The original\npublications are:\n\n- **BLOSUM45 / 50 / 62 / 80 / 90** — Henikoff S., Henikoff J.G. (1992).\n  *Amino acid substitution matrices from protein blocks*. PNAS\n  89(22):10915–10919.\n  [doi:10.1073/pnas.89.22.10915](https://doi.org/10.1073/pnas.89.22.10915)\n  \u0026nbsp;·\u0026nbsp;\n  [PDF (open access)](https://www.pnas.org/doi/pdf/10.1073/pnas.89.22.10915)\n- **PAM30 / 70 / 250** — Dayhoff M.O., Schwartz R.M., Orcutt B.C.\n  (1978). *A model of evolutionary change in proteins*. In *Atlas of\n  Protein Sequence and Structure*, vol. 5, supplement 3, pages 345–352.\n  National Biomedical Research Foundation, Washington, D.C. (Book\n  chapter; not available online as an open PDF. A widely cited\n  follow-on derivation appears in Schwartz R.M., Dayhoff M.O. (1978),\n  *Matrices for detecting distant relationships*, same volume,\n  pages 353–358.)\n\n### rapidfuzz compatibility (drop-in shim)\n\nReplace one import line and most rapidfuzz code keeps working:\n\n```python\n# Before:\n# import rapidfuzz\n\n# After:\nimport stride_align.rapidfuzz as rapidfuzz\n\n# fuzz: full token-ratio family, scores in [0, 100]\nrapidfuzz.fuzz.ratio(\"hello\", \"hallo\")                  # 80.0\nrapidfuzz.fuzz.WRatio(\"foo bar baz\", \"foo bar\")         # 90.0\nrapidfuzz.fuzz.token_set_ratio(\"the cat\", \"cat the\")    # 100.0\n\n# distance: classes with distance / normalized / similarity methods,\n# plus editops / opcodes for Levenshtein.\nrapidfuzz.distance.Levenshtein.distance(\"kitten\", \"sitting\")          # 3\nrapidfuzz.distance.JaroWinkler.normalized_similarity(\"MARTHA\", \"MARHTA\")\nrapidfuzz.distance.Levenshtein.editops(\"kitten\", \"sitting\")\n# -\u003e Editops([Editop(tag='replace', src_pos=0, dest_pos=0), ...], src_len=6, dest_len=7)\n\n# process: extract / extractOne / cdist\nrapidfuzz.process.extract(\"hello\", [\"hallo\", \"world\", \"helo\"], limit=2)\n# -\u003e [('helo', 88.88, 2), ('hallo', 80.0, 0)]\n\n# utils: default_process (matches upstream bit-exactly, does NOT\n# collapse internal whitespace runs)\nrapidfuzz.utils.default_process(\"Hello, World!\")        # 'hello  world'\n```\n\nKnown divergences: the `partial_ratio` family inherits stride-align's\nPhase D.3 conservative-underestimate — never overshoots upstream, but\ncan underestimate by a few points on pairs where rapidfuzz finds a\nshifted optimal window. `Levenshtein.distance` does not yet support\nthe `weights=(insert, delete, replace)` kwarg.\n\n### parasail compatibility (drop-in shim)\n\nReplace one import line and most parasail code keeps working:\n\n```python\n# Before:\n# import parasail\n\n# After:\nimport stride_align.parasail as parasail\n\n# Same parasail signature: (s1, s2, open, extend, matrix)\n# Gap penalties are positive numbers (BLAST convention:\n# cost(N) = open + (N-1)*extend).\nr = parasail.sw_trace(\"HEAGAWGHEE\", \"PAWHEAE\", 11, 1, parasail.blosum62)\nprint(r.score)               # int\nprint(r.cigar.decode)        # bytes, e.g. b'2=1X3='\nprint(r.traceback.query)     # 'HEAGAWGHEE' aligned with gaps\nprint(r.traceback.ref)       # 'PAWHEAE'    aligned with gaps\nprint(r.traceback.comp)      # '|.| ||'-style match annotation\n\n# matrix_create + stats\nm = parasail.matrix_create(\"ACGT\", 2, -1)\nr = parasail.sw_stats(\"ACGTAC\", \"ACATAC\", 5, 2, m)\nprint(r.matches, r.similar, r.length)\n\n# The 2000+ kernel-suffix variants (sw_striped_avx2_16, nw_scan_64,\n# sw_trace_diag_sat, ...) all alias to the matching core entry —\n# stride-align picks the kernel based on score range and hardware.\nparasail.sw_striped_avx2_16(\"ACGT\", \"ACGT\", 5, 2, m)\n```\n\nKnown divergences: SW with multiple optimal alignments may pick a\ndifferent path than upstream parasail (both score-correct); the\n`sg_qb`/`sg_qe`/`sg_qb_de` style semi-global mode selectors and the\n`dnafull` / `nuc44` matrices are not yet provided.\n\n### Edit-distance scorers\n\nBeyond Smith-Waterman and Needleman-Wunsch, `stride-align` exposes\nsix unit-cost edit-distance and similarity metrics — each with its\nown SIMD-batched code path:\n\n```python\nimport stride_align\n\n# Levenshtein (Myers 1999 bit-parallel) — inserts, deletes, substitutes\nstride_align.levenshtein_score(\"kitten\", \"sitting\")               # -\u003e 3\nstride_align.levenshtein_normalized_score(\"kitten\", \"sitting\")    # -\u003e 0.571...\nstride_align.levenshtein_scores(\"kitten\", [\"kit\", \"sitting\"])     # -\u003e ndarray[int64]\n\n# Optional `score_cutoff` (rapidfuzz convention): bail early per-target,\n# results that exceed the cutoff come back as `cutoff + 1`.\nstride_align.levenshtein_scores(query, targets, score_cutoff=3)\n\n# Damerau-Levenshtein (OSA-restricted, Hyyrö 2002) — adds adjacent\n# transposition at unit cost. This is what rapidfuzz exposes as\n# OSA.distance and is what most callers asking for\n# \"Damerau-Levenshtein\" actually want.\nstride_align.damerau_levenshtein_score(\"ab\", \"ba\")                # -\u003e 1\n\n# True Damerau-Levenshtein — the unrestricted form, where one\n# character may participate in more than one edit. Slower (no\n# bit-parallel kernel yet) but matches rapidfuzz.distance.DamerauLevenshtein\n# exactly. Diverges from OSA on overlapping transpositions, e.g.\n# \"ca\" -\u003e \"abc\": OSA=3, true-DL=2.\nstride_align.true_damerau_levenshtein_score(\"ca\", \"abc\")          # -\u003e 2\n\n# Indel — Levenshtein restricted to insertions and deletions, no\n# substitutions. Equivalent to |a| + |b| - 2 * LCS(a, b). Bit-\n# parallel Allison-Dix (1986) inner loop.\nstride_align.indel_score(\"kitten\", \"sitting\")                     # -\u003e 5\n\n# Hamming — count of positions where two equal-length strings differ.\n# Cutoff variant bails the byte loop once mismatches exceed the cap.\nstride_align.hamming_score(\"100\", \"110\")                          # -\u003e 1\n\n# Jaro / Jaro-Winkler — similarities in [0, 1]; Winkler adds a\n# capped prefix bonus.\nstride_align.jaro_similarity(\"martha\", \"marhta\")                  # -\u003e 0.944...\nstride_align.jaro_winkler_similarity(\"martha\", \"marhta\")          # -\u003e 0.961...\n```\n\nThe batch variants (`*_scores`, `*_similarities`) pack one target\nper SIMD lane on every supported backend:\n\n- x86: SSE4.1 / AVX2 / AVX-512 / AVX10-256 / AVX10-512\n- ARM: NEON (Linux + macOS), SVE / SVE2\n- LoongArch: LSX / LASX\n- PowerPC: VSX\n\nFor Lev / OSA, patterns up to 64 chars run a single-word Myers;\n65–256 chars use the multi-word kernel (W=2/3/4). Indel and OSA\nfall back to scalar bit-parallel for patterns \u003e64 (multi-word\ngeneralization deferred); true-DL is scalar DP only.\n\n### Longest Common Subsequence + Substring\n\nTwo related but distinct dynamic programs, both shipped:\n\n```python\nimport stride_align as sa\n\n# Longest Common Subsequence — characters need not be contiguous.\n# \"ABCBDAB\" and \"BDCAB\" share \"BCAB\" (length 4).\nsa.lcs_length(\"ABCBDAB\", \"BDCAB\")                    # -\u003e 4\n\n# Closed-form relation to Indel distance: indel = |a| + |b| - 2·LCS.\nsa.indel_score(\"kitten\", \"sitting\") == \\\n    len(\"kitten\") + len(\"sitting\") - 2 * sa.lcs_length(\"kitten\", \"sitting\")\n# -\u003e True\n\n# Longest Common Substring — characters MUST be contiguous.\nsa.lcs_substring_length(\"ABCBDAB\", \"BDCAB\")          # -\u003e 2\nsa.lcs_substring(\"ABCBDAB\", \"BDCAB\")                 # -\u003e \"AB\"\n\n# Result type matches inputs: bytes in, bytes out.\nsa.lcs_substring(b\"hello world\", b\"world hello\")     # -\u003e b\"hello\"\n\n# Codepoint engine — non-ASCII is first-class.\nsa.lcs_substring(\"Müller\", \"Mueller\")                # -\u003e \"ller\"\n```\n\nBoth DPs are scalar `O(m·n)` time with two rolling rows for\n`O(min(m,n))` (subsequence) or `O(|b|)` (substring) space. When\nmultiple substrings tie at the maximum length, the first occurrence\nin `a` is returned (matches `str.find` convention).\n\n### Ratcliff-Obershelp similarity\n\nThe algorithm Python's `difflib.SequenceMatcher().ratio()` ships,\nwhich `rapidfuzz` does not — recursive longest-matching-substring\nsplit, summed match lengths divided by total length:\n\n```python\nimport stride_align as sa\n\nsa.ratcliff_obershelp_similarity(\"kitten\", \"sitting\")\n# -\u003e 0.6153846153846154\n\n# Bit-exact with difflib at autojunk=False (we have no junk\n# character heuristic):\nimport difflib\nsa.ratcliff_obershelp_similarity(\"ABCBDAB\", \"BDCAB\") == \\\n    difflib.SequenceMatcher(None, \"ABCBDAB\", \"BDCAB\", autojunk=False).ratio()\n# -\u003e True\n\n# Batch form: one query against many targets, returned as\n# ndarray[float64].\nsa.ratcliff_obershelp_similarities(\"kitten\",\n                                    [\"sitting\", \"kitten\", \"kit\"])\n# -\u003e array([0.61538462, 1.        , 0.66666667])\n```\n\nNot commutative — the inner longest-common-substring tiebreak\n(`earliest in a, then earliest in b`) means the recursion splits\nleftover ranges differently for `(a, b)` vs `(b, a)`, and the total\nmatch length can differ. Faithful to difflib, which has the same\nproperty; `sa.ratcliff_obershelp_similarity(\"ABCBDAB\", \"BDCAB\")`\ngives `0.333…` while the reverse gives `0.667…`. Pin both\ndirections if your tests need an order-independent metric.\n\n### N-gram set similarity\n\nFour metrics over character n-gram **multisets** (each n-gram counted\nwith multiplicity), keyword-only `n=` (default 2 — character bigrams):\n\n```python\nimport stride_align as sa\n\n# Jaccard: |A ∩ B| / |A ∪ B|\nsa.jaccard(\"ABCBDAB\", \"BDCAB\")                  # -\u003e 0.25\n\n# Sørensen-Dice: 2 * |A ∩ B| / (|A| + |B|)\nsa.dice(\"ABCBDAB\", \"BDCAB\")                     # -\u003e 0.4\n\n# Overlap coefficient: |A ∩ B| / min(|A|, |B|)\nsa.overlap(\"ABCBDAB\", \"BDCAB\")                  # -\u003e 0.5\n\n# Cosine over multiset frequency vectors: ⟨A, B⟩ / (‖A‖ · ‖B‖)\nsa.cosine(\"ABCBDAB\", \"BDCAB\")                   # -\u003e ~0.5303\n\n# Trigrams.\nsa.jaccard(\"hello\", \"help\", n=3)                # -\u003e 0.25\n\n# Batch — query multiset built once and reused across targets.\nsa.jaccard_similarities(\"kitten\", [\"sitting\", \"kitten\", \"kit\"])\n# -\u003e array([0.25, 1.0, 0.111...])\n```\n\nAll four metrics are symmetric and bounded in `[0, 1]`. Identity\nconvention: both inputs empty (or both shorter than `n`) → `1.0`;\none empty → `0.0`. Dice and Jaccard satisfy the closed-form\nrelation `D = 2·J / (1 + J)`.\n\n### Token-ratio family (rapidfuzz `fuzz.*` parity)\n\nDrop-in replacements for the `rapidfuzz.fuzz.*` token-ratio API,\nreturning values in `[0, 1]` (multiply by 100 for rapidfuzz's\n`[0, 100]` convention). The base ratio is `sa.indel_normalized_score`\n— algebraically identical to `rapidfuzz.fuzz.ratio / 100` (both\nreduce to `2 · LCS / (|a| + |b|)`).\n\n```python\nimport stride_align as sa\n\n# Token sort: split on whitespace, sort, join, compute the ratio.\nsa.token_sort_ratio(\"fuzzy wuzzy bear\", \"bear wuzzy fuzzy\")     # -\u003e 1.0\n\n# Token set: set intersection + per-side differences, max of three\n# pairwise ratios.\nsa.token_set_ratio(\"the quick brown fox\", \"the quick brown dog\") # -\u003e ~0.895\n\n# Partial ratio: best match of the shorter string within the longer\n# (sliding-window + LCS-substring candidate).\nsa.partial_ratio(\"apple\", \"an apple a day\")                     # -\u003e 1.0\nsa.partial_ratio(\"java language\",\n                 \"python programming language\")                 # -\u003e ~0.818\n\n# Token-sort / token-set combined with partial ratio.\nsa.partial_token_sort_ratio(\"apple bear\", \"an apple and a bear\") # -\u003e 1.0\nsa.partial_token_set_ratio(\"the cat\",     \"a cat sat down\")      # -\u003e 1.0\n\n# rapidfuzz's weighted blend.\nsa.WRatio(\"fuzzy wuzzy was a bear\", \"wuzzy fuzzy was a bear\")    # -\u003e 1.0\n\n# Case-insensitive: pass a processor callable.\nsa.token_sort_ratio(\"FOO BAR\", \"bar foo\", processor=str.lower)   # -\u003e 1.0\n```\n\n`token_set_ratio` and `partial_token_set_ratio` follow rapidfuzz's\nconvention of returning `0.0` when either side has no tokens after\nwhitespace splitting. The implementations are pure Python on top of\nstride-align's own kernels — no third-party code is imported into\nthe production path.\n\n### Monge-Elkan multi-token similarity\n\nClassic record-linkage hybrid (Monge \u0026 Elkan, 1996). For each token\nin `s1`, find the best-matching token in `s2` under a configurable\ninner similarity, then average across `s1`'s tokens. Asymmetric by\ndefinition — pass `symmetric=True` to average both directions when\nan order-independent score is wanted.\n\n```python\nimport stride_align as sa\n\n# Default inner is Jaro.\nsa.monge_elkan(\"paul johnson\", \"paul jones\")      # -\u003e ~0.94\n\n# Asymmetric: |s1| tokens drive the average.\nsa.monge_elkan(\"paul\",         \"paul johnson\")    # -\u003e 1.0\nsa.monge_elkan(\"paul johnson\", \"paul\")            # -\u003e 0.5\n\n# Symmetric variant.\nsa.monge_elkan(\"paul\",         \"paul johnson\",\n               symmetric=True)                    # -\u003e 0.75\n\n# Inner similarity selection.\nsa.monge_elkan(\"hello world\", \"hallo world\",\n               inner=\"jaro_winkler\")              # boost common prefixes\nsa.monge_elkan(\"hello world\", \"hallo world\",\n               inner=\"levenshtein_ratio\")         # bit-parallel Levenshtein\nsa.monge_elkan(\"a b c\", \"a c d\",\n               inner=lambda x, y: 1.0 if x == y else 0.0)  # custom callable\n\n# Preprocessor (e.g. case-insensitive).\nsa.monge_elkan(\"PAUL JOHNSON\", \"paul Johnson\",\n               processor=str.lower)               # -\u003e 1.0\n```\n\nReturns `1.0` when both inputs have no tokens after whitespace\nsplitting (vacuously identical); `0.0` when exactly one side has no\ntokens. The implementation is pure Python on top of stride-align's\nJaro / Jaro-Winkler / Levenshtein / Indel kernels — no new C++\nkernels and no third-party code in the production path.\n\n### Phonetic encoders\n\nFor name matching, deduplication, and search-as-you-type, `stride-align`\nships the full standard phonetic-encoder family. Each encoder maps a\nstring to a short code such that names that *sound* similar share a\ncode, regardless of spelling:\n\n```python\nimport stride_align as sa\n\n# American Soundex (Russell \u0026 Odell, 1918). 4-character code.\nsa.soundex(\"Robert\")                                     # -\u003e \"R163\"\nsa.soundex(\"Rupert\")                                     # -\u003e \"R163\"\nsa.soundex_equal(\"Robert\", \"Rupert\")                     # -\u003e True\n\n# Metaphone (Lawrence Philips, 1990) — two-letter and longer\n# spec-correct variants. The published 1990 spec and the popular\n# jellyfish library disagree on a handful of edge cases; the variant\n# kwarg picks the rule family.\nsa.metaphone(\"Schmidt\")                                  # -\u003e \"SKMTT\"  (PHILIPS, spec)\nsa.metaphone(\"Schmidt\", variant=sa.MetaphoneVariant.JELLYFISH)  # -\u003e \"SXMTT\"\nsa.metaphone_equal(\"Schmidt\", \"Smith\")                   # -\u003e False\n\n# Double Metaphone (Lawrence Philips, 2000) — primary and alternate\n# codes; the alternate captures plausible non-English pronunciations.\n# COMMONS is the faithful Apache Commons Codec port; PYTHON is bug-\n# compat with the metaphone PyPI package.\nsa.double_metaphone(\"Schwartz\")                          # -\u003e (\"XRTS\", \"XFRTS\")\nsa.double_metaphone(\"Hugh\")                              # -\u003e (\"H\", \"\")\nsa.double_metaphone(\"Hugh\",\n    variant=sa.DoubleMetaphoneVariant.PYTHON)            # -\u003e (\"HH\", \"\")\n\n# NYSIIS (Taft, 1970). More discriminative than Soundex for English\n# names — \"Watkins\" / \"Wilkins\" / \"Wilkinson\" don't collide.\nsa.nysiis(\"Watkins\"), sa.nysiis(\"Wilkins\")               # -\u003e (\"WATCAN\", \"WALCAN\")\n\n# Match Rating Approach (Moore, Western Airlines, 1977). A codex plus\n# a pairwise comparator with length-difference + rating-threshold rules.\nsa.match_rating_codex(\"Christopher\")                     # -\u003e \"CHRPHR\"\nsa.match_rating_compare(\"Robert\", \"Rupert\")              # -\u003e True\n\n# Caverphone 2.0 (Hood, 2004). Fixed-length 10-character code,\n# right-padded with '1'. Designed for late-19th-century New Zealand\n# electoral rolls but widely applied to general English-language\n# name matching.\nsa.caverphone(\"Stevenson\")                               # -\u003e \"STFNSN1111\"\n\n# Cologne Phonetic / Kölner Phonetik (Postel, 1969). German-language\n# encoder that maps letters to digits 0-8 with context-sensitive rules\n# for C, X, D, T, P. Umlauts and ß preprocess to their Latin-letter\n# equivalents so callers don't have to NFKD-fold first.\nsa.cologne_phonetic(\"Müller\")                            # -\u003e \"657\"\nsa.cologne_phonetic(\"Schmidt\")                           # -\u003e \"862\"\n\n# Daitch-Mokotoff Soundex (Daitch \u0026 Mokotoff, 1985). Six-digit\n# Soundex tuned for Slavic and Yiddish surnames. The leading letter\n# is encoded (not preserved verbatim); multi-character clusters like\n# 'sch', 'tsch', 'rz' fire before any single-letter rule; several\n# rules emit '|'-joined alternative codes via branching.\nsa.daitch_mokotoff(\"LEWINSKY\")                           # -\u003e \"876450\"\nsa.daitch_mokotoff(\"Goldman\")                            # -\u003e \"583660\"\nsa.daitch_mokotoff(\"AUERBACH\")                           # -\u003e \"097400|097500\"\nsa.daitch_mokotoff(\"AUERBACH\", branching=False)          # -\u003e \"097400\"\n\n# Beider-Morse Phonetic Matching (Beider \u0026 Morse, 2008). Multi-\n# language phonetic encoder returning a '|'-separated set of plausible\n# pronunciation codes across European languages, optimised for family\n# names. stride-align ships the GENERIC name-type only — the broad\n# general-purpose rule set; the Ashkenazi and Sephardic rule sets from\n# the upstream Apache Commons Codec distribution are not included.\nsa.beider_morse(\"Renault\")\n# -\u003e \"rinD|rinDlt|rina|rinalt|rino|rinolt|rinu|rinult\"\nsa.beider_morse(\"Renault\", rule_type=sa.BmpmRuleType.EXACT)\n# -\u003e \"renau|renault|reno|renolt\"\nsa.beider_morse(\"Müller\", rule_type=sa.BmpmRuleType.EXACT)\n# -\u003e \"mQler|muler\"\nsa.beider_morse(\"d'ortley\", rule_type=sa.BmpmRuleType.EXACT)\n# -\u003e \"(ortlaj|ortlej)-(dortlaj|dortlej)\"   (d' prefix handler)\n```\n\nThe first seven encoders are dispatched through the same byte-\nextraction helper, accept `str` and `bytes` inputs interchangeably,\nand skip non-letter / non-ASCII codepoints before encoding — pre-\nnormalise with `unicodedata.normalize(\"NFKD\", s)` if you want accent\nfolding. Cologne Phonetic re-encodes `str` inputs through UTF-8 so its\nß / Ä / Ö / Ü preprocessing fires correctly. Beider-Morse ships its\nGENERIC rule data (the 63 `gen_*.txt` files from Apache Commons Codec)\nas package resources loaded once at first call via\n`importlib.resources`, runs the language guesser plus a rule-based\nphonetic engine entirely in C++, and returns a `|`-separated UTF-8\nstring of phonetic codes. Cross-checked against the canonical Apache\nCommons Codec reference data and the `jellyfish`, `metaphone`, and\n`doublemetaphone` PyPI packages.\n\n### Dynamic Time Warping\n\nFor aligning numeric sequences whose timing or speed varies — audio\nsignals, gesture / sensor traces, financial time series —\n`stride-align` exposes Dynamic Time Warping with optional Sakoe-Chiba\nband:\n\n```python\nimport numpy as np\nimport stride_align as sa\n\nq = np.array([1.0, 2.0, 3.0, 4.0, 5.0])\nt = np.array([1.0, 2.0, 2.5, 4.0, 5.0])\n\n# Default distance follows the dtype:\n#   float32 / float64 -\u003e L2-squared, (x - y)^2\n#   int16             -\u003e L1, |x - y|  (audio convention)\nsa.dtw(q, t)                                              # -\u003e 0.25\n\n# Sakoe-Chiba band: int radius or fraction of max(|q|, |t|).\nsa.dtw(q, t, window=2)\nsa.dtw(q, t, window=0.2)\n\n# Explicit distance.\nsa.dtw(q.astype(np.int16), t.astype(np.int16), distance=\"l1\")\n\n# Batch.\nsa.dtw_distances(q, [t, t * 2, t + 0.5], window=2)\n```\n\nInputs must be NumPy `ndarray` with matching dtype (`float32`,\n`float64`, or `int16` — the natural audio dtype). Other dtypes and\nnon-ndarray inputs raise `TypeError`.\n\n### `cdist`, `cdist_above_threshold`, `cdist_top_k`, `cdist_top_k_per_query`\n\nFor all-pairs scoring across two lists of strings, `stride-align`\nships three matrix-style entry points:\n\n```python\nqs = [\"kitten\", \"sitting\", \"kit\"]\nts = [\"kitten\", \"kit\", \"sitting\", \"biting\"]\n\n# Full N×M similarity matrix — ndarray[float64] (similarity scorers)\n# or ndarray[int64] (distance scorers).\nsa.cdist(qs, ts, scorer=sa.Scorer.JARO)\n\n# Streaming filter — yields only pairs whose similarity exceeds the\n# threshold. Workers feed a bounded queue; the caller drains it.\n# Length pruning + per-pair cutoff push-down into the kernel skip\n# most of the work at high thresholds.\nfor score, q, t in sa.cdist_above_threshold(\n    qs, ts, scorer=sa.Scorer.LEVENSHTEIN_NORMALIZED, threshold=0.7,\n):\n    ...\n\n# Top-k by score — returns at most k highest-scoring (or lowest, for\n# distance scorers) (score, query, target) tuples. Heaps are\n# per-thread; a shared atomic global-min bound lets the per-pair\n# cutoff push-down lift the prune threshold as work progresses.\nsa.cdist_top_k(qs, ts, scorer=sa.Scorer.JARO, k=10)\n\n# Top-k targets PER QUERY, yielded as a generator. Differs from\n# cdist_top_k (which returns the k highest pairs globally) by keeping\n# a separate top-k heap per query. With pruning=True, the worst-in-\n# heap score adapts as scoring progresses and targets whose closed-\n# form length-difference upper bound on similarity can't beat it\n# are skipped before the kernel runs — a big win on workloads with\n# wide length variation.\nfor query, top in sa.cdist_top_k_per_query(\n    qs, ts, scorer=sa.Scorer.LEVENSHTEIN_NORMALIZED, k=5, pruning=True,\n):\n    # top is [(score, target), ...] sorted descending\n    ...\n\n# Smith-Waterman and Needleman-Wunsch on cdist. The SW / NW scorers\n# accept the same scoring parameters as the per-pair calls; both\n# raw-score and normalised-similarity variants are available. The\n# dispatch happens via a Python-level ThreadPoolExecutor over rows\n# because the SW / NW per-row kernels release the GIL.\nsa.cdist(qs, ts, scorer=sa.Scorer.SMITH_WATERMAN,\n         match_score=2, mismatch_score=-1, gap_score=-1)   # int64\nsa.cdist(qs, ts, scorer=sa.Scorer.SMITH_WATERMAN_NORMALIZED)  # float64 in [0, 1]\nsa.cdist(qs, ts, scorer=sa.Scorer.NEEDLEMAN_WUNSCH,\n         gap_open_score=-5, gap_extend_score=-1)            # int64, can be negative\nsa.cdist(qs, ts, scorer=sa.Scorer.NEEDLEMAN_WUNSCH_NORMALIZED)\n```\n\nAt high thresholds the pruning is dramatic — see the cross-arch\ntable in [BENCHMARK.md](https://stride-align.com/BENCHMARK.html) (the `cdist pruning` rows).\nLoongson LASX in particular flips the expected ranking against\nTiger Lake AVX-512 at T=0.99; the comparison report lives at\n[docs/loongson-vs-tiger-lake-cdist-2026-05-24.md](docs/loongson-vs-tiger-lake-cdist-2026-05-24.md).\n\nSee [BENCHMARK.md](https://stride-align.com/BENCHMARK.html) for full cross-architecture numbers.\n\n## Optimizations and Benchmarks\n\nCareful attention has been, and continues to be, paid to `stride-align`'s\nperformance story. The library includes SIMD optimization for a variety of\ncommon targets, including x86, Arm, and LoongArch.\n\n**LoongArch / Loongson.** The Loongson optimization story is especially\ntelling: for the checked benchmark case -- English text, 16-bit score width,\nscore-only Smith-Waterman -- the LASX backend is 16x faster than the generic\nbackend and **22.4x** faster than Parasail.\n\nIf you are a researcher using Loongson servers and benefiting from this\nspeedup, citations, bug reports, benchmark cases, and tiny inexpensive Chinese\nsouvenirs are appreciated. Tea, calligraphy bookmarks, paper-cut ornaments,\nChinese knot charms, panda keychains, and small dragon desk objects are all\nwelcome. Please do not send anything expensive or anything that requires\ncustoms paperwork.\n\nSee [complete benchmarks](https://stride-align.com/BENCHMARK.html).\n\n## Native Microbench\n\nFor perf profiling without Python frames or benchmark orchestration, configure a\nnative x86 microbench build:\n\n```bash\nnanobind_dir=\"$(.venv/bin/python -m nanobind --cmake_dir)\"\ncmake -S . -B build/perf \\\n  -DCMAKE_BUILD_TYPE=RelWithDebInfo \\\n  -DSTRIDE_ALIGN_BUILD_MICROBENCH=ON \\\n  -DSTRIDE_ALIGN_PERF_SYMBOLS=ON \\\n  -DPython_EXECUTABLE=.venv/bin/python \\\n  -Dnanobind_DIR=\"$nanobind_dir\"\ncmake --build build/perf --target stride_align_x86_microbench\nbuild/perf/stride_align_x86_microbench --backend avx2 --shape 1:many --pass english --width 16\npython tools/x86_microbench_regression.py \\\n  --binary build/perf/stride_align_x86_microbench \\\n  --cpu 2 \\\n  --backends avx2,avx512bwvl \\\n  --shapes 1:1,1:many \\\n  --passes english,chinese \\\n  --widths 16,32 \\\n  --write-json /tmp/stride-align-x86-microbench.json\n.venv/bin/python tools/pinned_benchmark_sweep.py \\\n  --output-dir /tmp/stride-align-pinned \\\n  --cpu 2 \\\n  --iterations 15 \\\n  --warmups 3\n```\n\n`STRIDE_ALIGN_PERF_SYMBOLS=ON` keeps nanobind modules unstripped and adds debug\nsymbols plus frame pointers while preserving `-O3`.\n\nThe checked-in native microbench baseline lives at\n`benchmarks/x86_microbench_baseline.json`. Treat it as a local guardrail with a\nloose threshold, not as a cross-machine SLA.\n\n\n## LoongArch installation\n\nLoongArch wheels are **not on PyPI** (PyPI doesn't index the\n`linux_loongarch64` platform tag), so they ship through a different\nchannel:\n\n| Channel | URL prefix |\n| --- | --- |\n| GitHub Releases (primary) | `https://github.com/adamdeprince/stride-align/releases/download/v0.5.0/` |\n| `stride-align.com` mirror | `https://stride-align.com/wheels/v0.5.0/` |\n\nSame wheels on both, pick whichever loads faster from your network.\nThe mirror is convenient when GitHub egress is slow from inside\nChina; GitHub Releases is the canonical home.\n\nInstall NumPy from your distro first (loongarch64 NumPy wheels are\nsparse on PyPI, and the distro one is usually ABI-compatible with\nthe rest of the system):\n\n```bash\nsudo apt install python3-numpy\nPY=$(python3 -c 'import sys; print(f\"cp{sys.version_info.major}{sys.version_info.minor}\")')\n```\n\n### Old-world vs new-world: what to pick\n\nLoongArch hardware runs in one of two mutually incompatible binary\n**worlds**. They differ in two things:\n\n1. **Which dynamic loader the executable references** (this is the\n   filename baked into the ELF header at link time).\n2. **Which glibc ABI version the binary depends on**.\n\nA wheel built for one world will not load on the other — the loader\nfilename doesn't exist on the wrong side, and the symbols would\nmismatch even if it did. We ship one wheel per world:\n\n| World | Loader | glibc | Typical hosts | Wheel build tag |\n| --- | --- | --- | --- | --- |\n| **Old-world** | `/lib64/ld.so.1` | 2.28-era | Stock Kylin, original Loongson distros | `1.oldworld` |\n| **New-world** | `/lib64/ld-linux-loongarch-lp64d.so.1` | ≥ 2.36 | Recent LoongArch distros, anything where the new loader has been installed | `1.newworld` |\n\nBoth wheels are statically linked against libstdc++ / libgcc so the\nonly thing separating them is the loader / glibc ABI.\n\n**Pick the right one with this one-liner**:\n\n```bash\ntest -e /lib64/ld-linux-loongarch-lp64d.so.1 \u0026\u0026 echo new-world || echo old-world\n```\n\nIf you see `new-world`, the loader is in place — use the new-world\nwheel. If you see `old-world`, either install the old-world wheel,\nor run the one-time sudo symlink below to enter new-world land\n(safe — it's a new filename, not a replacement, so existing\nold-world binaries keep working).\n\n### Old-world wheel\n\n```bash\npip install \\\n  https://github.com/adamdeprince/stride-align/releases/download/v0.5.0/stride_align-0.5.0-1.oldworld-${PY}-${PY}-linux_loongarch64.whl\n```\n\nMirror:\n\n```bash\npip install \\\n  https://stride-align.com/wheels/v0.5.0/stride_align-0.5.0-1.oldworld-${PY}-${PY}-linux_loongarch64.whl\n```\n\n### New-world wheel\n\nThe new-world wheel needs the new loader available at the path the\nELF references. One sudo step, once per box, leaves old-world\nbinaries unaffected:\n\n```bash\nsudo ln -sf /opt/loongson-gcc-16.1.0/sysroot/lib64/ld-linux-loongarch-lp64d.so.1 \\\n            /lib64/ld-linux-loongarch-lp64d.so.1\n```\n\n(Distro packagers usually drop an equivalent symlink as part of the\nnew-world transition, in which case you can skip this.)\n\nThen:\n\n```bash\npip install \\\n  https://github.com/adamdeprince/stride-align/releases/download/v0.5.0/stride_align-0.5.0-1.newworld-${PY}-${PY}-linux_loongarch64.whl\n```\n\nMirror:\n\n```bash\npip install \\\n  https://stride-align.com/wheels/v0.5.0/stride_align-0.5.0-1.newworld-${PY}-${PY}-linux_loongarch64.whl\n```\n\n### Other notes\n\nPrebuilt LoongArch64 wheels are available for Python 3.12, 3.13,\nand 3.14 — in both worlds — on both mirrors. The build details\n(toolchains, RPATH wrapper, static C++ runtime) live in\n[docs/loongson-build.md](docs/loongson-build.md). If you're on a\ndifferent Python or want to build from source, `pip install\nstride-align` falls back to the PyPI source distribution and\ncompiles the LSX/LASX kernels locally.\n\n\n## Citations\n\nIf you use my software in your research, please cite me.\n\n```bibtex\n@software{deprince_stride_align,\n  author       = {DePrince, Adam},\n  title        = {stride-align: Fast Smith-Waterman and Needleman-Wunsch alignment for Python},\n  year         = {2026},\n  publisher    = {GitHub},\n  url          = {https://github.com/adamdeprince/stride-align},\n  note         = {Python/C++ library for sequence and string alignment}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamdeprince%2Fstride-align","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamdeprince%2Fstride-align","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamdeprince%2Fstride-align/lists"}