fuzzgpu 0.1.5__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/Cargo.lock +2 -2
  2. fuzzgpu-0.1.6/PKG-INFO +402 -0
  3. fuzzgpu-0.1.6/README.md +372 -0
  4. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/Cargo.toml +1 -1
  5. fuzzgpu-0.1.6/crates/fuzzgpu-core/src/fuzz.rs +804 -0
  6. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/jaro.rs +89 -23
  7. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/levenshtein.rs +9 -0
  8. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/lib.rs +6 -1
  9. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +36 -29
  10. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +35 -25
  11. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/simd.rs +3 -3
  12. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-python/Cargo.toml +2 -2
  13. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-python/src/lib.rs +52 -62
  14. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
  15. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/pyproject.toml +1 -1
  16. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/__init__.py +24 -4
  17. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/__init__.pyi +67 -6
  18. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/Hamming.py +40 -1
  19. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/Indel.py +13 -0
  20. fuzzgpu-0.1.6/python/fuzzgpu/distance/LCSseq.py +140 -0
  21. fuzzgpu-0.1.6/python/fuzzgpu/distance/Levenshtein.py +174 -0
  22. fuzzgpu-0.1.6/python/fuzzgpu/distance/Postfix.py +41 -0
  23. fuzzgpu-0.1.6/python/fuzzgpu/distance/Prefix.py +41 -0
  24. fuzzgpu-0.1.6/python/fuzzgpu/distance/__init__.py +34 -0
  25. fuzzgpu-0.1.6/python/fuzzgpu/distance/__init__.pyi +42 -0
  26. fuzzgpu-0.1.6/python/fuzzgpu/distance/_common.py +619 -0
  27. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/fuzz.py +30 -18
  28. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/fuzz.pyi +15 -3
  29. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/process.py +4 -4
  30. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_api_signatures.py +1 -1
  31. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_edge_cases.py +3 -3
  32. fuzzgpu-0.1.6/tests/test_rapidfuzz_compat.py +213 -0
  33. fuzzgpu-0.1.5/PKG-INFO +0 -559
  34. fuzzgpu-0.1.5/README.md +0 -529
  35. fuzzgpu-0.1.5/crates/fuzzgpu-core/src/fuzz.rs +0 -322
  36. fuzzgpu-0.1.5/python/fuzzgpu/distance/Levenshtein.py +0 -111
  37. fuzzgpu-0.1.5/python/fuzzgpu/distance/__init__.py +0 -5
  38. fuzzgpu-0.1.5/python/fuzzgpu/distance/_common.py +0 -6
  39. fuzzgpu-0.1.5/tests/test_rapidfuzz_compat.py +0 -43
  40. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/Cargo.toml +0 -0
  41. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/LICENSE +0 -0
  42. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/assets/logo.png +0 -0
  43. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/benches/bench.rs +0 -0
  44. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/damerau.rs +0 -0
  45. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/gpu.rs +0 -0
  46. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/needleman.rs +0 -0
  47. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/damerau.wgsl +0 -0
  48. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/damerau_matrix.wgsl +0 -0
  49. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  50. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl +0 -0
  51. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  52. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/levenshtein_myers.wgsl +0 -0
  53. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/levenshtein_short.wgsl +0 -0
  54. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/needleman_affine.wgsl +0 -0
  55. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/src/shaders/needleman_wavefront.wgsl +0 -0
  56. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/tests/differential.proptest-regressions +0 -0
  57. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/tests/differential.rs +0 -0
  58. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/tests/fixtures/broken.wgsl +0 -0
  59. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-core/tests/kernel_registration.rs +0 -0
  60. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-wasm/src/lib.rs +0 -0
  61. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-wasm/tests/differential_harness.js +0 -0
  62. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/crates/fuzzgpu-wasm/tests/js_api.test.cjs +0 -0
  63. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/DamerauLevenshtein.py +0 -0
  64. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/Jaro.py +0 -0
  65. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/JaroWinkler.py +0 -0
  66. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/distance/OSA.py +0 -0
  67. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/python/fuzzgpu/process.pyi +0 -0
  68. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_basic.py +0 -0
  69. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_concurrency.py +0 -0
  70. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_invariants.py +0 -0
  71. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_out_buffers.py +0 -0
  72. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/test_stress.py +0 -0
  73. {fuzzgpu-0.1.5 → fuzzgpu-0.1.6}/tests/wasm_python_differential.py +0 -0
@@ -372,7 +372,7 @@ dependencies = [
372
372
 
373
373
  [[package]]
374
374
  name = "fuzzgpu-core"
375
- version = "0.1.5"
375
+ version = "0.1.6"
376
376
  dependencies = [
377
377
  "bytemuck",
378
378
  "criterion",
@@ -386,7 +386,7 @@ dependencies = [
386
386
 
387
387
  [[package]]
388
388
  name = "fuzzgpu-python"
389
- version = "0.1.5"
389
+ version = "0.1.6"
390
390
  dependencies = [
391
391
  "fuzzgpu-core",
392
392
  "log",
fuzzgpu-0.1.6/PKG-INFO ADDED
@@ -0,0 +1,402 @@
1
+ Metadata-Version: 2.4
2
+ Name: fuzzgpu
3
+ Version: 0.1.6
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Rust
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Classifier: Topic :: Text Processing :: General
15
+ Requires-Dist: pytest>=7.0 ; extra == 'test'
16
+ Requires-Dist: pytest-benchmark ; extra == 'test'
17
+ Provides-Extra: test
18
+ License-File: LICENSE
19
+ Summary: Hardware-accelerated fuzzy string matching & sequence alignment — cross-platform, no CUDA required
20
+ Keywords: gpu,fuzzy,string-matching,levenshtein,damerau,jaro-winkler,needleman-wunsch,webgpu
21
+ Home-Page: https://github.com/kuntal-devrat/fuzzgpu
22
+ Author: Devrat Kuntal
23
+ License: MIT
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
26
+ Project-URL: Homepage, https://github.com/kuntal-devrat/fuzzgpu
27
+ Project-URL: Issues, https://github.com/kuntal-devrat/fuzzgpu/issues
28
+ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
29
+
30
+ <div align="center">
31
+
32
+ <img src="https://raw.githubusercontent.com/kuntal-devrat/fuzzgpu/main/assets/logo.png" alt="fuzzgpu logo" width="140" height="140" />
33
+
34
+ # fuzzgpu
35
+
36
+ **Hardware-Accelerated Fuzzy String Matching & Sequence Alignment**
37
+
38
+ *Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
39
+
40
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.6-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
42
+ [![Rust](https://img.shields.io/badge/rust-1.87+-orange.svg?style=flat-square)](https://www.rust-lang.org)
43
+ [![Cross Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20WASM-lightgrey.svg?style=flat-square)](https://github.com/kuntal-devrat/fuzzgpu)
44
+
45
+ </div>
46
+
47
+ ---
48
+
49
+ ## Overview
50
+
51
+ `fuzzgpu` is a high-throughput string distance and sequence alignment engine written in **Rust** with native **Python** and **WebAssembly** bindings. It leverages GPU compute shaders (`wgpu` / WGSL) and Rayon multi-threading to accelerate large-scale batch queries and distance matrix computations across:
52
+
53
+ - **Apple Silicon (Metal)**
54
+ - **Linux (Vulkan)**
55
+ - **Windows (DirectX 12 / Vulkan)**
56
+ - **Integrated GPUs (Intel Iris Xe, AMD Radeon)**
57
+ - **WebAssembly (In-browser execution)**
58
+
59
+ No NVIDIA CUDA drivers or complex toolkits required.
60
+
61
+ ---
62
+
63
+ ## What's New in v0.1.6
64
+
65
+ ### Drop-in rapidfuzz parity (Python)
66
+ The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-pair differential harness across `ratio`, `partial_ratio`, `token_sort_ratio`, `token_set_ratio`, `token_ratio`, `WRatio`, `QRatio`, `partial_token_*`, `jaro`, `jaro_winkler`, `levenshtein`, `indel`, `hamming`, `osa` — **0 mismatches**.
67
+
68
+ ### Bug fixes (Rust core, float parity)
69
+ - **`ratio` / `partial_ratio` cutoff imprecision** — port of rapidfuzz's load-bearing `NormSim_to_NormDist = min(1, 1 - cutoff/100 + 1e-5)` term. Without it, the branch-and-bound would silently reject ties at exact cutoffs (e.g. `partial_ratio("park", "ba", score_cutoff=50.0)` returned `0` instead of `50`).
70
+ - **`ratio` score formula** — switched from `((len-dist)/len)*100` to `(1 - dist/len)*100` to match rapidfuzz C++'s exact ulp order (`indel_normalized_similarity * 100`).
71
+
72
+ ### New features (Python distance layer)
73
+ - **`Editops` / `Opcodes` / `Editop` / `Opcode` / `MatchingBlock` / `ScoreAlignment`** classes (rapidfuzz-compatible list-/tuple-likes with `as_list`, `as_opcodes`, `as_editops`, `as_matching_blocks`, `apply`, `inverse`, `remove_subsequence`, `from_*`).
74
+ - **`Levenshtein.editops` / `.opcodes`** — exact Myers bit-parallel port with `common_affix` (suffix measured on post-prefix slice, matching rapidfuzz).
75
+ - **`LCSseq`** module — Myers LCS bit-parallel matrix + editops/opcodes (delete-checked-first traceback).
76
+ - **`Prefix`** and **`Postfix`** modules with the C++ `1 - dist/maximum` ulp order.
77
+ - **`Hamming.editops` / `.opcodes`** — replace-per-mismatch + padding delete/insert model.
78
+ - **`Indel.editops` / `.opcodes`** — delegates to `LCSseq` (matching rapidfuzz C++).
79
+ - **`fuzz.partial_ratio_alignment`** now returns `ScoreAlignment(score, src_start, src_end, dest_start, dest_end)` — drop-in compatible with rapidfuzz.
80
+ - **`process.extract` / `extractOne` / `cdist`** default to `WRatio` (matches rapidfuzz 3.14.5).
81
+ - **`token_ratio` / `partial_token_ratio`** exposed at top level (`fuzzgpu.token_ratio`, `fuzzgpu.partial_token_ratio`).
82
+ - All alignment types re-exported at the package root (`fuzzgpu.Editop`, `fuzzgpu.Editops`, etc.).
83
+
84
+ ### Type stubs
85
+ - `__init__.pyi`, `fuzz.pyi`, `process.pyi`, `distance/__init__.pyi` updated for the new APIs.
86
+
87
+ ---
88
+
89
+ <details>
90
+ <summary><b>Previous (v0.1.5</b</summary>
91
+
92
+ ### Bug fixes
93
+ - **Damerau-Levenshtein safety gate** now fires in release builds (`assert!` not `debug_assert!`) — non-ASCII inputs no longer silently produce wrong distances in production wheels
94
+ - **Needleman-Wunsch GPU f32 precision guard** — scoring parameters that exceed the exact f32 integer range (2²⁴ = 16,777,216) now automatically route to CPU, preventing silent precision loss
95
+ - **Wavefront shader race condition** fixed — `diags[1]` seed initialization consolidated into a single thread with a proper `workgroupBarrier()`
96
+ - **`extract_one` early-exit** fixed — the `break` at score==100.0 now only fires after the `is_better` check
97
+ - **Distance module processor bug** fixed across all `distance/*.py` modules — `similarity`/`normalized_*` now apply the processor once, then compute `maximum` on the processed strings
98
+
99
+ ### Optimizations
100
+ - **Zero-allocation SIMD hot paths** — `levenshtein_cdist`, `levenshtein_batch`, and `jaro_winkler_batch` now use stack-allocated `[&[u8]; 8]` instead of per-group heap `Vec`, eliminating millions of tiny allocations at 1M-cell matrix scale
101
+ - **`token_set_ratio`** uses `Cow<str>` to skip heap allocation when intersection/difference sets are empty
102
+ - **`process.cdist`** fast path routes through the Rayon/GPU `ratio_batch` when the default scorer is used, instead of one Python call per cell
103
+
104
+ ### New features
105
+ - `partial_ratio_alignment(s1, s2)` → `(score, src_start, dest_start, length)` — rapidfuzz-compatible alignment result
106
+ - `partial_token_sort_ratio`, `partial_token_set_ratio`, `QRatio` — now exposed at the top level
107
+ - **Jaro-Winkler GPU routing in Python** — `jaro_winkler_batch` and `jaro_winkler_cdist` now use the GPU kernel on discrete GPUs
108
+ - **Needleman-Wunsch GPU routing in Python** — `needleman_wunsch_affine_batch` now uses `GpuNeedlemanAffineKernel`
109
+ - `editops` and `opcodes` re-exported at the top level (`fuzzgpu.editops`, `fuzzgpu.opcodes`)
110
+ - Complete type stubs (`__init__.pyi`, `fuzz.pyi`, `process.pyi`)
111
+
112
+ </details>
113
+
114
+ ---
115
+
116
+ ## Benchmark Results
117
+
118
+ *Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores)*
119
+ *Versions: fuzzgpu 0.1.6 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
120
+ *Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
121
+
122
+ ### Levenshtein Batch (1 query × N candidates, 10-char strings)
123
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
124
+ | :--- | :---: | :---: | :---: | :---: | :---: |
125
+ | **100** | 0.12 ms | 0.01 ms | 0.03 ms | 0.25× | 2.21× |
126
+ | **1,000** | 0.83 ms | 0.09 ms | 0.15 ms | 0.18× | 1.67× |
127
+ | **10,000** | 2.60 ms | 0.87 ms | 1.47 ms | 0.57× | 1.68× |
128
+ | **50,000** | 9.92 ms | 5.04 ms | 6.38 ms | 0.64× | 1.27× |
129
+
130
+ ### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
131
+ | Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
132
+ | :--- | :---: | :---: | :---: | :---: | :---: |
133
+ | **1,000** | 0.58 ms | 0.40 ms | 1.63 ms | 2.81× | 4.06× |
134
+ | **10,000** | 2.95 ms | 2.15 ms | 20.86 ms | 7.06× | 9.70× |
135
+ | **50,000** | 17.49 ms | 12.08 ms | 120.25 ms | 6.88× | 9.95× |
136
+
137
+ > **Note:** rapidfuzz's `DamerauLevenshtein` uses Optimal String Alignment (OSA). fuzzgpu implements the **unrestricted** Lowrance-Wagner (1975) algorithm which allows non-adjacent transpositions. For example: `damerau("ca", "abc") == 2` (fuzzgpu) vs `3` (rapidfuzz OSA). Use `fuzzgpu.distance.OSA` for OSA-compatible semantics.
138
+
139
+ ---
140
+
141
+ ## Installation
142
+
143
+ ```bash
144
+ pip install fuzzgpu
145
+ ```
146
+
147
+ ```toml
148
+ # Rust
149
+ [dependencies]
150
+ fuzzgpu-core = "0.1.6"
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Quickstart
156
+
157
+ ```python
158
+ import fuzzgpu
159
+
160
+ # ── Core distance metrics ─────────────────────────────────────────────────────
161
+ lev = fuzzgpu.levenshtein("kitten", "sitting") # 3
162
+ dam = fuzzgpu.damerau("ab", "ba") # 1 (transposition)
163
+ jw = fuzzgpu.jaro_winkler("MARTHA", "MARHTA") # 0.9611...
164
+
165
+ # ── Batch (auto-routed GPU/CPU) ───────────────────────────────────────────────
166
+ candidates = ["hallo", "hullo", "jello", "yellow", "hello world"] * 10_000
167
+ distances = fuzzgpu.levenshtein_batch("hello", candidates)
168
+ jw_scores = fuzzgpu.jaro_winkler_batch("hello", candidates, p=0.1)
169
+ nw_scores = fuzzgpu.needleman_wunsch_affine_batch(
170
+ "AGTACGCA", candidates, match=2, mismatch=-1, gap_open=-3, gap_extend=-1
171
+ )
172
+
173
+ # ── Cross-product distance matrix ─────────────────────────────────────────────
174
+ matrix = fuzzgpu.levenshtein_cdist(["abc", "def", "xyz"], ["abd", "axy", "def"])
175
+
176
+ # ── Zero-allocation outputs (write into preallocated numpy arrays) ────────────
177
+ import numpy as np
178
+ out_u32 = np.empty(len(candidates), dtype=np.uint32)
179
+ out_f64 = np.empty(len(candidates), dtype=np.float64)
180
+ mat_u32 = np.empty((3, 3), dtype=np.uint32)
181
+ fuzzgpu.levenshtein_batch_into("hello", candidates, out_u32)
182
+ fuzzgpu.jaro_winkler_batch_into("hello", candidates, out_f64)
183
+ fuzzgpu.levenshtein_cdist_into(["abc", "def", "xyz"], ["abd", "axy", "def"], mat_u32)
184
+
185
+ # ── Global sequence alignment (Gotoh 1982 affine gap) ────────────────────────
186
+ score = fuzzgpu.needleman_wunsch_affine("AGTACGCA", "TATGC", 2, -1, -3, -1)
187
+
188
+ # ── Fuzzy ratios (rapidfuzz-compatible) ──────────────────────────────────────
189
+ from fuzzgpu.fuzz import (
190
+ ratio, partial_ratio, partial_ratio_alignment,
191
+ token_sort_ratio, token_set_ratio,
192
+ partial_token_sort_ratio, partial_token_set_ratio,
193
+ QRatio, WRatio,
194
+ )
195
+
196
+ ratio("fuzzy was a bear", "fuzzy was a bear") # 100.0
197
+ partial_ratio("hello", "oh hello there") # 100.0
198
+ score, src, dst, length = partial_ratio_alignment("hello", "oh hello there")
199
+ # (100.0, 0, 3, 5) ← window starts at char 3 of the longer string
200
+
201
+ token_sort_ratio("new york mets", "mets new york") # 100.0
202
+ token_set_ratio("fuzzy was a bear", "fuzzy bear") # 100.0
203
+
204
+ # ── Alignment helpers (rapidfuzz-compatible) ──────────────────────────────────
205
+ from fuzzgpu.distance import Levenshtein
206
+ ops = fuzzgpu.editops("kitten", "sitting") # top-level alias
207
+ codes = fuzzgpu.opcodes("kitten", "sitting")
208
+
209
+ # ── Search ────────────────────────────────────────────────────────────────────
210
+ from fuzzgpu.fuzz import extract, extractOne
211
+ best = extractOne("hellp", ["hello", "world", "help"], score_cutoff=50.0)
212
+ # ("help", 88.88888888888889, 2)
213
+ top_3 = extract("apple", ["apply", "ape", "banana", "applesauce"],
214
+ score_cutoff=50.0, limit=3)
215
+
216
+ # ── rapidfuzz.process-compatible API ─────────────────────────────────────────
217
+ from fuzzgpu.process import extract, extractOne, cdist
218
+ matrix = cdist(["hello", "world"], ["hallo", "wurld"]) # uses GPU/Rayon
219
+
220
+ # ── distance submodule (rapidfuzz.distance-compatible) ───────────────────────
221
+ from fuzzgpu.distance import Levenshtein, DamerauLevenshtein, Jaro, JaroWinkler
222
+ from fuzzgpu.distance import Hamming, OSA, Indel
223
+
224
+ Levenshtein.distance("kitten", "sitting") # 3
225
+ Levenshtein.normalized_similarity("kitten", "sitting") # 0.571...
226
+ Levenshtein.similarity(" abc ", "abc", processor=str.strip) # 3
227
+ DamerauLevenshtein.distance("ca", "abc") # 2 (unrestricted)
228
+ OSA.distance("ca", "abc") # 3 (OSA / rapidfuzz-compatible)
229
+ JaroWinkler.similarity("MARTHA", "MARHTA", prefix_weight=0.1) # 0.9611...
230
+
231
+ # ── Hardware diagnostics ──────────────────────────────────────────────────────
232
+ print(fuzzgpu.gpu_info()) # "Intel(R) Iris(R) Xe Graphics (Vulkan)"
233
+ print(fuzzgpu.hardware_info()) # adapter, threshold, last routing stats
234
+
235
+ fuzzgpu.set_gpu_threshold(100) # force GPU for batches >= 100 pairs
236
+ fuzzgpu.set_gpu_threshold(None) # restore auto-selection
237
+ fuzzgpu.set_cpu_only(True) # force CPU-only mode
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Rust API
243
+
244
+ ```toml
245
+ [dependencies]
246
+ fuzzgpu-core = "0.1.6" # GPU + CPU fallback
247
+ # fuzzgpu-core = { version = "0.1.6", default-features = false } # CPU-only
248
+ ```
249
+
250
+ ```rust
251
+ use fuzzgpu_core::levenshtein::gpu_ext::GpuLevenshteinKernel;
252
+
253
+ fn main() -> fuzzgpu_core::Result<()> {
254
+ let kernel = GpuLevenshteinKernel::get()?;
255
+
256
+ // Batch
257
+ let pairs = vec![("kitten", "sitting"), ("hello", "hullo")];
258
+ let distances = kernel.compute(&pairs)?; // [3, 1]
259
+
260
+ // Cross-product matrix
261
+ let matrix = kernel.compute_matrix(&["abc", "def"], &["abc", "xyz"])?;
262
+
263
+ // Multi-op batch (one GPU dispatch + readback amortized across all ops)
264
+ let mut batch = kernel.batch();
265
+ batch.add(&pairs);
266
+ batch.add(&[("foo", "bar"), ("test", "taste")]);
267
+ let results = batch.execute()?; // Vec<Vec<u32>>
268
+ Ok(())
269
+ }
270
+ ```
271
+
272
+ Available GPU kernels: `GpuLevenshteinKernel`, `GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`.
273
+
274
+ ---
275
+
276
+ ## WebAssembly
277
+
278
+ ```bash
279
+ cd crates/fuzzgpu-wasm
280
+ wasm-pack build --target web --release
281
+ ```
282
+
283
+ ```js
284
+ import init, {
285
+ levenshtein_distance, jaro_winkler, ratio, extract,
286
+ needleman_wunsch, needleman_wunsch_affine,
287
+ } from './pkg/fuzzgpu_wasm.js';
288
+ await init();
289
+
290
+ levenshtein_distance('kitten', 'sitting'); // 3
291
+ jaro_winkler('MARTHA', 'MARHTA', 0.1); // 0.9611...
292
+
293
+ // Needleman-Wunsch scores are i64 → JavaScript BigInt
294
+ needleman_wunsch('AGTACGCA', 'TATGC', 2n, -1n, -2n); // 1n
295
+ needleman_wunsch_affine('AGTACGCA', 'TATGC', 2n, -1n, -3n, -1n); // -2n
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Technical Architecture
301
+
302
+ ### Execution pipeline
303
+
304
+ ```
305
+ ┌─────────────────────────┐
306
+ │ User Query / API │
307
+ └────────────┬────────────┘
308
+ │
309
+ Batch size / dataset assessment
310
+ │
311
+ ┌──────────────────────┴──────────────────────┐
312
+ ▼ ▼
313
+ Small batches (< threshold) Large batches (≥ threshold)
314
+ │ │
315
+ ┌───────────────────┐ ┌─────────────────────────────┐
316
+ │ Rayon Parallel │ │ wgpu WebGPU Compute │
317
+ │ Myers bit-vector │ │ WGSL shaders │
318
+ │ AVX512/AVX2/NEON │ │ Metal / Vulkan / DX12 │
319
+ └───────────────────┘ └─────────────────────────────┘
320
+ ```
321
+
322
+ ### Key design points
323
+
324
+ - **Myers (1999) bit-vector** — O(n) Levenshtein for patterns ≤ 64 chars, zero inner DP loop. Vectorized with AVX512 (8 texts/vector), AVX2 (4), NEON (2), portable scalar fallback. ISA detected at runtime via cached CPUID; override with `FUZZGPU_SIMD=portable|neon|avx2|avx512`.
325
+ - **Unrestricted Damerau-Levenshtein** — Full Lowrance-Wagner (1975) with non-adjacent transpositions. GPU shader keeps the full DP matrix in workgroup shared memory (≤ 32 chars ASCII).
326
+ - **Gotoh (1982) affine gaps** — 3-state recurrence, O(n) memory. GPU shader computes in f32; automatically routes to CPU when scoring parameters exceed f32 exact range (2²⁴).
327
+ - **WGSL shaders require no adapter features** — bit-vectors implemented as u32×2 pairs (no `SHADER_INT64`), works on every WebGPU backend including browsers and integrated GPUs.
328
+ - **Metric-aware routing** — iGPUs auto-route Jaro/Damerau to CPU (where AVX2 SIMD wins); discrete GPUs dispatch above a scaled threshold. `hardware_info()` shows every routing decision.
329
+ - **Dispatch lock** — serializes GPU calls across threads to work around `gfx-rs/wgpu#10085` (heap corruption under ≥3 concurrent dispatchers on Intel iGPUs).
330
+ - **Zero-copy Python bindings** — `Bound<PyString>` pointers, no `Vec<String>` copies; `*_into` APIs write directly into caller-supplied numpy arrays.
331
+
332
+ ### GPU kernels
333
+
334
+ | Kernel | Algorithm | Max length | Notes |
335
+ |--------|-----------|------------|-------|
336
+ | `levenshtein.wgsl` | Standard DP | 256 chars | General path |
337
+ | `levenshtein_short.wgsl` | SLM row DP | 64 chars | Transposed layout, no register spill |
338
+ | `levenshtein_myers.wgsl` | Myers bit-vector | 64 chars | Shared Peq per workgroup, 2×u32 bitmask |
339
+ | `levenshtein_cdist_myers.wgsl` | Row-wise Myers | 64 chars | One workgroup per matrix row |
340
+ | `levenshtein_matrix.wgsl` | 2D DP grid | 256 chars | O(N+M) data upload |
341
+ | `jaro.wgsl` | Bitmap matcher | 128 chars | 128-bit registers, transposed layout |
342
+ | `jaro_matrix.wgsl` | 2D Jaro grid | 128 chars | O(N+M) data upload |
343
+ | `damerau.wgsl` | Lowrance-Wagner | 32 chars ASCII | Full matrix in SLM, non-adjacent transpositions |
344
+ | `damerau_matrix.wgsl` | 2D Damerau grid | 32 chars ASCII | Same |
345
+ | `needleman_affine.wgsl` | Gotoh serial | 128 chars | f32 scores, one thread per pair |
346
+ | `needleman_wavefront.wgsl` | Gotoh wavefront | 128 chars | Anti-diagonal parallel, O(m+n) steps |
347
+
348
+ ---
349
+
350
+ ## Project Structure
351
+
352
+ ```
353
+ fuzzgpu/
354
+ ├── crates/
355
+ │ ├── fuzzgpu-core/ # Core Rust engine + GPU shaders
356
+ │ ├── fuzzgpu-python/ # PyO3 Python extension
357
+ │ └── fuzzgpu-wasm/ # wasm-bindgen WebAssembly module
358
+ ├── python/fuzzgpu/ # Python package wrapper + type stubs
359
+ │ ├── distance/ # rapidfuzz.distance-compatible modules
360
+ │ ├── fuzz.py # rapidfuzz.fuzz-compatible scorers
361
+ │ └── process.py # rapidfuzz.process-compatible helpers
362
+ ├── fuzz/ # libFuzzer targets + stable self-harness
363
+ ├── benchmarks/ # Comparative benchmark scripts
364
+ ├── tests/ # Python pytest suite (174 tests)
365
+ └── docs/GPU_TESTING.md # Fault injection & GPU test conventions
366
+ ```
367
+
368
+ ---
369
+
370
+ ## Building from Source
371
+
372
+ ```bash
373
+ # Prerequisites: Rust 1.87+, Python 3.10+, maturin
374
+ git clone https://github.com/kuntal-devrat/fuzzgpu.git
375
+ cd fuzzgpu
376
+ maturin develop --release
377
+ pytest tests/ -v
378
+ cargo test --workspace
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Environment Variables
384
+
385
+ | Variable | Effect |
386
+ |----------|--------|
387
+ | `FUZZGPU_USE_CPU` | Force CPU-only mode |
388
+ | `FUZZGPU_FORCE_GPU` | Error (not fallback) on GPU failure in Python |
389
+ | `FUZZGPU_DEBUG` | Log GPU→CPU fallback decisions |
390
+ | `FUZZGPU_SIMD` | Force ISA: `portable\|neon\|avx2\|avx512` |
391
+ | `FUZZGPU_READBACK_TIMEOUT_MS` | GPU readback timeout (default 10000 ms) |
392
+ | `FUZZGPU_SKIP_DISPATCH_LOCK` | Bypass serialization lock (repro only) |
393
+ | `FUZZGPU_REQUIRE_GPU` | In tests: fail instead of skip when no GPU |
394
+ | `WGPU_BACKEND` | Force wgpu backend: `vulkan\|metal\|dx12` |
395
+ | `PROPTEST_CASES` | Override proptest case count |
396
+
397
+ ---
398
+
399
+ ## License
400
+
401
+ [MIT](LICENSE)
402
+