fuzzgpu 0.1.7__tar.gz → 0.1.8__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.
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/Cargo.lock +2 -2
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/PKG-INFO +87 -28
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/README.md +86 -27
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/Cargo.toml +1 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/damerau.rs +3 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/jaro.rs +3 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/levenshtein.rs +5 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/needleman.rs +3 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/simd.rs +19 -6
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-python/Cargo.toml +2 -2
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-wasm/Cargo.toml +1 -1
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/pyproject.toml +1 -1
- fuzzgpu-0.1.8/python/fuzzgpu/distance/DamerauLevenshtein.py +145 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Jaro.py +3 -3
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/JaroWinkler.py +3 -3
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/fuzz.py +54 -3
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/process.py +21 -14
- fuzzgpu-0.1.7/python/fuzzgpu/distance/DamerauLevenshtein.py +0 -43
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/Cargo.toml +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/LICENSE +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/assets/logo.png +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/benches/bench.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/fuzz.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/gpu.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/lib.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/damerau.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/damerau_matrix.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein_myers.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein_short.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/needleman_affine.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/needleman_wavefront.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/tests/differential.proptest-regressions +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/tests/differential.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/tests/fixtures/broken.wgsl +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/tests/kernel_registration.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-python/src/lib.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-wasm/src/lib.rs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-wasm/tests/differential_harness.js +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-wasm/tests/js_api.test.cjs +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/__init__.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/__init__.pyi +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Hamming.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Indel.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/LCSseq.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Levenshtein.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/OSA.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Postfix.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/Prefix.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/__init__.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/__init__.pyi +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/distance/_common.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/fuzz.pyi +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/python/fuzzgpu/process.pyi +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_api_signatures.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_basic.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_concurrency.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_edge_cases.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_invariants.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_out_buffers.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_rapidfuzz_compat.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/tests/test_stress.py +0 -0
- {fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/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.
|
|
375
|
+
version = "0.1.8"
|
|
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.
|
|
389
|
+
version = "0.1.8"
|
|
390
390
|
dependencies = [
|
|
391
391
|
"fuzzgpu-core",
|
|
392
392
|
"log",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuzzgpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -37,7 +37,7 @@ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
|
|
|
37
37
|
|
|
38
38
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
39
39
|
|
|
40
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
41
41
|
[](https://opensource.org/licenses/MIT)
|
|
42
42
|
[](https://www.rust-lang.org)
|
|
43
43
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -60,28 +60,60 @@ No NVIDIA CUDA drivers or complex toolkits required.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
## What's New in v0.1.
|
|
63
|
+
## What's New in v0.1.8
|
|
64
|
+
|
|
65
|
+
### Production hardening
|
|
66
|
+
- **Kernel `get()` panics eliminated** — all four GPU kernels (`GpuLevenshteinKernel`,
|
|
67
|
+
`GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`) previously called
|
|
68
|
+
`.unwrap()` on `OnceLock::get()` after initialization, which could panic the Python
|
|
69
|
+
interpreter under rare concurrent races. Replaced with `.ok_or_else(...)` returning
|
|
70
|
+
a proper `FuzzGpuError::NoDevice`.
|
|
71
|
+
- **`debug_assert!` → real release guards** — `MyersPattern::new`, `jaro_bitpar`, `jaro_4way`
|
|
72
|
+
were guarded only by `debug_assert!`. In release builds, calling them with inputs outside
|
|
73
|
+
their contract (non-ASCII or > 64 bytes) would silently produce wrong results. Promoted to
|
|
74
|
+
proper `assert!` with descriptive messages that surface immediately in both debug and release.
|
|
75
|
+
|
|
76
|
+
### New API surface
|
|
77
|
+
- **`fuzz.cdist`** — pairwise score matrix, mirrors `rapidfuzz.fuzz.cdist`. Delegates to
|
|
78
|
+
`process.cdist` with `ratio` as the default scorer.
|
|
79
|
+
- **`DamerauLevenshtein.editops` / `.opcodes`** — full Lowrance-Wagner traceback returning
|
|
80
|
+
`Editops` / `Opcodes` (insert/delete/replace), completing parity with rapidfuzz's alignment
|
|
81
|
+
API for this module.
|
|
82
|
+
- **`fuzz.__all__`** now includes `partial_ratio_alignment` and `cdist` (were missing).
|
|
83
|
+
|
|
84
|
+
### API correctness fixes
|
|
85
|
+
- **`Jaro.similarity` / `JaroWinkler.similarity` `score_cutoff`** — changed default from
|
|
86
|
+
`0.0` to `None`, matching rapidfuzz's semantics (`0.0` treated scores of exactly 0.0 as
|
|
87
|
+
filtered, which was wrong).
|
|
88
|
+
- **`ratio_batch(workers=)`** — was silently ignored (`del workers`). Now wires up a
|
|
89
|
+
`ThreadPoolExecutor` for the processor path; the no-processor path continues to use Rayon
|
|
90
|
+
internally (ignoring `workers` is correct there — Rayon already uses all cores).
|
|
91
|
+
- **`process.cdist` fast path** — rewrote to use `fuzz_ratio_batch` row-by-row (each row
|
|
92
|
+
runs under Rayon across all cores) instead of a dead `raw = _native.fuzz_ratio_batch`
|
|
93
|
+
assignment followed by the same loop. Removed the dead `raw:` type-hint-only line.
|
|
94
|
+
- **`process.cdist` silent swallow** — `except Exception: pass` replaced with
|
|
95
|
+
`warnings.warn(...)` so unexpected fast-path failures are visible instead of silently
|
|
96
|
+
producing slow results.
|
|
97
|
+
|
|
98
|
+
### Includes all v0.1.7 fixes
|
|
99
|
+
All fixes from v0.1.7 are included — see the v0.1.7 changelog below.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
<details>
|
|
104
|
+
<summary><b>Previous (v0.1.7)</b></summary>
|
|
64
105
|
|
|
65
106
|
### Bug fixes (Windows DX12 / Jaro shader)
|
|
66
107
|
- **Jaro GPU shader FXC crash fixed** — `jaro.wgsl` and `jaro_matrix.wgsl` used dynamic vector
|
|
67
|
-
component writes (`v[j >> 5u] = ...`) in `bit_set()`.
|
|
68
|
-
|
|
69
|
-
with `X3550`/`X3511` on every Windows DX12 runner — all 11 Jaro GPU tests were panicking.
|
|
70
|
-
Fixed by rewriting `bit_set` with `select()`-based static construction, which compiles
|
|
71
|
-
identically on all backends (Vulkan, Metal, DX12).
|
|
108
|
+
component writes (`v[j >> 5u] = ...`) in `bit_set()`. Fixed by rewriting `bit_set` with
|
|
109
|
+
`select()`-based static construction (Vulkan, Metal, DX12 all compile identically).
|
|
72
110
|
|
|
73
111
|
### Bug fixes (arity mismatch — wasm & fuzz crates)
|
|
74
|
-
- **`fuzzgpu-wasm`** —
|
|
75
|
-
|
|
76
|
-
the
|
|
77
|
-
`0.0` as the cutoff (no cutoff — identical to the previous behaviour).
|
|
78
|
-
- **`fuzzgpu-fuzz`** — same four callsites in `fuzz/src/lib.rs` had the same arity mismatch.
|
|
79
|
-
Fixed identically.
|
|
80
|
-
|
|
81
|
-
### Includes all v0.1.6 features
|
|
82
|
-
All features from v0.1.6 are included — see the v0.1.6 changelog below.
|
|
112
|
+
- **`fuzzgpu-wasm`** and **`fuzzgpu-fuzz`** — 4 × `E0061` arity mismatch for
|
|
113
|
+
`partial_ratio`/`token_sort_ratio`/`token_set_ratio`/`wratio`. Fixed by passing `0.0`
|
|
114
|
+
as the cutoff.
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
</details>
|
|
85
117
|
|
|
86
118
|
<details>
|
|
87
119
|
<summary><b>Previous (v0.1.6)</b></summary>
|
|
@@ -132,26 +164,53 @@ The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-p
|
|
|
132
164
|
## Benchmark Results
|
|
133
165
|
|
|
134
166
|
*Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores)*
|
|
135
|
-
*Versions: fuzzgpu 0.1.
|
|
167
|
+
*Versions: fuzzgpu 0.1.8 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
|
|
136
168
|
*Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
|
|
137
169
|
|
|
138
170
|
### Levenshtein Batch (1 query × N candidates, 10-char strings)
|
|
139
171
|
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
140
172
|
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
141
|
-
| **100** | 0.
|
|
142
|
-
| **1,000** | 0.
|
|
143
|
-
| **10,000** |
|
|
144
|
-
| **50,000** |
|
|
173
|
+
| **100** | 0.04 ms | 0.00 ms | 0.01 ms | 0.24× | 1.90× |
|
|
174
|
+
| **1,000** | 0.50 ms | 0.04 ms | 0.07 ms | 0.13× | 1.89× |
|
|
175
|
+
| **10,000** | 1.53 ms | 0.40 ms | 0.64 ms | 0.42× | 1.61× |
|
|
176
|
+
| **50,000** | 6.87 ms | 3.36 ms | 4.46 ms | 0.65× | 1.33× |
|
|
145
177
|
|
|
146
178
|
### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
|
|
147
179
|
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
148
180
|
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
149
|
-
| **
|
|
150
|
-
| **
|
|
151
|
-
| **
|
|
181
|
+
| **100** | 0.04 ms | 0.04 ms | 0.10 ms | 2.68× | 2.63× |
|
|
182
|
+
| **1,000** | 0.21 ms | 0.20 ms | 0.98 ms | 4.71× | 4.89× |
|
|
183
|
+
| **10,000** | 1.39 ms | 1.49 ms | 9.93 ms | 7.14× | 6.67× |
|
|
184
|
+
| **50,000** | 10.19 ms | 9.91 ms | 66.57 ms | 6.54× | 6.72× |
|
|
152
185
|
|
|
153
186
|
> **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.
|
|
154
187
|
|
|
188
|
+
### Jaro-Winkler Batch (p = 0.1)
|
|
189
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
190
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
191
|
+
| **100** | 0.02 ms | 0.01 ms | 0.02 ms | 0.87× | 1.14× |
|
|
192
|
+
| **1,000** | 0.18 ms | 0.13 ms | 0.12 ms | 0.70× | 0.94× |
|
|
193
|
+
| **10,000** | 1.01 ms | 0.83 ms | 0.97 ms | 0.96× | 1.18× |
|
|
194
|
+
| **50,000** | 5.61 ms | 3.25 ms | 6.69 ms | 1.19× | 2.06× |
|
|
195
|
+
|
|
196
|
+
### Needleman-Wunsch Affine Batch (match=1, mismatch=-1, gap_open=-2, gap_extend=-1)
|
|
197
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` |
|
|
198
|
+
| :--- | :---: | :---: | :---: |
|
|
199
|
+
| **100** | 0.07 ms | 0.05 ms | — |
|
|
200
|
+
| **1,000** | 1.65 ms | 0.43 ms | — |
|
|
201
|
+
| **10,000** | 12.49 ms | 3.91 ms | — |
|
|
202
|
+
| **50,000** | 47.86 ms | 26.95 ms | — |
|
|
203
|
+
|
|
204
|
+
> rapidfuzz has no Needleman-Wunsch API — no comparison available.
|
|
205
|
+
|
|
206
|
+
### Levenshtein Cross-Product Matrix (`cdist`)
|
|
207
|
+
| Matrix Size | Total Pairs | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | python-Levenshtein | vs RF (GPU) | vs RF (CPU) |
|
|
208
|
+
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
|
209
|
+
| **10 × 10** | 100 | 0.05 ms | 0.02 ms | 0.01 ms | 0.06 ms | 0.18× | 0.56× |
|
|
210
|
+
| **50 × 50** | 2,500 | 0.58 ms | 0.11 ms | 0.09 ms | 2.17 ms | 0.16× | 0.81× |
|
|
211
|
+
| **100 × 100** | 10,000 | 0.67 ms | 0.25 ms | 0.18 ms | 5.75 ms | 0.27× | 0.70× |
|
|
212
|
+
| **200 × 200** | 40,000 | 1.19 ms | 0.81 ms | 0.61 ms | 22.31 ms | 0.51× | 0.75× |
|
|
213
|
+
|
|
155
214
|
---
|
|
156
215
|
|
|
157
216
|
## Installation
|
|
@@ -163,7 +222,7 @@ pip install fuzzgpu
|
|
|
163
222
|
```toml
|
|
164
223
|
# Rust
|
|
165
224
|
[dependencies]
|
|
166
|
-
fuzzgpu-core = "0.1.
|
|
225
|
+
fuzzgpu-core = "0.1.8"
|
|
167
226
|
```
|
|
168
227
|
|
|
169
228
|
---
|
|
@@ -259,7 +318,7 @@ fuzzgpu.set_cpu_only(True) # force CPU-only mode
|
|
|
259
318
|
|
|
260
319
|
```toml
|
|
261
320
|
[dependencies]
|
|
262
|
-
fuzzgpu-core = "0.1.
|
|
321
|
+
fuzzgpu-core = "0.1.8" # GPU + CPU fallback
|
|
263
322
|
# fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
|
|
264
323
|
```
|
|
265
324
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
10
10
|
|
|
11
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](https://www.rust-lang.org)
|
|
14
14
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -31,28 +31,60 @@ No NVIDIA CUDA drivers or complex toolkits required.
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
-
## What's New in v0.1.
|
|
34
|
+
## What's New in v0.1.8
|
|
35
|
+
|
|
36
|
+
### Production hardening
|
|
37
|
+
- **Kernel `get()` panics eliminated** — all four GPU kernels (`GpuLevenshteinKernel`,
|
|
38
|
+
`GpuJaroKernel`, `GpuNeedlemanAffineKernel`, `GpuDamerauKernel`) previously called
|
|
39
|
+
`.unwrap()` on `OnceLock::get()` after initialization, which could panic the Python
|
|
40
|
+
interpreter under rare concurrent races. Replaced with `.ok_or_else(...)` returning
|
|
41
|
+
a proper `FuzzGpuError::NoDevice`.
|
|
42
|
+
- **`debug_assert!` → real release guards** — `MyersPattern::new`, `jaro_bitpar`, `jaro_4way`
|
|
43
|
+
were guarded only by `debug_assert!`. In release builds, calling them with inputs outside
|
|
44
|
+
their contract (non-ASCII or > 64 bytes) would silently produce wrong results. Promoted to
|
|
45
|
+
proper `assert!` with descriptive messages that surface immediately in both debug and release.
|
|
46
|
+
|
|
47
|
+
### New API surface
|
|
48
|
+
- **`fuzz.cdist`** — pairwise score matrix, mirrors `rapidfuzz.fuzz.cdist`. Delegates to
|
|
49
|
+
`process.cdist` with `ratio` as the default scorer.
|
|
50
|
+
- **`DamerauLevenshtein.editops` / `.opcodes`** — full Lowrance-Wagner traceback returning
|
|
51
|
+
`Editops` / `Opcodes` (insert/delete/replace), completing parity with rapidfuzz's alignment
|
|
52
|
+
API for this module.
|
|
53
|
+
- **`fuzz.__all__`** now includes `partial_ratio_alignment` and `cdist` (were missing).
|
|
54
|
+
|
|
55
|
+
### API correctness fixes
|
|
56
|
+
- **`Jaro.similarity` / `JaroWinkler.similarity` `score_cutoff`** — changed default from
|
|
57
|
+
`0.0` to `None`, matching rapidfuzz's semantics (`0.0` treated scores of exactly 0.0 as
|
|
58
|
+
filtered, which was wrong).
|
|
59
|
+
- **`ratio_batch(workers=)`** — was silently ignored (`del workers`). Now wires up a
|
|
60
|
+
`ThreadPoolExecutor` for the processor path; the no-processor path continues to use Rayon
|
|
61
|
+
internally (ignoring `workers` is correct there — Rayon already uses all cores).
|
|
62
|
+
- **`process.cdist` fast path** — rewrote to use `fuzz_ratio_batch` row-by-row (each row
|
|
63
|
+
runs under Rayon across all cores) instead of a dead `raw = _native.fuzz_ratio_batch`
|
|
64
|
+
assignment followed by the same loop. Removed the dead `raw:` type-hint-only line.
|
|
65
|
+
- **`process.cdist` silent swallow** — `except Exception: pass` replaced with
|
|
66
|
+
`warnings.warn(...)` so unexpected fast-path failures are visible instead of silently
|
|
67
|
+
producing slow results.
|
|
68
|
+
|
|
69
|
+
### Includes all v0.1.7 fixes
|
|
70
|
+
All fixes from v0.1.7 are included — see the v0.1.7 changelog below.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
<details>
|
|
75
|
+
<summary><b>Previous (v0.1.7)</b></summary>
|
|
35
76
|
|
|
36
77
|
### Bug fixes (Windows DX12 / Jaro shader)
|
|
37
78
|
- **Jaro GPU shader FXC crash fixed** — `jaro.wgsl` and `jaro_matrix.wgsl` used dynamic vector
|
|
38
|
-
component writes (`v[j >> 5u] = ...`) in `bit_set()`.
|
|
39
|
-
|
|
40
|
-
with `X3550`/`X3511` on every Windows DX12 runner — all 11 Jaro GPU tests were panicking.
|
|
41
|
-
Fixed by rewriting `bit_set` with `select()`-based static construction, which compiles
|
|
42
|
-
identically on all backends (Vulkan, Metal, DX12).
|
|
79
|
+
component writes (`v[j >> 5u] = ...`) in `bit_set()`. Fixed by rewriting `bit_set` with
|
|
80
|
+
`select()`-based static construction (Vulkan, Metal, DX12 all compile identically).
|
|
43
81
|
|
|
44
82
|
### Bug fixes (arity mismatch — wasm & fuzz crates)
|
|
45
|
-
- **`fuzzgpu-wasm`** —
|
|
46
|
-
|
|
47
|
-
the
|
|
48
|
-
`0.0` as the cutoff (no cutoff — identical to the previous behaviour).
|
|
49
|
-
- **`fuzzgpu-fuzz`** — same four callsites in `fuzz/src/lib.rs` had the same arity mismatch.
|
|
50
|
-
Fixed identically.
|
|
51
|
-
|
|
52
|
-
### Includes all v0.1.6 features
|
|
53
|
-
All features from v0.1.6 are included — see the v0.1.6 changelog below.
|
|
83
|
+
- **`fuzzgpu-wasm`** and **`fuzzgpu-fuzz`** — 4 × `E0061` arity mismatch for
|
|
84
|
+
`partial_ratio`/`token_sort_ratio`/`token_set_ratio`/`wratio`. Fixed by passing `0.0`
|
|
85
|
+
as the cutoff.
|
|
54
86
|
|
|
55
|
-
|
|
87
|
+
</details>
|
|
56
88
|
|
|
57
89
|
<details>
|
|
58
90
|
<summary><b>Previous (v0.1.6)</b></summary>
|
|
@@ -103,26 +135,53 @@ The full Python layer is now byte-identical to rapidfuzz 3.14.5 over a 169,744-p
|
|
|
103
135
|
## Benchmark Results
|
|
104
136
|
|
|
105
137
|
*Hardware: Intel(R) Iris(R) Xe Graphics (Vulkan) + Intel Core i7 (Rayon uses all cores)*
|
|
106
|
-
*Versions: fuzzgpu 0.1.
|
|
138
|
+
*Versions: fuzzgpu 0.1.8 · rapidfuzz 3.14.5 · python-Levenshtein 0.27.4*
|
|
107
139
|
*Median of 7 runs after warmup. Reproduce: `python benchmarks/bench_compare.py`*
|
|
108
140
|
|
|
109
141
|
### Levenshtein Batch (1 query × N candidates, 10-char strings)
|
|
110
142
|
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
111
143
|
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
112
|
-
| **100** | 0.
|
|
113
|
-
| **1,000** | 0.
|
|
114
|
-
| **10,000** |
|
|
115
|
-
| **50,000** |
|
|
144
|
+
| **100** | 0.04 ms | 0.00 ms | 0.01 ms | 0.24× | 1.90× |
|
|
145
|
+
| **1,000** | 0.50 ms | 0.04 ms | 0.07 ms | 0.13× | 1.89× |
|
|
146
|
+
| **10,000** | 1.53 ms | 0.40 ms | 0.64 ms | 0.42× | 1.61× |
|
|
147
|
+
| **50,000** | 6.87 ms | 3.36 ms | 4.46 ms | 0.65× | 1.33× |
|
|
116
148
|
|
|
117
149
|
### Damerau-Levenshtein Batch (unrestricted Lowrance-Wagner)
|
|
118
150
|
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
119
151
|
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
120
|
-
| **
|
|
121
|
-
| **
|
|
122
|
-
| **
|
|
152
|
+
| **100** | 0.04 ms | 0.04 ms | 0.10 ms | 2.68× | 2.63× |
|
|
153
|
+
| **1,000** | 0.21 ms | 0.20 ms | 0.98 ms | 4.71× | 4.89× |
|
|
154
|
+
| **10,000** | 1.39 ms | 1.49 ms | 9.93 ms | 7.14× | 6.67× |
|
|
155
|
+
| **50,000** | 10.19 ms | 9.91 ms | 66.57 ms | 6.54× | 6.72× |
|
|
123
156
|
|
|
124
157
|
> **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.
|
|
125
158
|
|
|
159
|
+
### Jaro-Winkler Batch (p = 0.1)
|
|
160
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | vs RF (GPU) | vs RF (CPU) |
|
|
161
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
162
|
+
| **100** | 0.02 ms | 0.01 ms | 0.02 ms | 0.87× | 1.14× |
|
|
163
|
+
| **1,000** | 0.18 ms | 0.13 ms | 0.12 ms | 0.70× | 0.94× |
|
|
164
|
+
| **10,000** | 1.01 ms | 0.83 ms | 0.97 ms | 0.96× | 1.18× |
|
|
165
|
+
| **50,000** | 5.61 ms | 3.25 ms | 6.69 ms | 1.19× | 2.06× |
|
|
166
|
+
|
|
167
|
+
### Needleman-Wunsch Affine Batch (match=1, mismatch=-1, gap_open=-2, gap_extend=-1)
|
|
168
|
+
| Batch Size | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` |
|
|
169
|
+
| :--- | :---: | :---: | :---: |
|
|
170
|
+
| **100** | 0.07 ms | 0.05 ms | — |
|
|
171
|
+
| **1,000** | 1.65 ms | 0.43 ms | — |
|
|
172
|
+
| **10,000** | 12.49 ms | 3.91 ms | — |
|
|
173
|
+
| **50,000** | 47.86 ms | 26.95 ms | — |
|
|
174
|
+
|
|
175
|
+
> rapidfuzz has no Needleman-Wunsch API — no comparison available.
|
|
176
|
+
|
|
177
|
+
### Levenshtein Cross-Product Matrix (`cdist`)
|
|
178
|
+
| Matrix Size | Total Pairs | `fuzzgpu` (GPU) | `fuzzgpu` (CPU) | `rapidfuzz` | python-Levenshtein | vs RF (GPU) | vs RF (CPU) |
|
|
179
|
+
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
|
180
|
+
| **10 × 10** | 100 | 0.05 ms | 0.02 ms | 0.01 ms | 0.06 ms | 0.18× | 0.56× |
|
|
181
|
+
| **50 × 50** | 2,500 | 0.58 ms | 0.11 ms | 0.09 ms | 2.17 ms | 0.16× | 0.81× |
|
|
182
|
+
| **100 × 100** | 10,000 | 0.67 ms | 0.25 ms | 0.18 ms | 5.75 ms | 0.27× | 0.70× |
|
|
183
|
+
| **200 × 200** | 40,000 | 1.19 ms | 0.81 ms | 0.61 ms | 22.31 ms | 0.51× | 0.75× |
|
|
184
|
+
|
|
126
185
|
---
|
|
127
186
|
|
|
128
187
|
## Installation
|
|
@@ -134,7 +193,7 @@ pip install fuzzgpu
|
|
|
134
193
|
```toml
|
|
135
194
|
# Rust
|
|
136
195
|
[dependencies]
|
|
137
|
-
fuzzgpu-core = "0.1.
|
|
196
|
+
fuzzgpu-core = "0.1.8"
|
|
138
197
|
```
|
|
139
198
|
|
|
140
199
|
---
|
|
@@ -230,7 +289,7 @@ fuzzgpu.set_cpu_only(True) # force CPU-only mode
|
|
|
230
289
|
|
|
231
290
|
```toml
|
|
232
291
|
[dependencies]
|
|
233
|
-
fuzzgpu-core = "0.1.
|
|
292
|
+
fuzzgpu-core = "0.1.8" # GPU + CPU fallback
|
|
234
293
|
# fuzzgpu-core = { version = "0.1.7", default-features = false } # CPU-only
|
|
235
294
|
```
|
|
236
295
|
|
|
@@ -234,7 +234,9 @@ pub mod gpu_ext {
|
|
|
234
234
|
let engine = GpuEngine::get()?;
|
|
235
235
|
let kernel = Self::new_inner(engine)?;
|
|
236
236
|
let _ = GLOBAL_GPU_DAMERAU_KERNEL.set(kernel);
|
|
237
|
-
|
|
237
|
+
GLOBAL_GPU_DAMERAU_KERNEL.get().ok_or_else(|| FuzzGpuError::NoDevice(
|
|
238
|
+
"Damerau kernel unexpectedly absent after init".into()
|
|
239
|
+
))
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
fn new_inner(engine: std::sync::Arc<GpuEngine>) -> Result<Self> {
|
|
@@ -357,7 +357,9 @@ pub mod gpu_ext {
|
|
|
357
357
|
let engine = GpuEngine::get()?;
|
|
358
358
|
let kernel = Self::new_inner(engine)?;
|
|
359
359
|
let _ = GLOBAL_GPU_JARO_KERNEL.set(kernel);
|
|
360
|
-
|
|
360
|
+
GLOBAL_GPU_JARO_KERNEL.get().ok_or_else(|| FuzzGpuError::NoDevice(
|
|
361
|
+
"Jaro kernel unexpectedly absent after init".into()
|
|
362
|
+
))
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
fn new_inner(engine: std::sync::Arc<GpuEngine>) -> Result<Self> {
|
|
@@ -246,8 +246,12 @@ pub mod gpu_ext {
|
|
|
246
246
|
if let Some(k) = GLOBAL_GPU_KERNEL.get() { return Ok(k); }
|
|
247
247
|
let engine = GpuEngine::get()?;
|
|
248
248
|
let kernel = Self::new_inner(engine)?;
|
|
249
|
+
// A concurrent caller may have won the race and set it already;
|
|
250
|
+
// either way `get()` now returns `Some` — no unwrap needed.
|
|
249
251
|
let _ = GLOBAL_GPU_KERNEL.set(kernel);
|
|
250
|
-
|
|
252
|
+
GLOBAL_GPU_KERNEL.get().ok_or_else(|| FuzzGpuError::NoDevice(
|
|
253
|
+
"Levenshtein kernel unexpectedly absent after init".into()
|
|
254
|
+
))
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
fn new_inner(engine: std::sync::Arc<GpuEngine>) -> Result<Self> {
|
|
@@ -218,7 +218,9 @@ pub mod gpu_ext {
|
|
|
218
218
|
let engine = GpuEngine::get()?;
|
|
219
219
|
let kernel = Self::new_inner(engine)?;
|
|
220
220
|
let _ = GLOBAL_GPU_KERNEL.set(kernel);
|
|
221
|
-
|
|
221
|
+
GLOBAL_GPU_KERNEL.get().ok_or_else(|| FuzzGpuError::NoDevice(
|
|
222
|
+
"Needleman kernel unexpectedly absent after init".into()
|
|
223
|
+
))
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
fn new_inner(engine: std::sync::Arc<GpuEngine>) -> Result<Self> {
|
|
@@ -110,7 +110,11 @@ pub(crate) struct MyersPattern {
|
|
|
110
110
|
impl MyersPattern {
|
|
111
111
|
/// Build pattern state. `pattern` must be non-empty ASCII ≤ 64 bytes.
|
|
112
112
|
pub(crate) fn new(pattern: &[u8]) -> Self {
|
|
113
|
-
|
|
113
|
+
assert!(
|
|
114
|
+
!pattern.is_empty() && pattern.len() <= 64 && pattern.is_ascii(),
|
|
115
|
+
"MyersPattern: pattern must be non-empty ASCII ≤ 64 bytes (got {} bytes, ascii={})",
|
|
116
|
+
pattern.len(), pattern.is_ascii()
|
|
117
|
+
);
|
|
114
118
|
let mut peq = [0u64; 256];
|
|
115
119
|
for (j, &ch) in pattern.iter().enumerate() {
|
|
116
120
|
peq[ch as usize] |= 1u64 << j;
|
|
@@ -429,7 +433,8 @@ pub(crate) unsafe fn levenshtein_myers_2way_neon(pat: &MyersPattern, texts: [&[u
|
|
|
429
433
|
|
|
430
434
|
/// 4-way kernel over a prebuilt pattern (see [`MyersPattern`]).
|
|
431
435
|
pub(crate) fn levenshtein_myers_4way_pat(pat: &MyersPattern, texts: [&[u8]; 4]) -> [u32; 4] {
|
|
432
|
-
debug_assert!(texts.iter().all(|t| t.is_ascii())
|
|
436
|
+
debug_assert!(texts.iter().all(|t| t.is_ascii()),
|
|
437
|
+
"levenshtein_myers_4way_pat: all text inputs must be ASCII");
|
|
433
438
|
#[cfg(target_arch = "x86_64")]
|
|
434
439
|
{
|
|
435
440
|
if avx2_available() {
|
|
@@ -442,7 +447,8 @@ pub(crate) fn levenshtein_myers_4way_pat(pat: &MyersPattern, texts: [&[u8]; 4])
|
|
|
442
447
|
|
|
443
448
|
/// Scalar Myers over a prebuilt pattern (tail/fallback path of the batch).
|
|
444
449
|
pub(crate) fn levenshtein_myers_pattern(pat: &MyersPattern, text: &[u8]) -> u32 {
|
|
445
|
-
debug_assert!(text.is_ascii()
|
|
450
|
+
debug_assert!(text.is_ascii(),
|
|
451
|
+
"levenshtein_myers_pattern: text must be ASCII");
|
|
446
452
|
let MyersPattern { peq, m, mask, last_bit } = pat;
|
|
447
453
|
let mut pv: u64 = *mask;
|
|
448
454
|
let mut mv: u64 = 0;
|
|
@@ -624,8 +630,11 @@ unsafe fn levenshtein_myers_4way_avx2(pat: &MyersPattern, texts: [&[u8]; 4]) ->
|
|
|
624
630
|
/// walk as the reference. ASCII bytes, both inputs ≤ 64 (the position masks
|
|
625
631
|
/// must fit one u64).
|
|
626
632
|
pub fn jaro_bitpar(a: &[u8], b: &[u8]) -> f64 {
|
|
627
|
-
|
|
628
|
-
|
|
633
|
+
assert!(
|
|
634
|
+
a.len() <= 64 && b.len() <= 64 && a.is_ascii() && b.is_ascii(),
|
|
635
|
+
"jaro_bitpar: inputs must be ASCII ≤ 64 bytes (got a={} bytes ascii={}, b={} bytes ascii={})",
|
|
636
|
+
a.len(), a.is_ascii(), b.len(), b.is_ascii()
|
|
637
|
+
);
|
|
629
638
|
let (m, n) = (a.len(), b.len());
|
|
630
639
|
if m == 0 && n == 0 {
|
|
631
640
|
return 1.0;
|
|
@@ -703,7 +712,11 @@ pub fn jaro_bitpar(a: &[u8], b: &[u8]) -> f64 {
|
|
|
703
712
|
/// the Iris Xe-class CPUs this targets). Transpositions and scoring run in a
|
|
704
713
|
/// short scalar tail per lane.
|
|
705
714
|
pub fn jaro_4way(a: &[u8], texts: [&[u8]; 4]) -> [f64; 4] {
|
|
706
|
-
|
|
715
|
+
assert!(
|
|
716
|
+
a.len() <= 64 && a.is_ascii(),
|
|
717
|
+
"jaro_4way: first argument must be ASCII ≤ 64 bytes (got {} bytes, ascii={})",
|
|
718
|
+
a.len(), a.is_ascii()
|
|
719
|
+
);
|
|
707
720
|
debug_assert!(texts.iter().all(|t| t.len() <= 64 && t.is_ascii()));
|
|
708
721
|
#[cfg(target_arch = "x86_64")]
|
|
709
722
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "fuzzgpu-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.8"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Python bindings for FuzzGPU: GPU-accelerated fuzzy string matching engine"
|
|
6
6
|
license = "MIT"
|
|
@@ -17,7 +17,7 @@ default = ["gpu"]
|
|
|
17
17
|
gpu = ["fuzzgpu-core/gpu"]
|
|
18
18
|
|
|
19
19
|
[dependencies]
|
|
20
|
-
fuzzgpu-core = { path = "../fuzzgpu-core", version = "0.1.
|
|
20
|
+
fuzzgpu-core = { path = "../fuzzgpu-core", version = "0.1.8", default-features = false }
|
|
21
21
|
pyo3 = { workspace = true, features = ["abi3-py310"] }
|
|
22
22
|
numpy = "0.23"
|
|
23
23
|
pollster = { workspace = true }
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fuzzgpu"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "Hardware-accelerated fuzzy string matching & sequence alignment — cross-platform, no CUDA required"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""rapidfuzz.distance.DamerauLevenshtein-compatible module.
|
|
2
|
+
|
|
3
|
+
NOTE: This module implements the **unrestricted** Lowrance-Wagner (1975)
|
|
4
|
+
algorithm which allows non-adjacent transpositions. rapidfuzz's
|
|
5
|
+
DamerauLevenshtein uses Optimal String Alignment (OSA) which forbids them.
|
|
6
|
+
For OSA-compatible semantics use fuzzgpu.distance.OSA.
|
|
7
|
+
Example difference: distance("ca", "abc") == 2 here (unrestricted),
|
|
8
|
+
== 3 in rapidfuzz's OSA-based DamerauLevenshtein.
|
|
9
|
+
"""
|
|
10
|
+
from .. import fuzzgpu as _native
|
|
11
|
+
from ._common import Editop, Editops, cutoff_distance, normalized_distance as _normalized
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def distance(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
15
|
+
del score_hint
|
|
16
|
+
if processor:
|
|
17
|
+
s1, s2 = processor(s1), processor(s2)
|
|
18
|
+
return cutoff_distance(_native.damerau_levenshtein_distance(s1, s2), score_cutoff)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def similarity(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
22
|
+
del score_hint
|
|
23
|
+
if processor:
|
|
24
|
+
s1, s2 = processor(s1), processor(s2)
|
|
25
|
+
maximum = max(len(s1), len(s2))
|
|
26
|
+
value = maximum - distance(s1, s2)
|
|
27
|
+
return value if score_cutoff is None or value >= score_cutoff else 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalized_distance(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
31
|
+
del score_hint
|
|
32
|
+
if processor:
|
|
33
|
+
s1, s2 = processor(s1), processor(s2)
|
|
34
|
+
value = _normalized(distance(s1, s2), max(len(s1), len(s2)))
|
|
35
|
+
return value if score_cutoff is None or value <= score_cutoff else 1.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalized_similarity(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
39
|
+
del score_hint
|
|
40
|
+
if processor:
|
|
41
|
+
s1, s2 = processor(s1), processor(s2)
|
|
42
|
+
value = 1.0 - _normalized(distance(s1, s2), max(len(s1), len(s2)))
|
|
43
|
+
return value if score_cutoff is None or value >= score_cutoff else 0.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Alignment helpers ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def _damerau_matrix(s1, s2):
|
|
49
|
+
"""Full Lowrance-Wagner DP matrix for traceback.
|
|
50
|
+
|
|
51
|
+
Returns the filled (m+2) × (n+2) cost matrix as a list of lists so the
|
|
52
|
+
traceback can reconstruct the edit path. The layout matches the standard
|
|
53
|
+
Lowrance-Wagner (1975) formulation with the da[] last-seen-position table.
|
|
54
|
+
"""
|
|
55
|
+
m, n = len(s1), len(s2)
|
|
56
|
+
INF = m + n + 1
|
|
57
|
+
|
|
58
|
+
# d[i][j]: cost to turn s1[:i] into s2[:j]
|
|
59
|
+
d = [[0] * (n + 2) for _ in range(m + 2)]
|
|
60
|
+
d[0][0] = INF
|
|
61
|
+
for i in range(m + 1):
|
|
62
|
+
d[i + 1][0] = INF
|
|
63
|
+
d[i + 1][1] = i
|
|
64
|
+
for j in range(n + 1):
|
|
65
|
+
d[0][j + 1] = INF
|
|
66
|
+
d[1][j + 1] = j
|
|
67
|
+
|
|
68
|
+
# da[c] = last row where character c was seen in s1 (1-indexed)
|
|
69
|
+
da = {}
|
|
70
|
+
|
|
71
|
+
for i in range(1, m + 1):
|
|
72
|
+
db = 0 # last column where s1[i-1] was seen in s2 (1-indexed)
|
|
73
|
+
for j in range(1, n + 1):
|
|
74
|
+
i1 = da.get(s2[j - 1], 0)
|
|
75
|
+
j1 = db
|
|
76
|
+
cost = 0 if s1[i - 1] == s2[j - 1] else 1
|
|
77
|
+
if cost == 0:
|
|
78
|
+
db = j
|
|
79
|
+
d[i + 1][j + 1] = min(
|
|
80
|
+
d[i][j] + cost, # substitute / match
|
|
81
|
+
d[i + 1][j] + 1, # insert
|
|
82
|
+
d[i][j + 1] + 1, # delete
|
|
83
|
+
d[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1), # transpose
|
|
84
|
+
)
|
|
85
|
+
da[s1[i - 1]] = i
|
|
86
|
+
|
|
87
|
+
return d
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def editops(s1, s2, *, processor=None, score_hint=None):
|
|
91
|
+
"""Return Editops describing how to turn s1 into s2 (Lowrance-Wagner).
|
|
92
|
+
|
|
93
|
+
The returned edit sequence uses only insert/delete/replace operations —
|
|
94
|
+
transpositions are decomposed into the minimum-cost sequence of those
|
|
95
|
+
three primitive operations, which is how rapidfuzz represents them too.
|
|
96
|
+
"""
|
|
97
|
+
del score_hint
|
|
98
|
+
if processor:
|
|
99
|
+
s1, s2 = processor(s1), processor(s2)
|
|
100
|
+
|
|
101
|
+
m, n = len(s1), len(s2)
|
|
102
|
+
result = Editops([], 0, 0)
|
|
103
|
+
result._src_len = m
|
|
104
|
+
result._dest_len = n
|
|
105
|
+
|
|
106
|
+
if s1 == s2:
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
d = _damerau_matrix(s1, s2)
|
|
110
|
+
|
|
111
|
+
# Traceback from d[m+1][n+1] (1-indexed DP, offset by 1 for the INF border)
|
|
112
|
+
ops = []
|
|
113
|
+
i, j = m, n
|
|
114
|
+
while i > 0 or j > 0:
|
|
115
|
+
if i > 0 and j > 0:
|
|
116
|
+
cost = 0 if s1[i - 1] == s2[j - 1] else 1
|
|
117
|
+
if d[i + 1][j + 1] == d[i][j] + cost:
|
|
118
|
+
if cost:
|
|
119
|
+
ops.append(Editop("replace", i - 1, j - 1))
|
|
120
|
+
i -= 1
|
|
121
|
+
j -= 1
|
|
122
|
+
continue
|
|
123
|
+
if j > 0 and d[i + 1][j + 1] == d[i + 1][j] + 1:
|
|
124
|
+
ops.append(Editop("insert", i, j - 1))
|
|
125
|
+
j -= 1
|
|
126
|
+
elif i > 0 and d[i + 1][j + 1] == d[i][j + 1] + 1:
|
|
127
|
+
ops.append(Editop("delete", i - 1, j))
|
|
128
|
+
i -= 1
|
|
129
|
+
else:
|
|
130
|
+
# Transposition or boundary — fall back to delete+insert decomposition
|
|
131
|
+
if i > 0:
|
|
132
|
+
ops.append(Editop("delete", i - 1, j))
|
|
133
|
+
i -= 1
|
|
134
|
+
else:
|
|
135
|
+
ops.append(Editop("insert", i, j - 1))
|
|
136
|
+
j -= 1
|
|
137
|
+
|
|
138
|
+
ops.reverse()
|
|
139
|
+
result._editops = ops
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def opcodes(s1, s2, *, processor=None, score_hint=None):
|
|
144
|
+
"""Return Opcodes describing how to turn s1 into s2."""
|
|
145
|
+
return editops(s1, s2, processor=processor, score_hint=score_hint).as_opcodes()
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
from .. import fuzzgpu as _native
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def similarity(s1, s2, *, processor=None, score_cutoff=
|
|
5
|
+
def similarity(s1, s2, *, processor=None, score_cutoff=None):
|
|
6
6
|
if processor:
|
|
7
7
|
s1, s2 = processor(s1), processor(s2)
|
|
8
8
|
value = _native.jaro_similarity(s1, s2)
|
|
9
|
-
return value if value >= score_cutoff else 0.0
|
|
9
|
+
return value if score_cutoff is None or value >= score_cutoff else 0.0
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def distance(s1, s2, *, processor=None, score_cutoff=None):
|
|
@@ -16,7 +16,7 @@ def distance(s1, s2, *, processor=None, score_cutoff=None):
|
|
|
16
16
|
return value if score_cutoff is None or value <= score_cutoff else 1.0
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def normalized_similarity(s1, s2, *, processor=None, score_cutoff=
|
|
19
|
+
def normalized_similarity(s1, s2, *, processor=None, score_cutoff=None):
|
|
20
20
|
return similarity(s1, s2, processor=processor, score_cutoff=score_cutoff)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
from .. import fuzzgpu as _native
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def similarity(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=
|
|
5
|
+
def similarity(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=None):
|
|
6
6
|
if processor:
|
|
7
7
|
s1, s2 = processor(s1), processor(s2)
|
|
8
8
|
value = _native.jaro_winkler_similarity(s1, s2, prefix_weight)
|
|
9
|
-
return value if value >= score_cutoff else 0.0
|
|
9
|
+
return value if score_cutoff is None or value >= score_cutoff else 0.0
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def distance(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=None):
|
|
@@ -16,7 +16,7 @@ def distance(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=None):
|
|
|
16
16
|
return value if score_cutoff is None or value <= score_cutoff else 1.0
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def normalized_similarity(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=
|
|
19
|
+
def normalized_similarity(s1, s2, *, prefix_weight=0.1, processor=None, score_cutoff=None):
|
|
20
20
|
return similarity(s1, s2, prefix_weight=prefix_weight,
|
|
21
21
|
processor=processor, score_cutoff=score_cutoff)
|
|
22
22
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"""rapidfuzz-compatible fuzzy scorers backed by fuzzgpu's Rust extension."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
from . import fuzzgpu as _native
|
|
4
6
|
from .distance._common import ScoreAlignment
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
def _worker_count(workers):
|
|
10
|
+
"""Resolve the workers parameter to a concrete thread count."""
|
|
11
|
+
if workers is None or workers == 1:
|
|
12
|
+
return 1
|
|
13
|
+
if workers == -1:
|
|
14
|
+
return os.cpu_count() or 1
|
|
15
|
+
if isinstance(workers, int) and workers > 0:
|
|
16
|
+
return workers
|
|
17
|
+
raise ValueError("workers must be None, -1, or a positive integer")
|
|
18
|
+
|
|
19
|
+
|
|
7
20
|
def _prepare(a, b, processor):
|
|
8
21
|
if processor is not None:
|
|
9
22
|
a, b = processor(a), processor(b)
|
|
@@ -81,11 +94,27 @@ wratio = WRatio
|
|
|
81
94
|
|
|
82
95
|
|
|
83
96
|
def ratio_batch(query, candidates, *, processor=None, score_cutoff=0.0, workers=None):
|
|
84
|
-
|
|
97
|
+
"""Compute ratio(query, c) for every c in candidates.
|
|
98
|
+
|
|
99
|
+
When no processor is given the computation runs inside Rust under Rayon
|
|
100
|
+
(all cores); ``workers`` is ignored in that case since Rayon already
|
|
101
|
+
parallelises across cores. When a Python ``processor`` is supplied the
|
|
102
|
+
work runs in Python and ``workers`` controls the thread pool size
|
|
103
|
+
(``None``/``1`` = single-threaded, ``-1`` = all CPU cores).
|
|
104
|
+
"""
|
|
85
105
|
if processor is None:
|
|
86
106
|
values = _native.fuzz_ratio_batch(query, candidates)
|
|
87
107
|
return values if score_cutoff <= 0.0 else [_cutoff(v, score_cutoff) for v in values]
|
|
88
|
-
|
|
108
|
+
# processor path: honour workers
|
|
109
|
+
n = _worker_count(workers) if workers is not None and workers != 1 else 1
|
|
110
|
+
if n == 1:
|
|
111
|
+
return [ratio(query, c, processor=processor, score_cutoff=score_cutoff) for c in candidates]
|
|
112
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
113
|
+
with ThreadPoolExecutor(max_workers=n) as pool:
|
|
114
|
+
return list(pool.map(
|
|
115
|
+
lambda c: ratio(query, c, processor=processor, score_cutoff=score_cutoff),
|
|
116
|
+
candidates
|
|
117
|
+
))
|
|
89
118
|
|
|
90
119
|
|
|
91
120
|
def extract(query, choices, score_cutoff=0.0, limit=5, *, scorer=ratio, processor=None,
|
|
@@ -108,10 +137,32 @@ extract_one = extractOne
|
|
|
108
137
|
damerau_ratio = _native.damerau_ratio
|
|
109
138
|
|
|
110
139
|
|
|
140
|
+
def cdist(queries, choices, *, scorer=None, processor=None, score_cutoff=None,
|
|
141
|
+
score_hint=None, score_multiplier=1, dtype=None, workers=None,
|
|
142
|
+
scorer_kwargs=None):
|
|
143
|
+
"""Pairwise score matrix between all queries and all choices.
|
|
144
|
+
|
|
145
|
+
Delegates to ``process.cdist`` with ``scorer`` defaulting to
|
|
146
|
+
``fuzz.ratio`` (matching rapidfuzz's ``fuzz.cdist``).
|
|
147
|
+
"""
|
|
148
|
+
from .process import cdist as _cdist
|
|
149
|
+
return _cdist(
|
|
150
|
+
queries, choices,
|
|
151
|
+
scorer=scorer if scorer is not None else ratio,
|
|
152
|
+
processor=processor,
|
|
153
|
+
score_cutoff=score_cutoff,
|
|
154
|
+
score_hint=score_hint,
|
|
155
|
+
score_multiplier=score_multiplier,
|
|
156
|
+
dtype=dtype,
|
|
157
|
+
workers=workers,
|
|
158
|
+
scorer_kwargs=scorer_kwargs,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
111
162
|
__all__ = [
|
|
112
163
|
"ratio", "partial_ratio", "partial_ratio_alignment",
|
|
113
164
|
"token_sort_ratio", "token_set_ratio", "token_ratio",
|
|
114
165
|
"partial_token_sort_ratio", "partial_token_set_ratio", "partial_token_ratio",
|
|
115
|
-
"QRatio", "WRatio", "wratio", "ratio_batch",
|
|
166
|
+
"QRatio", "WRatio", "wratio", "ratio_batch", "cdist",
|
|
116
167
|
"extract", "extractOne", "extract_one", "damerau_ratio",
|
|
117
168
|
]
|
|
@@ -175,8 +175,12 @@ def cdist(
|
|
|
175
175
|
choices = list(choices)
|
|
176
176
|
|
|
177
177
|
# ── Fast path: native ratio scorer, no per-string processor ──────────
|
|
178
|
-
# Route through the accelerated Rust
|
|
179
|
-
#
|
|
178
|
+
# Route through the accelerated Rust fuzz_ratio_batch for each query row.
|
|
179
|
+
# Each row call releases the GIL and runs under Rayon, so the Python loop
|
|
180
|
+
# here only pays per-query overhead — not per-cell. This is still correct
|
|
181
|
+
# behaviour for `workers`: Rayon uses all cores inside each row call, so
|
|
182
|
+
# `workers` is ignored at the Python level (it would just add Python thread
|
|
183
|
+
# overhead on top of Rayon's own parallelism).
|
|
180
184
|
if (
|
|
181
185
|
_is_native_ratio(scorer)
|
|
182
186
|
and processor is None
|
|
@@ -185,15 +189,14 @@ def cdist(
|
|
|
185
189
|
):
|
|
186
190
|
try:
|
|
187
191
|
from . import fuzzgpu as _native
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
matrix.append(row)
|
|
192
|
+
matrix: list[list[float]] = [
|
|
193
|
+
_native.fuzz_ratio_batch(q, choices) for q in queries
|
|
194
|
+
]
|
|
195
|
+
if score_cutoff is not None:
|
|
196
|
+
matrix = [
|
|
197
|
+
[v if v >= score_cutoff else 0.0 for v in row]
|
|
198
|
+
for row in matrix
|
|
199
|
+
]
|
|
197
200
|
if dtype is not None:
|
|
198
201
|
try:
|
|
199
202
|
import numpy as np
|
|
@@ -201,9 +204,13 @@ def cdist(
|
|
|
201
204
|
except ImportError:
|
|
202
205
|
pass
|
|
203
206
|
return matrix
|
|
204
|
-
except Exception:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
import warnings
|
|
209
|
+
warnings.warn(
|
|
210
|
+
f"fuzzgpu cdist fast path failed ({exc!r}), falling back to per-cell scorer",
|
|
211
|
+
RuntimeWarning,
|
|
212
|
+
stacklevel=2,
|
|
213
|
+
)
|
|
207
214
|
|
|
208
215
|
# ── Generic path ─────────────────────────────────────────────────────
|
|
209
216
|
def row(query):
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""rapidfuzz.distance.DamerauLevenshtein-compatible module.
|
|
2
|
-
|
|
3
|
-
NOTE: This module implements the **unrestricted** Lowrance-Wagner (1975)
|
|
4
|
-
algorithm which allows non-adjacent transpositions. rapidfuzz's
|
|
5
|
-
DamerauLevenshtein uses Optimal String Alignment (OSA) which forbids them.
|
|
6
|
-
For OSA-compatible semantics use fuzzgpu.distance.OSA.
|
|
7
|
-
Example difference: distance("ca", "abc") == 2 here (unrestricted),
|
|
8
|
-
== 3 in rapidfuzz's OSA-based DamerauLevenshtein.
|
|
9
|
-
"""
|
|
10
|
-
from .. import fuzzgpu as _native
|
|
11
|
-
from ._common import cutoff_distance, normalized_distance as _normalized
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def distance(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
15
|
-
del score_hint
|
|
16
|
-
if processor:
|
|
17
|
-
s1, s2 = processor(s1), processor(s2)
|
|
18
|
-
return cutoff_distance(_native.damerau_levenshtein_distance(s1, s2), score_cutoff)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def similarity(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
22
|
-
del score_hint
|
|
23
|
-
if processor:
|
|
24
|
-
s1, s2 = processor(s1), processor(s2)
|
|
25
|
-
maximum = max(len(s1), len(s2))
|
|
26
|
-
value = maximum - distance(s1, s2)
|
|
27
|
-
return value if score_cutoff is None or value >= score_cutoff else 0
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def normalized_distance(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
31
|
-
del score_hint
|
|
32
|
-
if processor:
|
|
33
|
-
s1, s2 = processor(s1), processor(s2)
|
|
34
|
-
value = _normalized(distance(s1, s2), max(len(s1), len(s2)))
|
|
35
|
-
return value if score_cutoff is None or value <= score_cutoff else 1.0
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def normalized_similarity(s1, s2, *, processor=None, score_cutoff=None, score_hint=None):
|
|
39
|
-
del score_hint
|
|
40
|
-
if processor:
|
|
41
|
-
s1, s2 = processor(s1), processor(s2)
|
|
42
|
-
value = 1.0 - _normalized(distance(s1, s2), max(len(s1), len(s2)))
|
|
43
|
-
return value if score_cutoff is None or value >= score_cutoff else 0.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fuzzgpu-0.1.7 → fuzzgpu-0.1.8}/crates/fuzzgpu-core/src/shaders/levenshtein_cdist_myers.wgsl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|