fuzzgpu 0.1.0__tar.gz → 0.1.1__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.0 → fuzzgpu-0.1.1}/Cargo.lock +2 -2
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/PKG-INFO +3 -3
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/README.md +2 -2
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/Cargo.toml +1 -1
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/damerau.rs +85 -12
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/fuzz.rs +52 -49
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/jaro.rs +58 -17
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/levenshtein.rs +23 -10
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/needleman.rs +40 -22
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +13 -11
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +13 -10
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-python/Cargo.toml +2 -2
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-python/src/lib.rs +4 -1
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/pyproject.toml +1 -1
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/python/fuzzgpu/__init__.py +10 -7
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/Cargo.toml +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/LICENSE +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/assets/logo.png +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/assets/logo.svg +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/gpu.rs +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/lib.rs +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/simd.rs +0 -0
- {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/python/fuzzgpu/fuzz.py +0 -0
|
@@ -248,7 +248,7 @@ dependencies = [
|
|
|
248
248
|
|
|
249
249
|
[[package]]
|
|
250
250
|
name = "fuzzgpu-core"
|
|
251
|
-
version = "0.1.
|
|
251
|
+
version = "0.1.1"
|
|
252
252
|
dependencies = [
|
|
253
253
|
"bytemuck",
|
|
254
254
|
"log",
|
|
@@ -260,7 +260,7 @@ dependencies = [
|
|
|
260
260
|
|
|
261
261
|
[[package]]
|
|
262
262
|
name = "fuzzgpu-python"
|
|
263
|
-
version = "0.1.
|
|
263
|
+
version = "0.1.1"
|
|
264
264
|
dependencies = [
|
|
265
265
|
"fuzzgpu-core",
|
|
266
266
|
"pollster",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fuzzgpu
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -35,7 +35,7 @@ Project-URL: Repository, https://github.com/kuntal-devrat/fuzzgpu
|
|
|
35
35
|
|
|
36
36
|
*Cross-platform GPU compute via WebGPU (`wgpu`) & Multi-Core CPU parallelism with Rayon. Zero CUDA dependencies.*
|
|
37
37
|
|
|
38
|
-
[](https://pypi.org/project/fuzzgpu/)
|
|
39
39
|
[](https://opensource.org/licenses/MIT)
|
|
40
40
|
[](https://www.rust-lang.org)
|
|
41
41
|
[](https://github.com/kuntal-devrat/fuzzgpu)
|
|
@@ -100,7 +100,7 @@ pip install fuzzgpu
|
|
|
100
100
|
### Rust (Cargo.toml)
|
|
101
101
|
```toml
|
|
102
102
|
[dependencies]
|
|
103
|
-
fuzzgpu-core = "0.1.
|
|
103
|
+
fuzzgpu-core = "0.1.1"
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
---
|
|
@@ -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)
|
|
@@ -73,7 +73,7 @@ pip install fuzzgpu
|
|
|
73
73
|
### Rust (Cargo.toml)
|
|
74
74
|
```toml
|
|
75
75
|
[dependencies]
|
|
76
|
-
fuzzgpu-core = "0.1.
|
|
76
|
+
fuzzgpu-core = "0.1.1"
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
---
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
use rayon::prelude::*;
|
|
2
|
+
use std::collections::HashMap;
|
|
2
3
|
|
|
3
4
|
/// True unrestricted Damerau-Levenshtein distance (Lowrance & Wagner 1975).
|
|
4
5
|
/// Computes edit distance allowing insertions, deletions, substitutions, and transpositions of any characters
|
|
5
6
|
/// (including non-adjacent transpositions where characters were inserted/deleted in between).
|
|
7
|
+
/// Supports both ASCII (fast array path) and full Unicode characters.
|
|
6
8
|
pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
if a.is_ascii() && b.is_ascii() {
|
|
10
|
+
damerau_bytes(a.as_bytes(), b.as_bytes())
|
|
11
|
+
} else {
|
|
12
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
13
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
14
|
+
damerau_chars(&a_chars, &b_chars)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
|
|
18
|
+
fn damerau_bytes(a: &[u8], b: &[u8]) -> u32 {
|
|
19
|
+
let (m, n) = (a.len(), b.len());
|
|
11
20
|
if m == 0 { return n as u32; }
|
|
12
21
|
if n == 0 { return m as u32; }
|
|
13
22
|
if a == b { return 0; }
|
|
14
23
|
|
|
15
24
|
let max_dist = (m + n) as u32;
|
|
16
|
-
|
|
17
|
-
// 2D distance matrix of size (m + 2) x (n + 2)
|
|
18
|
-
let rows = m + 2;
|
|
19
25
|
let cols = n + 2;
|
|
20
|
-
let mut h = vec![0u32;
|
|
26
|
+
let mut h = vec![0u32; (m + 2) * cols];
|
|
21
27
|
|
|
22
28
|
let idx = |i: isize, j: isize| -> usize {
|
|
23
29
|
((i + 1) as usize) * cols + ((j + 1) as usize)
|
|
@@ -33,11 +39,10 @@ pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
|
33
39
|
h[idx(0, j as isize)] = j as u32;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
// da[c] stores the last row where character c appeared in a
|
|
37
42
|
let mut da = [0usize; 256];
|
|
38
43
|
|
|
39
44
|
for i in 1..=m {
|
|
40
|
-
let mut db = 0usize;
|
|
45
|
+
let mut db = 0usize;
|
|
41
46
|
let ai = a[i - 1];
|
|
42
47
|
|
|
43
48
|
for j in 1..=n {
|
|
@@ -71,6 +76,67 @@ pub fn damerau_levenshtein_distance(a: &str, b: &str) -> u32 {
|
|
|
71
76
|
h[idx(m as isize, n as isize)]
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
fn damerau_chars(a: &[char], b: &[char]) -> u32 {
|
|
80
|
+
let (m, n) = (a.len(), b.len());
|
|
81
|
+
if m == 0 { return n as u32; }
|
|
82
|
+
if n == 0 { return m as u32; }
|
|
83
|
+
if a == b { return 0; }
|
|
84
|
+
|
|
85
|
+
let max_dist = (m + n) as u32;
|
|
86
|
+
let cols = n + 2;
|
|
87
|
+
let mut h = vec![0u32; (m + 2) * cols];
|
|
88
|
+
|
|
89
|
+
let idx = |i: isize, j: isize| -> usize {
|
|
90
|
+
((i + 1) as usize) * cols + ((j + 1) as usize)
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
h[idx(-1, -1)] = max_dist;
|
|
94
|
+
for i in 0..=m {
|
|
95
|
+
h[idx(i as isize, -1)] = max_dist;
|
|
96
|
+
h[idx(i as isize, 0)] = i as u32;
|
|
97
|
+
}
|
|
98
|
+
for j in 0..=n {
|
|
99
|
+
h[idx(-1, j as isize)] = max_dist;
|
|
100
|
+
h[idx(0, j as isize)] = j as u32;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let mut da: HashMap<char, usize> = HashMap::with_capacity(a.len());
|
|
104
|
+
|
|
105
|
+
for i in 1..=m {
|
|
106
|
+
let mut db = 0usize;
|
|
107
|
+
let ai = a[i - 1];
|
|
108
|
+
|
|
109
|
+
for j in 1..=n {
|
|
110
|
+
let bj = b[j - 1];
|
|
111
|
+
let k = da.get(&bj).copied().unwrap_or(0);
|
|
112
|
+
let l = db;
|
|
113
|
+
|
|
114
|
+
let cost = if ai == bj {
|
|
115
|
+
db = j;
|
|
116
|
+
0u32
|
|
117
|
+
} else {
|
|
118
|
+
1u32
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let sub = h[idx((i - 1) as isize, (j - 1) as isize)] + cost;
|
|
122
|
+
let ins = h[idx(i as isize, (j - 1) as isize)] + 1;
|
|
123
|
+
let del = h[idx((i - 1) as isize, j as isize)] + 1;
|
|
124
|
+
|
|
125
|
+
let trans = if k > 0 && l > 0 {
|
|
126
|
+
h[idx((k - 1) as isize, (l - 1) as isize)] + ((i - k - 1) as u32) + 1 + ((j - l - 1) as u32)
|
|
127
|
+
} else {
|
|
128
|
+
max_dist
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
h[idx(i as isize, j as isize)] = sub.min(ins).min(del).min(trans);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
da.insert(ai, i);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
h[idx(m as isize, n as isize)]
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
/// Batch Damerau-Levenshtein: one query vs many candidates.
|
|
75
141
|
pub fn damerau_levenshtein_batch(query: &str, candidates: &[&str]) -> Vec<u32> {
|
|
76
142
|
candidates.par_iter().map(|c| damerau_levenshtein_distance(query, c)).collect()
|
|
@@ -88,7 +154,8 @@ pub fn damerau_levenshtein_cdist(list_a: &[&str], list_b: &[&str]) -> Vec<Vec<u3
|
|
|
88
154
|
|
|
89
155
|
/// Damerau-Levenshtein normalized ratio (0.0 to 100.0) based on Sørensen-Dice formula.
|
|
90
156
|
pub fn damerau_ratio(s1: &str, s2: &str) -> f64 {
|
|
91
|
-
let
|
|
157
|
+
let len_a = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
158
|
+
let len_b = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
92
159
|
let total = len_a + len_b;
|
|
93
160
|
if total == 0 { return 100.0; }
|
|
94
161
|
let dist = damerau_levenshtein_distance(s1, s2) as f64;
|
|
@@ -101,13 +168,19 @@ mod tests {
|
|
|
101
168
|
|
|
102
169
|
#[test]
|
|
103
170
|
fn test_damerau_transposition() {
|
|
104
|
-
// "ab" to "ba" is 1 transposition in Damerau-Levenshtein, but 2 edits in Levenshtein
|
|
105
171
|
assert_eq!(damerau_levenshtein_distance("ab", "ba"), 1);
|
|
106
|
-
// "ca" to "abc" is 2 in true Damerau-Levenshtein (transposition of 'c' and 'a', plus insertion of 'b')
|
|
107
172
|
assert_eq!(damerau_levenshtein_distance("ca", "abc"), 2);
|
|
108
173
|
assert_eq!(damerau_levenshtein_distance("kitten", "sitting"), 3);
|
|
109
174
|
}
|
|
110
175
|
|
|
176
|
+
#[test]
|
|
177
|
+
fn test_damerau_unicode() {
|
|
178
|
+
assert_eq!(damerau_levenshtein_distance("café", "cafe"), 1);
|
|
179
|
+
assert_eq!(damerau_levenshtein_distance("naïve", "naive"), 1);
|
|
180
|
+
assert_eq!(damerau_levenshtein_distance("🚀", ""), 1);
|
|
181
|
+
assert_eq!(damerau_levenshtein_distance("中文", "中问"), 1);
|
|
182
|
+
}
|
|
183
|
+
|
|
111
184
|
#[test]
|
|
112
185
|
fn test_damerau_identical_and_empty() {
|
|
113
186
|
assert_eq!(damerau_levenshtein_distance("", ""), 0);
|
|
@@ -2,10 +2,19 @@ use rayon::prelude::*;
|
|
|
2
2
|
use std::collections::BTreeSet;
|
|
3
3
|
|
|
4
4
|
/// Standard Indel / Levenshtein (substitution cost = 2) edit distance used for fuzzy ratios.
|
|
5
|
+
/// Supports both ASCII fast-path and full Unicode characters.
|
|
5
6
|
#[inline]
|
|
6
7
|
pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if a.is_ascii() && b.is_ascii() {
|
|
9
|
+
indel_distance_slice(a.as_bytes(), b.as_bytes())
|
|
10
|
+
} else {
|
|
11
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
12
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
13
|
+
indel_distance_slice(&a_chars, &b_chars)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn indel_distance_slice<T: PartialEq>(a: &[T], b: &[T]) -> u32 {
|
|
9
18
|
let (m, n) = (a.len(), b.len());
|
|
10
19
|
if m == 0 { return n as u32; }
|
|
11
20
|
if n == 0 { return m as u32; }
|
|
@@ -19,10 +28,10 @@ pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
|
19
28
|
for i in 1..=m {
|
|
20
29
|
let mut prev_diag = row[0];
|
|
21
30
|
row[0] = i as u32;
|
|
22
|
-
let ai = a[i - 1];
|
|
31
|
+
let ai = &a[i - 1];
|
|
23
32
|
for j in 1..=n {
|
|
24
33
|
let old = row[j];
|
|
25
|
-
let cost = if ai == b[j - 1] { 0 } else { 2 };
|
|
34
|
+
let cost = if ai == &b[j - 1] { 0 } else { 2 };
|
|
26
35
|
row[j] = (prev_diag + cost).min(row[j] + 1).min(row[j - 1] + 1);
|
|
27
36
|
prev_diag = old;
|
|
28
37
|
}
|
|
@@ -34,9 +43,10 @@ pub fn indel_distance(a: &str, b: &str) -> u32 {
|
|
|
34
43
|
///
|
|
35
44
|
/// Formula: `(|a| + |b| - indel_distance) / (|a| + |b|) × 100`
|
|
36
45
|
///
|
|
37
|
-
/// Matches RapidFuzz and FuzzyWuzzy identically across all
|
|
46
|
+
/// Matches RapidFuzz and FuzzyWuzzy identically across all ASCII and multi-byte Unicode test cases.
|
|
38
47
|
pub fn ratio(s1: &str, s2: &str) -> f64 {
|
|
39
|
-
let
|
|
48
|
+
let len_a = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
49
|
+
let len_b = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
40
50
|
let total = len_a + len_b;
|
|
41
51
|
if total == 0 { return 100.0; }
|
|
42
52
|
let dist = indel_distance(s1, s2) as f64;
|
|
@@ -51,33 +61,41 @@ pub fn ratio(s1: &str, s2: &str) -> f64 {
|
|
|
51
61
|
pub fn partial_ratio(s1: &str, s2: &str) -> f64 {
|
|
52
62
|
if s1.is_empty() || s2.is_empty() { return 0.0; }
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
let
|
|
64
|
+
let s1_count = if s1.is_ascii() { s1.len() } else { s1.chars().count() };
|
|
65
|
+
let s2_count = if s2.is_ascii() { s2.len() } else { s2.chars().count() };
|
|
56
66
|
|
|
57
|
-
let
|
|
58
|
-
|
|
67
|
+
let (shorter, longer, short_chars, long_chars) = if s1_count <= s2_count {
|
|
68
|
+
(s1, s2, s1_count, s2_count)
|
|
69
|
+
} else {
|
|
70
|
+
(s2, s1, s2_count, s1_count)
|
|
71
|
+
};
|
|
59
72
|
|
|
60
|
-
if
|
|
73
|
+
if short_chars == long_chars {
|
|
61
74
|
return ratio(shorter, longer);
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if start > 0 && !longer.is_char_boundary(start) { continue; }
|
|
71
|
-
let end = start + short_len;
|
|
72
|
-
if end <= long_len && longer.is_char_boundary(end) {
|
|
73
|
-
let window = &longer[start..end];
|
|
77
|
+
if shorter.is_ascii() && longer.is_ascii() {
|
|
78
|
+
let short_bytes = shorter.as_bytes();
|
|
79
|
+
let long_bytes = longer.as_bytes();
|
|
80
|
+
let mut best = 0.0f64;
|
|
81
|
+
for start in 0..=(long_bytes.len() - short_bytes.len()) {
|
|
82
|
+
let window = &longer[start..start + short_bytes.len()];
|
|
74
83
|
let score = ratio(shorter, window);
|
|
75
84
|
if score > best { best = score; }
|
|
76
|
-
if best == 100.0 { return 100.0; }
|
|
85
|
+
if best == 100.0 { return 100.0; }
|
|
86
|
+
}
|
|
87
|
+
best
|
|
88
|
+
} else {
|
|
89
|
+
let longer_chars: Vec<char> = longer.chars().collect();
|
|
90
|
+
let mut best = 0.0f64;
|
|
91
|
+
for start in 0..=(longer_chars.len() - short_chars) {
|
|
92
|
+
let window: String = longer_chars[start..start + short_chars].iter().collect();
|
|
93
|
+
let score = ratio(shorter, &window);
|
|
94
|
+
if score > best { best = score; }
|
|
95
|
+
if best == 100.0 { return 100.0; }
|
|
77
96
|
}
|
|
97
|
+
best
|
|
78
98
|
}
|
|
79
|
-
|
|
80
|
-
best
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
/// Token sort ratio: sort tokens alphabetically, then compare.
|
|
@@ -108,12 +126,9 @@ pub fn token_set_ratio(s1: &str, s2: &str) -> f64 {
|
|
|
108
126
|
|
|
109
127
|
// If both strings have identical token sets
|
|
110
128
|
if diff1.is_empty() && diff2.is_empty() {
|
|
111
|
-
return
|
|
129
|
+
return 100.0;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
// t0 = intersection
|
|
115
|
-
// t1 = intersection + diff1
|
|
116
|
-
// t2 = intersection + diff2
|
|
117
132
|
let t0 = &inter_str;
|
|
118
133
|
|
|
119
134
|
let t1 = if inter_str.is_empty() {
|
|
@@ -155,15 +170,10 @@ pub fn ratio_batch(query: &str, candidates: &[&str]) -> Vec<f64> {
|
|
|
155
170
|
/// Extract top matches with partial-sort optimization.
|
|
156
171
|
///
|
|
157
172
|
/// Returns `Vec<(match_string, score, original_index)>` sorted by score descending.
|
|
158
|
-
///
|
|
159
|
-
/// Uses `select_nth_unstable_by` for O(n) top-K selection when `limit << choices.len()`,
|
|
160
|
-
/// and parallelizes scoring with Rayon for large choice sets.
|
|
161
173
|
pub fn extract(query: &str, choices: &[&str], score_cutoff: f64, limit: usize) -> Vec<(String, f64, usize)> {
|
|
162
174
|
if choices.is_empty() || limit == 0 { return vec![]; }
|
|
163
175
|
|
|
164
|
-
// Score all choices, filtering by cutoff.
|
|
165
176
|
let mut results: Vec<(String, f64, usize)> = if choices.len() > 1000 {
|
|
166
|
-
// Parallelize for large choice sets.
|
|
167
177
|
choices.par_iter().enumerate()
|
|
168
178
|
.filter_map(|(i, c)| {
|
|
169
179
|
let score = ratio(query, c);
|
|
@@ -181,14 +191,11 @@ pub fn extract(query: &str, choices: &[&str], score_cutoff: f64, limit: usize) -
|
|
|
181
191
|
|
|
182
192
|
if results.is_empty() { return results; }
|
|
183
193
|
|
|
184
|
-
// Optimized top-K: if we need far fewer results than we have,
|
|
185
|
-
// use partial sort instead of full sort.
|
|
186
194
|
if limit < results.len() {
|
|
187
195
|
results.select_nth_unstable_by(limit, |a, b| {
|
|
188
196
|
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
|
|
189
197
|
});
|
|
190
198
|
results.truncate(limit);
|
|
191
|
-
// Sort the top-K for deterministic output order.
|
|
192
199
|
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
193
200
|
} else {
|
|
194
201
|
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
|
@@ -212,7 +219,7 @@ pub fn extract_one(query: &str, choices: &[&str], score_cutoff: f64) -> Option<(
|
|
|
212
219
|
best = Some((c.to_string(), score, i));
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
if score == 100.0 { break; }
|
|
222
|
+
if score == 100.0 { break; }
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
best
|
|
@@ -224,23 +231,26 @@ mod tests {
|
|
|
224
231
|
|
|
225
232
|
#[test]
|
|
226
233
|
fn test_ratio_sorensen_dice() {
|
|
227
|
-
// "hello" (5) vs "hallo" (5): edit distance = 2 (sub cost = 2)
|
|
228
|
-
// Expected: (5 + 5 - 2) / (5 + 5) * 100 = 80.0
|
|
229
234
|
let r = ratio("hello", "hallo");
|
|
230
235
|
assert!((r - 80.0).abs() < 0.01, "Expected 80.0, got {}", r);
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
#[test]
|
|
239
|
+
fn test_ratio_unicode() {
|
|
240
|
+
let r = ratio("café", "cafe");
|
|
241
|
+
// "café" (4 chars), "cafe" (4 chars), indel distance = 2
|
|
242
|
+
// (4 + 4 - 2) / (4 + 4) * 100 = 75.0%
|
|
243
|
+
assert!((r - 75.0).abs() < 0.01, "Expected 75.0, got {}", r);
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
#[test]
|
|
234
247
|
fn test_ratio_asymmetric() {
|
|
235
|
-
// "a" (1) vs "abc" (3): edit distance = 2
|
|
236
|
-
// Expected: (1 + 3 - 2) / (1 + 3) * 100 = 50.0
|
|
237
248
|
let r = ratio("a", "abc");
|
|
238
249
|
assert!((r - 50.0).abs() < 0.01, "Expected 50.0, got {}", r);
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
#[test]
|
|
242
253
|
fn test_partial_ratio() {
|
|
243
|
-
// "hello" is a substring of "oh hello there"
|
|
244
254
|
let r = partial_ratio("hello", "oh hello there");
|
|
245
255
|
assert!(r >= 100.0 - 0.01, "Expected ~100.0, got {}", r);
|
|
246
256
|
}
|
|
@@ -254,11 +264,4 @@ mod tests {
|
|
|
254
264
|
assert_eq!(m, "apple");
|
|
255
265
|
assert!((s - 100.0).abs() < 0.01);
|
|
256
266
|
}
|
|
257
|
-
|
|
258
|
-
#[test]
|
|
259
|
-
fn test_extract_one_none_below_cutoff() {
|
|
260
|
-
let choices = vec!["zzzzz"];
|
|
261
|
-
let result = extract_one("apple", &choices, 90.0);
|
|
262
|
-
assert!(result.is_none());
|
|
263
|
-
}
|
|
264
267
|
}
|
|
@@ -3,11 +3,38 @@ use rayon::prelude::*;
|
|
|
3
3
|
/// Jaro similarity between two strings.
|
|
4
4
|
///
|
|
5
5
|
/// Returns 1.0 for identical strings (including both empty), 0.0 if no matches.
|
|
6
|
+
/// Supports both ASCII fast-path and full Unicode characters.
|
|
6
7
|
pub fn jaro(a: &str, b: &str) -> f64 {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if a.is_ascii() && b.is_ascii() {
|
|
9
|
+
jaro_bytes(a.as_bytes(), b.as_bytes())
|
|
10
|
+
} else {
|
|
11
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
12
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
13
|
+
jaro_chars(&a_chars, &b_chars)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn jaro_bytes(a: &[u8], b: &[u8]) -> f64 {
|
|
9
18
|
let (m, n) = (a.len(), b.len());
|
|
19
|
+
if m == 0 && n == 0 { return 1.0; }
|
|
20
|
+
if m == 0 || n == 0 { return 0.0; }
|
|
21
|
+
if a == b { return 1.0; }
|
|
22
|
+
|
|
23
|
+
let match_distance = (m.max(n) / 2).saturating_sub(1);
|
|
24
|
+
|
|
25
|
+
if m <= 128 && n <= 128 {
|
|
26
|
+
let mut a_matches = [false; 128];
|
|
27
|
+
let mut b_matches = [false; 128];
|
|
28
|
+
jaro_inner_slice(a, b, &mut a_matches[..m], &mut b_matches[..n], match_distance)
|
|
29
|
+
} else {
|
|
30
|
+
let mut a_matches = vec![false; m];
|
|
31
|
+
let mut b_matches = vec![false; n];
|
|
32
|
+
jaro_inner_slice(a, b, &mut a_matches, &mut b_matches, match_distance)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
10
35
|
|
|
36
|
+
fn jaro_chars(a: &[char], b: &[char]) -> f64 {
|
|
37
|
+
let (m, n) = (a.len(), b.len());
|
|
11
38
|
if m == 0 && n == 0 { return 1.0; }
|
|
12
39
|
if m == 0 || n == 0 { return 0.0; }
|
|
13
40
|
if a == b { return 1.0; }
|
|
@@ -17,25 +44,25 @@ pub fn jaro(a: &str, b: &str) -> f64 {
|
|
|
17
44
|
if m <= 128 && n <= 128 {
|
|
18
45
|
let mut a_matches = [false; 128];
|
|
19
46
|
let mut b_matches = [false; 128];
|
|
20
|
-
|
|
47
|
+
jaro_inner_slice(a, b, &mut a_matches[..m], &mut b_matches[..n], match_distance)
|
|
21
48
|
} else {
|
|
22
49
|
let mut a_matches = vec![false; m];
|
|
23
50
|
let mut b_matches = vec![false; n];
|
|
24
|
-
|
|
51
|
+
jaro_inner_slice(a, b, &mut a_matches, &mut b_matches, match_distance)
|
|
25
52
|
}
|
|
26
53
|
}
|
|
27
54
|
|
|
28
55
|
#[inline]
|
|
29
|
-
fn
|
|
56
|
+
fn jaro_inner_slice<T: PartialEq>(a: &[T], b: &[T], a_matches: &mut [bool], b_matches: &mut [bool], match_distance: usize) -> f64 {
|
|
30
57
|
let (m, n) = (a.len(), b.len());
|
|
31
58
|
let mut matches = 0u32;
|
|
32
59
|
|
|
33
60
|
for i in 0..m {
|
|
34
61
|
let lo = i.saturating_sub(match_distance);
|
|
35
62
|
let hi = (i + match_distance + 1).min(n);
|
|
36
|
-
let ai = a[i];
|
|
63
|
+
let ai = &a[i];
|
|
37
64
|
for j in lo..hi {
|
|
38
|
-
if b_matches[j] || ai != b[j] { continue; }
|
|
65
|
+
if b_matches[j] || ai != &b[j] { continue; }
|
|
39
66
|
a_matches[i] = true;
|
|
40
67
|
b_matches[j] = true;
|
|
41
68
|
matches += 1;
|
|
@@ -61,15 +88,27 @@ fn jaro_inner(a: &[u8], b: &[u8], a_matches: &mut [bool], b_matches: &mut [bool]
|
|
|
61
88
|
|
|
62
89
|
/// Jaro-Winkler similarity with prefix bonus.
|
|
63
90
|
/// `p` is the prefix weight (0.0–0.25, default 0.1).
|
|
91
|
+
/// Winkler prefix boost is applied when base Jaro similarity is >= 0.7 (Winkler 1990 standard).
|
|
64
92
|
pub fn jaro_winkler(a: &str, b: &str, p: f64) -> f64 {
|
|
65
93
|
if a == b { return 1.0; }
|
|
66
94
|
|
|
67
95
|
let jaro_score = jaro(a, b);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
if jaro_score < 0.7 {
|
|
97
|
+
return jaro_score;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let prefix_len = if a.is_ascii() && b.is_ascii() {
|
|
101
|
+
a.as_bytes().iter()
|
|
102
|
+
.zip(b.as_bytes().iter())
|
|
103
|
+
.take_while(|(x, y)| x == y)
|
|
104
|
+
.count()
|
|
105
|
+
.min(4)
|
|
106
|
+
} else {
|
|
107
|
+
a.chars().zip(b.chars())
|
|
108
|
+
.take_while(|(x, y)| x == y)
|
|
109
|
+
.count()
|
|
110
|
+
.min(4)
|
|
111
|
+
};
|
|
73
112
|
|
|
74
113
|
jaro_score + (prefix_len as f64 * p * (1.0 - jaro_score))
|
|
75
114
|
}
|
|
@@ -261,11 +300,13 @@ pub mod gpu_ext {
|
|
|
261
300
|
let mut max_len = 0u32;
|
|
262
301
|
for &i in indices {
|
|
263
302
|
let (a, b) = pairs[i];
|
|
264
|
-
chars_a.extend(a.
|
|
303
|
+
chars_a.extend(a.chars().map(|c| c as u32));
|
|
265
304
|
offsets_a.push(chars_a.len() as u32);
|
|
266
|
-
chars_b.extend(b.
|
|
305
|
+
chars_b.extend(b.chars().map(|c| c as u32));
|
|
267
306
|
offsets_b.push(chars_b.len() as u32);
|
|
268
|
-
|
|
307
|
+
let a_count = a.chars().count();
|
|
308
|
+
let b_count = b.chars().count();
|
|
309
|
+
max_len = max_len.max(a_count.max(b_count) as u32);
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
if chars_a.is_empty() { chars_a.push(0); }
|
|
@@ -341,7 +382,7 @@ pub mod gpu_ext {
|
|
|
341
382
|
let mut chars_a: Vec<u32> = Vec::new();
|
|
342
383
|
offsets_a.push(0);
|
|
343
384
|
for a in list_a {
|
|
344
|
-
chars_a.extend(a.
|
|
385
|
+
chars_a.extend(a.chars().map(|c| c as u32));
|
|
345
386
|
offsets_a.push(chars_a.len() as u32);
|
|
346
387
|
}
|
|
347
388
|
|
|
@@ -350,7 +391,7 @@ pub mod gpu_ext {
|
|
|
350
391
|
let mut chars_b: Vec<u32> = Vec::new();
|
|
351
392
|
offsets_b.push(0);
|
|
352
393
|
for b in list_b {
|
|
353
|
-
chars_b.extend(b.
|
|
394
|
+
chars_b.extend(b.chars().map(|c| c as u32));
|
|
354
395
|
offsets_b.push(chars_b.len() as u32);
|
|
355
396
|
}
|
|
356
397
|
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
use rayon::prelude::*;
|
|
2
2
|
|
|
3
3
|
/// Standard single-row DP Levenshtein distance with diagonal optimization.
|
|
4
|
+
/// Supports both ASCII (byte-fast path) and Unicode characters (scalar value codepoint path).
|
|
4
5
|
pub fn levenshtein_distance_raw(a: &str, b: &str) -> u32 {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
if a.is_ascii() && b.is_ascii() {
|
|
7
|
+
levenshtein_distance_slice(a.as_bytes(), b.as_bytes())
|
|
8
|
+
} else {
|
|
9
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
10
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
11
|
+
levenshtein_distance_slice(&a_chars, &b_chars)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn levenshtein_distance_slice<T: PartialEq>(a: &[T], b: &[T]) -> u32 {
|
|
7
16
|
let (m, n) = (a.len(), b.len());
|
|
8
17
|
if m == 0 { return n as u32; }
|
|
9
18
|
if n == 0 { return m as u32; }
|
|
@@ -18,10 +27,10 @@ pub fn levenshtein_distance_raw(a: &str, b: &str) -> u32 {
|
|
|
18
27
|
for i in 1..=m {
|
|
19
28
|
let mut prev_diag = row[0];
|
|
20
29
|
row[0] = i as u32;
|
|
21
|
-
let ai = a[i - 1];
|
|
30
|
+
let ai = &a[i - 1];
|
|
22
31
|
for j in 1..=n {
|
|
23
32
|
let old = row[j];
|
|
24
|
-
let cost = if ai == b[j - 1] { 0 } else { 1 };
|
|
33
|
+
let cost = if ai == &b[j - 1] { 0 } else { 1 };
|
|
25
34
|
row[j] = (prev_diag + cost).min(row[j] + 1).min(row[j - 1] + 1);
|
|
26
35
|
prev_diag = old;
|
|
27
36
|
}
|
|
@@ -158,7 +167,9 @@ pub mod gpu_ext {
|
|
|
158
167
|
|
|
159
168
|
for (i, (a, b)) in pairs.iter().enumerate() {
|
|
160
169
|
if a.is_empty() || b.is_empty() {
|
|
161
|
-
|
|
170
|
+
let a_count = if a.is_ascii() { a.len() } else { a.chars().count() };
|
|
171
|
+
let b_count = if b.is_ascii() { b.len() } else { b.chars().count() };
|
|
172
|
+
results[i] = (a_count.max(b_count)) as u32;
|
|
162
173
|
} else if *a == *b {
|
|
163
174
|
results[i] = 0;
|
|
164
175
|
} else if a.len() > GPU_MAX_STRING_LEN || b.len() > GPU_MAX_STRING_LEN {
|
|
@@ -214,11 +225,13 @@ pub mod gpu_ext {
|
|
|
214
225
|
let mut max_len = 0u32;
|
|
215
226
|
for &i in indices {
|
|
216
227
|
let (a, b) = pairs[i];
|
|
217
|
-
chars_a.extend(a.
|
|
228
|
+
chars_a.extend(a.chars().map(|c| c as u32));
|
|
218
229
|
offsets_a.push(chars_a.len() as u32);
|
|
219
|
-
chars_b.extend(b.
|
|
230
|
+
chars_b.extend(b.chars().map(|c| c as u32));
|
|
220
231
|
offsets_b.push(chars_b.len() as u32);
|
|
221
|
-
|
|
232
|
+
let a_count = a.chars().count();
|
|
233
|
+
let b_count = b.chars().count();
|
|
234
|
+
max_len = max_len.max(a_count.max(b_count) as u32);
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
if chars_a.is_empty() { chars_a.push(0); }
|
|
@@ -329,7 +342,7 @@ pub mod gpu_ext {
|
|
|
329
342
|
let mut chars_a: Vec<u32> = Vec::new();
|
|
330
343
|
offsets_a.push(0);
|
|
331
344
|
for a in list_a {
|
|
332
|
-
chars_a.extend(a.
|
|
345
|
+
chars_a.extend(a.chars().map(|c| c as u32));
|
|
333
346
|
offsets_a.push(chars_a.len() as u32);
|
|
334
347
|
}
|
|
335
348
|
|
|
@@ -338,7 +351,7 @@ pub mod gpu_ext {
|
|
|
338
351
|
let mut chars_b: Vec<u32> = Vec::new();
|
|
339
352
|
offsets_b.push(0);
|
|
340
353
|
for b in list_b {
|
|
341
|
-
chars_b.extend(b.
|
|
354
|
+
chars_b.extend(b.chars().map(|c| c as u32));
|
|
342
355
|
offsets_b.push(chars_b.len() as u32);
|
|
343
356
|
}
|
|
344
357
|
|
|
@@ -3,17 +3,32 @@ use rayon::prelude::*;
|
|
|
3
3
|
/// Needleman-Wunsch global alignment score with linear gap penalty.
|
|
4
4
|
///
|
|
5
5
|
/// Uses single-row DP + scalar diagonal for minimal memory.
|
|
6
|
+
/// Supports both ASCII fast-path and full Unicode characters.
|
|
6
7
|
pub fn needleman_wunsch(a: &str, b: &str, match_score: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if a.is_ascii() && b.is_ascii() {
|
|
9
|
+
needleman_wunsch_bytes(a.as_bytes(), b.as_bytes(), match_score, mismatch_score, gap_penalty)
|
|
10
|
+
} else {
|
|
11
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
12
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
13
|
+
needleman_wunsch_chars(&a_chars, &b_chars, match_score, mismatch_score, gap_penalty)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn needleman_wunsch_bytes(a: &[u8], b: &[u8], match_score: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
|
|
18
|
+
needleman_wunsch_slice(a, b, match_score, mismatch_score, gap_penalty)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn needleman_wunsch_chars(a: &[char], b: &[char], match_score: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
|
|
22
|
+
needleman_wunsch_slice(a, b, match_score, mismatch_score, gap_penalty)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn needleman_wunsch_slice<T: PartialEq>(a: &[T], b: &[T], match_score: i32, mismatch_score: i32, gap_penalty: i32) -> i32 {
|
|
9
26
|
let (m, n) = (a.len(), b.len());
|
|
10
27
|
|
|
11
28
|
if m == 0 { return (n as i32) * gap_penalty; }
|
|
12
29
|
if n == 0 { return (m as i32) * gap_penalty; }
|
|
13
|
-
// Fast path: identical strings.
|
|
14
30
|
if a == b { return (m as i32) * match_score; }
|
|
15
31
|
|
|
16
|
-
// Single-row + diagonal optimization: halves memory.
|
|
17
32
|
let mut row = vec![0i32; n + 1];
|
|
18
33
|
for (j, item) in row.iter_mut().enumerate() {
|
|
19
34
|
*item = (j as i32) * gap_penalty;
|
|
@@ -22,10 +37,10 @@ pub fn needleman_wunsch(a: &str, b: &str, match_score: i32, mismatch_score: i32,
|
|
|
22
37
|
for i in 1..=m {
|
|
23
38
|
let mut prev_diag = row[0];
|
|
24
39
|
row[0] = (i as i32) * gap_penalty;
|
|
25
|
-
let ai = a[i - 1];
|
|
40
|
+
let ai = &a[i - 1];
|
|
26
41
|
for j in 1..=n {
|
|
27
42
|
let old = row[j];
|
|
28
|
-
let score = if ai == b[j - 1] { match_score } else { mismatch_score };
|
|
43
|
+
let score = if ai == &b[j - 1] { match_score } else { mismatch_score };
|
|
29
44
|
row[j] = (prev_diag + score)
|
|
30
45
|
.max(row[j] + gap_penalty)
|
|
31
46
|
.max(row[j - 1] + gap_penalty);
|
|
@@ -50,10 +65,6 @@ const NEG_INF: i32 = -1_000_000_000;
|
|
|
50
65
|
/// Needleman-Wunsch global alignment score with affine gap penalties (Gotoh 1982 algorithm).
|
|
51
66
|
///
|
|
52
67
|
/// Affine model: gap of length k costs `gap_open + k * gap_extend`.
|
|
53
|
-
/// Uses 3 DP matrices / rows:
|
|
54
|
-
/// - M(i, j): score where a[i-1] aligns with b[j-1]
|
|
55
|
-
/// - Ix(i, j): score where a[i-1] aligns with a gap (deletion in b)
|
|
56
|
-
/// - Iy(i, j): score where b[j-1] aligns with a gap (insertion in b)
|
|
57
68
|
pub fn needleman_wunsch_affine(
|
|
58
69
|
a: &str,
|
|
59
70
|
b: &str,
|
|
@@ -62,8 +73,23 @@ pub fn needleman_wunsch_affine(
|
|
|
62
73
|
gap_open: i32,
|
|
63
74
|
gap_extend: i32,
|
|
64
75
|
) -> i32 {
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
if a.is_ascii() && b.is_ascii() {
|
|
77
|
+
needleman_wunsch_affine_slice(a.as_bytes(), b.as_bytes(), match_score, mismatch_score, gap_open, gap_extend)
|
|
78
|
+
} else {
|
|
79
|
+
let a_chars: Vec<char> = a.chars().collect();
|
|
80
|
+
let b_chars: Vec<char> = b.chars().collect();
|
|
81
|
+
needleman_wunsch_affine_slice(&a_chars, &b_chars, match_score, mismatch_score, gap_open, gap_extend)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn needleman_wunsch_affine_slice<T: PartialEq>(
|
|
86
|
+
a: &[T],
|
|
87
|
+
b: &[T],
|
|
88
|
+
match_score: i32,
|
|
89
|
+
mismatch_score: i32,
|
|
90
|
+
gap_open: i32,
|
|
91
|
+
gap_extend: i32,
|
|
92
|
+
) -> i32 {
|
|
67
93
|
let (m, n) = (a.len(), b.len());
|
|
68
94
|
|
|
69
95
|
if m == 0 && n == 0 { return 0; }
|
|
@@ -71,7 +97,6 @@ pub fn needleman_wunsch_affine(
|
|
|
71
97
|
if n == 0 { return gap_open + (m as i32) * gap_extend; }
|
|
72
98
|
if a == b { return (m as i32) * match_score; }
|
|
73
99
|
|
|
74
|
-
// Memory-efficient O(N) space implementation using 3 rows
|
|
75
100
|
let mut m_row = vec![NEG_INF; n + 1];
|
|
76
101
|
let mut ix_row = vec![NEG_INF; n + 1];
|
|
77
102
|
let mut iy_row = vec![NEG_INF; n + 1];
|
|
@@ -93,23 +118,18 @@ pub fn needleman_wunsch_affine(
|
|
|
93
118
|
m_row[0] = gap_cost_i;
|
|
94
119
|
iy_row[0] = NEG_INF;
|
|
95
120
|
|
|
96
|
-
let ai = a[i - 1];
|
|
121
|
+
let ai = &a[i - 1];
|
|
97
122
|
|
|
98
123
|
for j in 1..=n {
|
|
99
|
-
let bj = b[j - 1];
|
|
124
|
+
let bj = &b[j - 1];
|
|
100
125
|
let sub_score = if ai == bj { match_score } else { mismatch_score };
|
|
101
126
|
|
|
102
|
-
// Diagonal best score from (i-1, j-1)
|
|
103
127
|
let prev_diag_best = prev_m_diag.max(prev_ix_diag).max(prev_iy_diag);
|
|
104
128
|
let new_m = prev_diag_best + sub_score;
|
|
105
129
|
|
|
106
|
-
// Ix: extension or new gap in B from row i-1
|
|
107
130
|
let new_ix = (ix_row[j] + gap_extend).max(m_row[j] + gap_open + gap_extend).max(iy_row[j] + gap_open + gap_extend);
|
|
108
|
-
|
|
109
|
-
// Iy: extension or new gap in A from col j-1
|
|
110
131
|
let new_iy = (iy_row[j - 1] + gap_extend).max(m_row[j - 1] + gap_open + gap_extend).max(ix_row[j - 1] + gap_open + gap_extend);
|
|
111
132
|
|
|
112
|
-
// Save old states for next diagonal
|
|
113
133
|
prev_m_diag = m_row[j];
|
|
114
134
|
prev_ix_diag = ix_row[j];
|
|
115
135
|
prev_iy_diag = iy_row[j];
|
|
@@ -147,8 +167,6 @@ mod tests {
|
|
|
147
167
|
let s2 = "AGCT";
|
|
148
168
|
assert_eq!(needleman_wunsch_affine(s1, s2, 2, -1, -3, -1), 8);
|
|
149
169
|
|
|
150
|
-
// Gap open -3, extend -1: 2-char gap costs -3 + 2*(-1) = -5
|
|
151
|
-
// vs linear with gap -2: 2-char gap costs 2*(-2) = -4
|
|
152
170
|
let score = needleman_wunsch_affine("ACGT", "AT", 2, -1, -3, -1);
|
|
153
171
|
assert!(score < 8);
|
|
154
172
|
}
|
|
@@ -104,18 +104,20 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
104
104
|
let t_f = f32(transpositions);
|
|
105
105
|
let jaro = (m_f / a_f + m_f / b_f + (m_f - t_f / 2.0) / m_f) / 3.0;
|
|
106
106
|
|
|
107
|
-
// Winkler prefix bonus
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
// Winkler prefix bonus (standard Winkler 1990: only when jaro >= 0.7)
|
|
108
|
+
var jw = jaro;
|
|
109
|
+
if (jaro >= 0.7) {
|
|
110
|
+
let p = bitcast<f32>(params.winkler_p_bits);
|
|
111
|
+
var prefix_len = 0u;
|
|
112
|
+
let max_prefix = min(min(a_len, b_len), 4u);
|
|
113
|
+
for (var i = 0u; i < max_prefix; i++) {
|
|
114
|
+
if (chars_a[a_start + i] == chars_b[b_start + i]) {
|
|
115
|
+
prefix_len += 1u;
|
|
116
|
+
} else {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
116
119
|
}
|
|
120
|
+
jw = jaro + f32(prefix_len) * p * (1.0 - jaro);
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
let jw = jaro + f32(prefix_len) * p * (1.0 - jaro);
|
|
120
122
|
results[pair_idx] = bitcast<u32>(jw);
|
|
121
123
|
}
|
|
@@ -104,17 +104,20 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
104
104
|
let t_f = f32(transpositions);
|
|
105
105
|
let jaro = (m_f / a_f + m_f / b_f + (m_f - t_f / 2.0) / m_f) / 3.0;
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
var
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
// Winkler prefix bonus (standard Winkler 1990: only when jaro >= 0.7)
|
|
108
|
+
var jw = jaro;
|
|
109
|
+
if (jaro >= 0.7) {
|
|
110
|
+
let p = bitcast<f32>(params.winkler_p_bits);
|
|
111
|
+
var prefix_len = 0u;
|
|
112
|
+
let max_prefix = min(min(a_len, b_len), 4u);
|
|
113
|
+
for (var i = 0u; i < max_prefix; i++) {
|
|
114
|
+
if (chars_a[a_start + i] == chars_b[b_start + i]) {
|
|
115
|
+
prefix_len += 1u;
|
|
116
|
+
} else {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
115
119
|
}
|
|
120
|
+
jw = jaro + f32(prefix_len) * p * (1.0 - jaro);
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
let jw = jaro + f32(prefix_len) * p * (1.0 - jaro);
|
|
119
122
|
matrix[out_idx] = bitcast<u32>(jw);
|
|
120
123
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "fuzzgpu-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Python bindings for FuzzGPU: GPU-accelerated fuzzy string matching engine"
|
|
6
6
|
license = "MIT"
|
|
@@ -17,6 +17,6 @@ default = ["gpu"]
|
|
|
17
17
|
gpu = ["fuzzgpu-core/gpu"]
|
|
18
18
|
|
|
19
19
|
[dependencies]
|
|
20
|
-
fuzzgpu-core = { path = "../fuzzgpu-core" }
|
|
20
|
+
fuzzgpu-core = { path = "../fuzzgpu-core", version = "0.1.1", default-features = false }
|
|
21
21
|
pyo3 = { workspace = true, features = ["abi3-py39"] }
|
|
22
22
|
pollster = { workspace = true }
|
|
@@ -119,11 +119,13 @@ fn jaro_similarity(a: &str, b: &str) -> PyResult<f64> {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
#[pyfunction]
|
|
122
|
+
#[pyo3(signature = (a, b, p = 0.1))]
|
|
122
123
|
fn jaro_winkler_similarity(a: &str, b: &str, p: f64) -> PyResult<f64> {
|
|
123
124
|
Ok(fuzzgpu_core::jaro_winkler(a, b, p))
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
#[pyfunction]
|
|
128
|
+
#[pyo3(signature = (query, candidates, p = 0.1))]
|
|
127
129
|
fn jaro_winkler_batch_fn(py: Python, query: String, candidates: Vec<String>, p: f64) -> PyResult<Vec<f64>> {
|
|
128
130
|
let pairs: Vec<(&str, &str)> = candidates.iter().map(|c| (query.as_str(), c.as_str())).collect();
|
|
129
131
|
#[cfg(feature = "gpu")]
|
|
@@ -144,6 +146,7 @@ fn jaro_winkler_batch_fn(py: Python, query: String, candidates: Vec<String>, p:
|
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
#[pyfunction]
|
|
149
|
+
#[pyo3(signature = (list_a, list_b, p = 0.1))]
|
|
147
150
|
fn jaro_winkler_cdist(py: Python, list_a: Vec<String>, list_b: Vec<String>, p: f64) -> PyResult<Vec<Vec<f64>>> {
|
|
148
151
|
let refs_a: Vec<&str> = list_a.iter().map(|s| s.as_str()).collect();
|
|
149
152
|
let refs_b: Vec<&str> = list_b.iter().map(|s| s.as_str()).collect();
|
|
@@ -268,6 +271,6 @@ fn fuzzgpu(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
268
271
|
m.add_function(wrap_pyfunction!(jaro_optimized, m)?)?;
|
|
269
272
|
// GPU info & version
|
|
270
273
|
m.add_function(wrap_pyfunction!(gpu_info, m)?)?;
|
|
271
|
-
m.add("__version__", "0.1.
|
|
274
|
+
m.add("__version__", "0.1.1")?;
|
|
272
275
|
Ok(())
|
|
273
276
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fuzzgpu"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
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" }
|
|
@@ -19,9 +19,9 @@ Usage:
|
|
|
19
19
|
score = fuzzgpu.needleman_wunsch_score("AGTACGCA", "TATGC", 2, -1, -2)
|
|
20
20
|
score_affine = fuzzgpu.needleman_wunsch_affine("AGTACGCA", "TATGC", 2, -1, -3, -1)
|
|
21
21
|
|
|
22
|
-
# Jaro-Winkler similarity (GPU-accelerated for batches)
|
|
23
|
-
sim = fuzzgpu.
|
|
24
|
-
jw_batch = fuzzgpu.jaro_winkler_batch("MARTHA", ["MARHTA", "MATRH"]
|
|
22
|
+
# Jaro & Jaro-Winkler similarity (GPU-accelerated for batches)
|
|
23
|
+
sim = fuzzgpu.jaro_winkler("MARTHA", "MARHTA") # 0.96 (default p=0.1)
|
|
24
|
+
jw_batch = fuzzgpu.jaro_winkler_batch("MARTHA", ["MARHTA", "MATRH"])
|
|
25
25
|
|
|
26
26
|
# Fuzzy matching (rapidfuzz-compatible)
|
|
27
27
|
from fuzzgpu.fuzz import ratio, partial_ratio, token_sort_ratio, token_set_ratio, extract, extractOne
|
|
@@ -65,11 +65,14 @@ from fuzzgpu.fuzzgpu import (
|
|
|
65
65
|
__version__,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
# Re-export with clean names
|
|
68
|
+
# Re-export with clean names & intuitive aliases
|
|
69
69
|
levenshtein = levenshtein_distance
|
|
70
70
|
damerau_levenshtein = damerau_levenshtein_distance
|
|
71
|
+
damerau = damerau_levenshtein_distance
|
|
71
72
|
needleman_wunsch = needleman_wunsch_score
|
|
72
73
|
needleman_wunsch_batch = needleman_wunsch_batch_fn
|
|
74
|
+
jaro = jaro_similarity
|
|
75
|
+
jaro_winkler = jaro_winkler_similarity
|
|
73
76
|
jaro_winkler_batch = jaro_winkler_batch_fn
|
|
74
77
|
ratio = fuzz_ratio
|
|
75
78
|
partial_ratio = fuzz_partial_ratio
|
|
@@ -90,7 +93,7 @@ __all__ = [
|
|
|
90
93
|
"levenshtein_myers", "needleman_wunsch_striped", "jaro_optimized",
|
|
91
94
|
"gpu_info", "__version__",
|
|
92
95
|
# Aliases
|
|
93
|
-
"levenshtein", "damerau_levenshtein", "needleman_wunsch", "needleman_wunsch_batch",
|
|
94
|
-
"
|
|
95
|
-
"wratio", "ratio_batch", "extract", "extractOne",
|
|
96
|
+
"levenshtein", "damerau_levenshtein", "damerau", "needleman_wunsch", "needleman_wunsch_batch",
|
|
97
|
+
"jaro", "jaro_winkler", "jaro_winkler_batch", "ratio", "partial_ratio",
|
|
98
|
+
"token_sort_ratio", "token_set_ratio", "wratio", "ratio_batch", "extract", "extractOne",
|
|
96
99
|
]
|
|
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
|