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.
Files changed (25) hide show
  1. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/Cargo.lock +2 -2
  2. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/PKG-INFO +3 -3
  3. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/README.md +2 -2
  4. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/Cargo.toml +1 -1
  5. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/damerau.rs +85 -12
  6. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/fuzz.rs +52 -49
  7. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/jaro.rs +58 -17
  8. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/levenshtein.rs +23 -10
  9. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/needleman.rs +40 -22
  10. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/jaro.wgsl +13 -11
  11. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/jaro_matrix.wgsl +13 -10
  12. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-python/Cargo.toml +2 -2
  13. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-python/src/lib.rs +4 -1
  14. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/pyproject.toml +1 -1
  15. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/python/fuzzgpu/__init__.py +10 -7
  16. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/Cargo.toml +0 -0
  17. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/LICENSE +0 -0
  18. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/assets/logo.png +0 -0
  19. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/assets/logo.svg +0 -0
  20. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/gpu.rs +0 -0
  21. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/lib.rs +0 -0
  22. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/levenshtein.wgsl +0 -0
  23. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/shaders/levenshtein_matrix.wgsl +0 -0
  24. {fuzzgpu-0.1.0 → fuzzgpu-0.1.1}/crates/fuzzgpu-core/src/simd.rs +0 -0
  25. {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.0"
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.0"
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.0
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
38
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.1-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
39
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
40
40
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?style=flat-square)](https://www.rust-lang.org)
41
41
  [![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)
@@ -100,7 +100,7 @@ pip install fuzzgpu
100
100
  ### Rust (Cargo.toml)
101
101
  ```toml
102
102
  [dependencies]
103
- fuzzgpu-core = "0.1.0"
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
- [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
11
+ [![PyPI Version](https://img.shields.io/badge/pypi-v0.1.1-blue.svg?style=flat-square)](https://pypi.org/project/fuzzgpu/)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://opensource.org/licenses/MIT)
13
13
  [![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?style=flat-square)](https://www.rust-lang.org)
14
14
  [![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)
@@ -73,7 +73,7 @@ pip install fuzzgpu
73
73
  ### Rust (Cargo.toml)
74
74
  ```toml
75
75
  [dependencies]
76
- fuzzgpu-core = "0.1.0"
76
+ fuzzgpu-core = "0.1.1"
77
77
  ```
78
78
 
79
79
  ---
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fuzzgpu-core"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  description = "GPU-accelerated fuzzy string matching engine"
6
6
  license = "MIT"
@@ -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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
9
- let (m, n) = (a.len(), b.len());
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; rows * cols];
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; // last column where character appeared in b
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 (len_a, len_b) = (s1.len(), s2.len());
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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
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 symmetric and asymmetric test cases.
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 (len_a, len_b) = (s1.len(), s2.len());
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
- // Ensure `shorter` is the shorter string.
55
- let (shorter, longer) = if s1.len() <= s2.len() { (s1, s2) } else { (s2, s1) };
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 short_len = shorter.len();
58
- let long_len = longer.len();
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 short_len == long_len {
73
+ if short_chars == long_chars {
61
74
  return ratio(shorter, longer);
62
75
  }
63
76
 
64
-
65
- let mut best = 0.0f64;
66
-
67
- // Slide a window of `short_len` across `longer`.
68
- for start in 0..=(long_len - short_len) {
69
- // Only compare at valid UTF-8 boundaries.
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; } // Early exit: can't do better.
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 if inter_str.is_empty() { 100.0 } else { 100.0 };
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; } // Can't do better.
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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
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
- jaro_inner(a, b, &mut a_matches[..m], &mut b_matches[..n], match_distance)
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
- jaro_inner(a, b, &mut a_matches, &mut b_matches, match_distance)
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 jaro_inner(a: &[u8], b: &[u8], a_matches: &mut [bool], b_matches: &mut [bool], match_distance: usize) -> f64 {
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
- let prefix_len = a.as_bytes().iter()
69
- .zip(b.as_bytes().iter())
70
- .take_while(|(x, y)| x == y)
71
- .count()
72
- .min(4);
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.bytes().map(|c| c as u32));
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.bytes().map(|c| c as u32));
305
+ chars_b.extend(b.chars().map(|c| c as u32));
267
306
  offsets_b.push(chars_b.len() as u32);
268
- max_len = max_len.max(a.len().max(b.len()) as u32);
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.bytes().map(|c| c as u32));
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.bytes().map(|c| c as u32));
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
- let a = a.as_bytes();
6
- let b = b.as_bytes();
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
- results[i] = (a.len().max(b.len())) as u32;
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.bytes().map(|c| c as u32));
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.bytes().map(|c| c as u32));
230
+ chars_b.extend(b.chars().map(|c| c as u32));
220
231
  offsets_b.push(chars_b.len() as u32);
221
- max_len = max_len.max(a.len().max(b.len()) as u32);
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.bytes().map(|c| c as u32));
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.bytes().map(|c| c as u32));
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
- let a = a.as_bytes();
8
- let b = b.as_bytes();
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
- let a = a.as_bytes();
66
- let b = b.as_bytes();
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
- let p = bitcast<f32>(params.winkler_p_bits);
109
- var prefix_len = 0u;
110
- let max_prefix = min(min(a_len, b_len), 4u);
111
- for (var i = 0u; i < max_prefix; i++) {
112
- if (chars_a[a_start + i] == chars_b[b_start + i]) {
113
- prefix_len += 1u;
114
- } else {
115
- break;
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
- let p = bitcast<f32>(params.winkler_p_bits);
108
- var prefix_len = 0u;
109
- let max_prefix = min(min(a_len, b_len), 4u);
110
- for (var i = 0u; i < max_prefix; i++) {
111
- if (chars_a[a_start + i] == chars_b[b_start + i]) {
112
- prefix_len += 1u;
113
- } else {
114
- break;
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.0"
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.0")?;
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.0"
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.jaro_winkler_similarity("MARTHA", "MARHTA", 0.1) # 0.96
24
- jw_batch = fuzzgpu.jaro_winkler_batch("MARTHA", ["MARHTA", "MATRH"], 0.1)
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
- "jaro_winkler_batch", "ratio", "partial_ratio", "token_sort_ratio", "token_set_ratio",
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