stride-align 0.6.0

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 (190) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/NOTICE +255 -0
  4. data/README.md +107 -0
  5. data/data/bmpm_data/gen_approx_any.txt +131 -0
  6. data/data/bmpm_data/gen_approx_arabic.txt +26 -0
  7. data/data/bmpm_data/gen_approx_common.txt +233 -0
  8. data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
  9. data/data/bmpm_data/gen_approx_czech.txt +18 -0
  10. data/data/bmpm_data/gen_approx_dutch.txt +18 -0
  11. data/data/bmpm_data/gen_approx_english.txt +47 -0
  12. data/data/bmpm_data/gen_approx_french.txt +25 -0
  13. data/data/bmpm_data/gen_approx_german.txt +73 -0
  14. data/data/bmpm_data/gen_approx_greek.txt +18 -0
  15. data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
  16. data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
  17. data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
  18. data/data/bmpm_data/gen_approx_italian.txt +18 -0
  19. data/data/bmpm_data/gen_approx_polish.txt +84 -0
  20. data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
  21. data/data/bmpm_data/gen_approx_romanian.txt +18 -0
  22. data/data/bmpm_data/gen_approx_russian.txt +48 -0
  23. data/data/bmpm_data/gen_approx_spanish.txt +21 -0
  24. data/data/bmpm_data/gen_approx_turkish.txt +18 -0
  25. data/data/bmpm_data/gen_exact_any.txt +40 -0
  26. data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
  27. data/data/bmpm_data/gen_exact_arabic.txt +18 -0
  28. data/data/bmpm_data/gen_exact_common.txt +32 -0
  29. data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
  30. data/data/bmpm_data/gen_exact_czech.txt +18 -0
  31. data/data/bmpm_data/gen_exact_dutch.txt +18 -0
  32. data/data/bmpm_data/gen_exact_english.txt +18 -0
  33. data/data/bmpm_data/gen_exact_french.txt +18 -0
  34. data/data/bmpm_data/gen_exact_german.txt +18 -0
  35. data/data/bmpm_data/gen_exact_greek.txt +18 -0
  36. data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
  37. data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
  38. data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
  39. data/data/bmpm_data/gen_exact_italian.txt +18 -0
  40. data/data/bmpm_data/gen_exact_polish.txt +23 -0
  41. data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
  42. data/data/bmpm_data/gen_exact_romanian.txt +18 -0
  43. data/data/bmpm_data/gen_exact_russian.txt +19 -0
  44. data/data/bmpm_data/gen_exact_spanish.txt +19 -0
  45. data/data/bmpm_data/gen_exact_turkish.txt +18 -0
  46. data/data/bmpm_data/gen_hebrew_common.txt +113 -0
  47. data/data/bmpm_data/gen_lang.txt +295 -0
  48. data/data/bmpm_data/gen_languages.txt +36 -0
  49. data/data/bmpm_data/gen_rules_any.txt +367 -0
  50. data/data/bmpm_data/gen_rules_arabic.txt +76 -0
  51. data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
  52. data/data/bmpm_data/gen_rules_czech.txt +67 -0
  53. data/data/bmpm_data/gen_rules_dutch.txt +78 -0
  54. data/data/bmpm_data/gen_rules_english.txt +113 -0
  55. data/data/bmpm_data/gen_rules_french.txt +114 -0
  56. data/data/bmpm_data/gen_rules_german.txt +129 -0
  57. data/data/bmpm_data/gen_rules_greek.txt +97 -0
  58. data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
  59. data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
  60. data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
  61. data/data/bmpm_data/gen_rules_italian.txt +77 -0
  62. data/data/bmpm_data/gen_rules_polish.txt +185 -0
  63. data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
  64. data/data/bmpm_data/gen_rules_romanian.txt +64 -0
  65. data/data/bmpm_data/gen_rules_russian.txt +142 -0
  66. data/data/bmpm_data/gen_rules_spanish.txt +85 -0
  67. data/data/bmpm_data/gen_rules_turkish.txt +50 -0
  68. data/data/keyboard_data/qwerty.npy +0 -0
  69. data/data/matrix_data/BLOSUM100 +31 -0
  70. data/data/matrix_data/BLOSUM30 +31 -0
  71. data/data/matrix_data/BLOSUM35 +31 -0
  72. data/data/matrix_data/BLOSUM40 +31 -0
  73. data/data/matrix_data/BLOSUM45 +25 -0
  74. data/data/matrix_data/BLOSUM50 +25 -0
  75. data/data/matrix_data/BLOSUM55 +31 -0
  76. data/data/matrix_data/BLOSUM60 +31 -0
  77. data/data/matrix_data/BLOSUM62 +25 -0
  78. data/data/matrix_data/BLOSUM65 +31 -0
  79. data/data/matrix_data/BLOSUM70 +31 -0
  80. data/data/matrix_data/BLOSUM75 +31 -0
  81. data/data/matrix_data/BLOSUM80 +25 -0
  82. data/data/matrix_data/BLOSUM85 +31 -0
  83. data/data/matrix_data/BLOSUM90 +25 -0
  84. data/data/matrix_data/NUC.4.4 +25 -0
  85. data/data/matrix_data/PAM10 +34 -0
  86. data/data/matrix_data/PAM100 +34 -0
  87. data/data/matrix_data/PAM110 +34 -0
  88. data/data/matrix_data/PAM120 +34 -0
  89. data/data/matrix_data/PAM130 +34 -0
  90. data/data/matrix_data/PAM140 +34 -0
  91. data/data/matrix_data/PAM150 +34 -0
  92. data/data/matrix_data/PAM160 +34 -0
  93. data/data/matrix_data/PAM170 +34 -0
  94. data/data/matrix_data/PAM180 +34 -0
  95. data/data/matrix_data/PAM190 +34 -0
  96. data/data/matrix_data/PAM20 +34 -0
  97. data/data/matrix_data/PAM200 +34 -0
  98. data/data/matrix_data/PAM210 +34 -0
  99. data/data/matrix_data/PAM220 +34 -0
  100. data/data/matrix_data/PAM230 +34 -0
  101. data/data/matrix_data/PAM240 +34 -0
  102. data/data/matrix_data/PAM250 +25 -0
  103. data/data/matrix_data/PAM260 +34 -0
  104. data/data/matrix_data/PAM270 +34 -0
  105. data/data/matrix_data/PAM280 +34 -0
  106. data/data/matrix_data/PAM290 +34 -0
  107. data/data/matrix_data/PAM30 +25 -0
  108. data/data/matrix_data/PAM300 +34 -0
  109. data/data/matrix_data/PAM310 +34 -0
  110. data/data/matrix_data/PAM320 +34 -0
  111. data/data/matrix_data/PAM330 +34 -0
  112. data/data/matrix_data/PAM340 +34 -0
  113. data/data/matrix_data/PAM350 +34 -0
  114. data/data/matrix_data/PAM360 +34 -0
  115. data/data/matrix_data/PAM370 +34 -0
  116. data/data/matrix_data/PAM380 +34 -0
  117. data/data/matrix_data/PAM390 +34 -0
  118. data/data/matrix_data/PAM40 +34 -0
  119. data/data/matrix_data/PAM400 +34 -0
  120. data/data/matrix_data/PAM410 +34 -0
  121. data/data/matrix_data/PAM420 +34 -0
  122. data/data/matrix_data/PAM430 +34 -0
  123. data/data/matrix_data/PAM440 +34 -0
  124. data/data/matrix_data/PAM450 +34 -0
  125. data/data/matrix_data/PAM460 +34 -0
  126. data/data/matrix_data/PAM470 +34 -0
  127. data/data/matrix_data/PAM480 +34 -0
  128. data/data/matrix_data/PAM490 +34 -0
  129. data/data/matrix_data/PAM50 +34 -0
  130. data/data/matrix_data/PAM500 +34 -0
  131. data/data/matrix_data/PAM60 +34 -0
  132. data/data/matrix_data/PAM70 +25 -0
  133. data/data/matrix_data/PAM80 +34 -0
  134. data/data/matrix_data/PAM90 +34 -0
  135. data/ext/stride_align/backend_avx2.cpp +2 -0
  136. data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
  137. data/ext/stride_align/backend_generic.cpp +3 -0
  138. data/ext/stride_align/backend_impl.hpp +983 -0
  139. data/ext/stride_align/backend_lasx.cpp +2 -0
  140. data/ext/stride_align/backend_lsx.cpp +2 -0
  141. data/ext/stride_align/backend_neon.cpp +2 -0
  142. data/ext/stride_align/backend_rvv.cpp +2 -0
  143. data/ext/stride_align/backend_sse41.cpp +2 -0
  144. data/ext/stride_align/backend_sve.cpp +2 -0
  145. data/ext/stride_align/backend_sve2.cpp +2 -0
  146. data/ext/stride_align/backend_vsx.cpp +2 -0
  147. data/ext/stride_align/beider_morse_impl.cpp +5 -0
  148. data/ext/stride_align/cpu_detect.cpp +172 -0
  149. data/ext/stride_align/cpu_detect.hpp +6 -0
  150. data/ext/stride_align/extconf.rb +114 -0
  151. data/ext/stride_align/target_profile.hpp +82 -0
  152. data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
  153. data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
  154. data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
  155. data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
  156. data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
  157. data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
  158. data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
  159. data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
  160. data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
  161. data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
  162. data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
  163. data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
  164. data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
  165. data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
  166. data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
  167. data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
  168. data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
  169. data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
  170. data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
  171. data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
  172. data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
  173. data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
  174. data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
  175. data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
  176. data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
  177. data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
  178. data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
  179. data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
  180. data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
  181. data/lib/stride_align/algorithms.rb +296 -0
  182. data/lib/stride_align/alignment_path.rb +217 -0
  183. data/lib/stride_align/backend.rb +47 -0
  184. data/lib/stride_align/batch.rb +705 -0
  185. data/lib/stride_align/core.rb +180 -0
  186. data/lib/stride_align/keyboard.rb +200 -0
  187. data/lib/stride_align/matrices.rb +403 -0
  188. data/lib/stride_align/version.rb +5 -0
  189. data/lib/stride_align.rb +87 -0
  190. metadata +231 -0
@@ -0,0 +1,705 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrideAlign
4
+ module Scorer
5
+ LEVENSHTEIN = 0
6
+ LEVENSHTEIN_NORMALIZED = 1
7
+ DAMERAU_LEVENSHTEIN = 2
8
+ DAMERAU_LEVENSHTEIN_NORMALIZED = 3
9
+ HAMMING = 4
10
+ HAMMING_NORMALIZED = 5
11
+ JARO = 6
12
+ JARO_WINKLER = 7
13
+ INDEL = 8
14
+ INDEL_NORMALIZED = 9
15
+ TRUE_DAMERAU_LEVENSHTEIN = 10
16
+ TRUE_DAMERAU_LEVENSHTEIN_NORMALIZED = 11
17
+ SMITH_WATERMAN = 12
18
+ SMITH_WATERMAN_NORMALIZED = 13
19
+ NEEDLEMAN_WUNSCH = 14
20
+ NEEDLEMAN_WUNSCH_NORMALIZED = 15
21
+ end
22
+
23
+ ScorerEntry = Struct.new(
24
+ :names, :id, :batch_method, :higher, :normalized, keyword_init: true
25
+ )
26
+
27
+ class LevenshteinScorer
28
+ attr_reader :query
29
+
30
+ def initialize(query)
31
+ @query = StrideAlign.__send__(:scalar_query, query)
32
+ end
33
+
34
+ def distance(target, score_cutoff: nil)
35
+ StrideAlign.levenshtein_score(@query, target, score_cutoff: score_cutoff)
36
+ end
37
+
38
+ def normalized_distance(target, score_cutoff: nil)
39
+ StrideAlign.levenshtein_normalized_score(
40
+ @query, target, score_cutoff: score_cutoff
41
+ )
42
+ end
43
+
44
+ def distances(targets, score_cutoff: nil)
45
+ StrideAlign.levenshtein_scores(@query, targets, score_cutoff: score_cutoff)
46
+ end
47
+
48
+ def normalized_distances(targets, score_cutoff: nil)
49
+ StrideAlign.levenshtein_normalized_scores(
50
+ @query, targets, score_cutoff: score_cutoff
51
+ )
52
+ end
53
+ end
54
+
55
+ class << self
56
+ def levenshtein_score(query, target, score_cutoff: nil)
57
+ cutoff = score_cutoff.nil? ? nil : non_negative_integer(score_cutoff, "score_cutoff")
58
+ pairwise(query, target) do |left, right|
59
+ value = __native_backend.levenshtein(left, right)
60
+ value.nil? || cutoff.nil? ? value : [value, cutoff + 1].min
61
+ end
62
+ end
63
+
64
+ def levenshtein_normalized_score(query, target, score_cutoff: nil)
65
+ cutoff = score_cutoff.nil? ? nil : non_negative_integer(score_cutoff, "score_cutoff")
66
+ pairwise(query, target) do |left, right|
67
+ if left.nil? || right.nil?
68
+ nil
69
+ else
70
+ distance = __native_backend.levenshtein(left, right)
71
+ distance = [distance, cutoff + 1].min unless cutoff.nil?
72
+ longest = [left.each_char.count, right.each_char.count].max
73
+ longest.zero? ? 1.0 : [0.0, 1.0 - distance.to_f / longest].max
74
+ end
75
+ end
76
+ end
77
+
78
+ def damerau_levenshtein_score(query, target)
79
+ stride_osa(query, target)
80
+ end
81
+
82
+ def damerau_levenshtein_normalized_score(query, target)
83
+ stride_osa_similarity(query, target)
84
+ end
85
+
86
+ def true_damerau_levenshtein_score(query, target)
87
+ stride_true_damerau_levenshtein(query, target)
88
+ end
89
+
90
+ def true_damerau_levenshtein_normalized_score(query, target)
91
+ stride_true_damerau_levenshtein_similarity(query, target)
92
+ end
93
+
94
+ def indel_score(query, target, score_cutoff: nil)
95
+ cutoff = score_cutoff.nil? ? nil : non_negative_integer(score_cutoff, "score_cutoff")
96
+ pairwise(query, target) do |left, right|
97
+ value = __native_backend.indel(left, right)
98
+ value.nil? || cutoff.nil? ? value : [value, cutoff + 1].min
99
+ end
100
+ end
101
+
102
+ def indel_normalized_score(query, target, score_cutoff: nil)
103
+ unless score_cutoff.nil? ||
104
+ (score_cutoff.is_a?(Numeric) && score_cutoff.finite? &&
105
+ score_cutoff.between?(0, 1))
106
+ raise ArgumentError, "score_cutoff must be between 0 and 1"
107
+ end
108
+ pairwise(query, target) do |left, right|
109
+ value = __native_backend.indel_similarity(left, right)
110
+ if value.nil? || score_cutoff.nil?
111
+ value
112
+ else
113
+ value < score_cutoff ? 0.0 : value
114
+ end
115
+ end
116
+ end
117
+
118
+ def hamming_score(query, target)
119
+ stride_hamming(query, target)
120
+ end
121
+
122
+ def hamming_normalized_score(query, target)
123
+ stride_hamming_similarity(query, target)
124
+ end
125
+
126
+ def jaro_similarity(query, target)
127
+ stride_jaro(query, target)
128
+ end
129
+
130
+ def jaro_winkler_similarity(query, target, prefix_weight: 0.1,
131
+ prefix_threshold: 0.7, prefix_cap: 4)
132
+ stride_jaro_winkler(
133
+ query, target, prefix_weight: prefix_weight,
134
+ prefix_threshold: prefix_threshold, prefix_cap: prefix_cap
135
+ )
136
+ end
137
+
138
+ def smith_waterman_score(query, target, match_score: 2,
139
+ mismatch_score: -1, gap_score: -1,
140
+ gap_open_score: nil, gap_extend_score: nil,
141
+ width: nil, matrix: nil)
142
+ if matrix
143
+ matrix_kwargs_clean(match_score, mismatch_score, width)
144
+ return matrix_pairwise(
145
+ query, target, matrix, true, gap_score: gap_score,
146
+ gap_open_score: gap_open_score, gap_extend_score: gap_extend_score
147
+ )
148
+ end
149
+ validate_width(width)
150
+ alignment_score(
151
+ query, target, true, match_score, mismatch_score, gap_score,
152
+ gap_open_score, gap_extend_score
153
+ )
154
+ end
155
+
156
+ def needleman_wunsch_score(query, target, match_score: 2,
157
+ mismatch_score: -1, gap_score: -1,
158
+ gap_open_score: nil, gap_extend_score: nil,
159
+ width: nil, matrix: nil)
160
+ if matrix
161
+ matrix_kwargs_clean(match_score, mismatch_score, width)
162
+ return matrix_pairwise(
163
+ query, target, matrix, false, gap_score: gap_score,
164
+ gap_open_score: gap_open_score, gap_extend_score: gap_extend_score
165
+ )
166
+ end
167
+ validate_width(width)
168
+ alignment_score(
169
+ query, target, false, match_score, mismatch_score, gap_score,
170
+ gap_open_score, gap_extend_score
171
+ )
172
+ end
173
+
174
+ def smith_waterman_normalized_score(query, target, **kwargs)
175
+ match_score = kwargs.fetch(:match_score, 2)
176
+ raw = smith_waterman_score(query, target, **kwargs)
177
+ normalize_alignment_result(raw, query, target, true, match_score)
178
+ end
179
+
180
+ def needleman_wunsch_normalized_score(query, target, **kwargs)
181
+ match_score = kwargs.fetch(:match_score, 2)
182
+ raw = needleman_wunsch_score(query, target, **kwargs)
183
+ normalize_alignment_result(raw, query, target, false, match_score)
184
+ end
185
+
186
+ alias smith_waterman_farrar_score smith_waterman_score
187
+ alias smith_waterman_farrar_normalized_score smith_waterman_normalized_score
188
+
189
+ def levenshtein_scores(query, targets, score_cutoff: nil)
190
+ one_to_many(query, targets) do |left, right|
191
+ levenshtein_score(left, right, score_cutoff: score_cutoff)
192
+ end
193
+ end
194
+
195
+ def levenshtein_normalized_scores(query, targets, score_cutoff: nil)
196
+ one_to_many(query, targets) do |left, right|
197
+ levenshtein_normalized_score(left, right, score_cutoff: score_cutoff)
198
+ end
199
+ end
200
+
201
+ def damerau_levenshtein_scores(query, targets)
202
+ one_to_many(query, targets) { |left, right| damerau_levenshtein_score(left, right) }
203
+ end
204
+
205
+ def damerau_levenshtein_normalized_scores(query, targets)
206
+ one_to_many(query, targets) do |left, right|
207
+ damerau_levenshtein_normalized_score(left, right)
208
+ end
209
+ end
210
+
211
+ def true_damerau_levenshtein_scores(query, targets)
212
+ one_to_many(query, targets) do |left, right|
213
+ true_damerau_levenshtein_score(left, right)
214
+ end
215
+ end
216
+
217
+ def true_damerau_levenshtein_normalized_scores(query, targets)
218
+ one_to_many(query, targets) do |left, right|
219
+ true_damerau_levenshtein_normalized_score(left, right)
220
+ end
221
+ end
222
+
223
+ def indel_scores(query, targets, score_cutoff: nil)
224
+ one_to_many(query, targets) do |left, right|
225
+ indel_score(left, right, score_cutoff: score_cutoff)
226
+ end
227
+ end
228
+
229
+ def indel_normalized_scores(query, targets, score_cutoff: nil)
230
+ one_to_many(query, targets) do |left, right|
231
+ indel_normalized_score(left, right, score_cutoff: score_cutoff)
232
+ end
233
+ end
234
+
235
+ def hamming_scores(query, targets)
236
+ one_to_many(query, targets) { |left, right| hamming_score(left, right) }
237
+ end
238
+
239
+ def hamming_normalized_scores(query, targets)
240
+ one_to_many(query, targets) { |left, right| hamming_normalized_score(left, right) }
241
+ end
242
+
243
+ def jaro_similarities(query, targets)
244
+ one_to_many(query, targets) { |left, right| jaro_similarity(left, right) }
245
+ end
246
+
247
+ def jaro_winkler_similarities(query, targets, **kwargs)
248
+ one_to_many(query, targets) do |left, right|
249
+ jaro_winkler_similarity(left, right, **kwargs)
250
+ end
251
+ end
252
+
253
+ def smith_waterman_scores(query, targets, **kwargs)
254
+ one_to_many(query, targets) do |left, right|
255
+ smith_waterman_score(left, right, **kwargs)
256
+ end
257
+ end
258
+
259
+ def smith_waterman_normalized_scores(query, targets, **kwargs)
260
+ one_to_many(query, targets) do |left, right|
261
+ smith_waterman_normalized_score(left, right, **kwargs)
262
+ end
263
+ end
264
+
265
+ alias smith_waterman_farrar_scores smith_waterman_scores
266
+ alias smith_waterman_farrar_normalized_scores smith_waterman_normalized_scores
267
+
268
+ def needleman_wunsch_scores(query, targets, **kwargs)
269
+ one_to_many(query, targets) do |left, right|
270
+ needleman_wunsch_score(left, right, **kwargs)
271
+ end
272
+ end
273
+
274
+ def needleman_wunsch_normalized_scores(query, targets, **kwargs)
275
+ one_to_many(query, targets) do |left, right|
276
+ needleman_wunsch_normalized_score(left, right, **kwargs)
277
+ end
278
+ end
279
+
280
+ def levenshtein_top_k(query, targets, k: 5)
281
+ top_k(query, targets, Scorer::LEVENSHTEIN, k: k)
282
+ end
283
+
284
+ def levenshtein_normalized_top_k(query, targets, k: 5)
285
+ top_k(query, targets, Scorer::LEVENSHTEIN_NORMALIZED, k: k)
286
+ end
287
+
288
+ def damerau_levenshtein_top_k(query, targets, k: 5)
289
+ top_k(query, targets, Scorer::DAMERAU_LEVENSHTEIN, k: k)
290
+ end
291
+
292
+ def damerau_levenshtein_normalized_top_k(query, targets, k: 5)
293
+ top_k(query, targets, Scorer::DAMERAU_LEVENSHTEIN_NORMALIZED, k: k)
294
+ end
295
+
296
+ def true_damerau_levenshtein_top_k(query, targets, k: 5)
297
+ top_k(query, targets, Scorer::TRUE_DAMERAU_LEVENSHTEIN, k: k)
298
+ end
299
+
300
+ def true_damerau_levenshtein_normalized_top_k(query, targets, k: 5)
301
+ top_k(query, targets, Scorer::TRUE_DAMERAU_LEVENSHTEIN_NORMALIZED, k: k)
302
+ end
303
+
304
+ def indel_top_k(query, targets, k: 5)
305
+ top_k(query, targets, Scorer::INDEL, k: k)
306
+ end
307
+
308
+ def indel_normalized_top_k(query, targets, k: 5)
309
+ top_k(query, targets, Scorer::INDEL_NORMALIZED, k: k)
310
+ end
311
+
312
+ def hamming_top_k(query, targets, k: 5)
313
+ top_k(query, targets, Scorer::HAMMING, k: k)
314
+ end
315
+
316
+ def hamming_normalized_top_k(query, targets, k: 5)
317
+ top_k(query, targets, Scorer::HAMMING_NORMALIZED, k: k)
318
+ end
319
+
320
+ def jaro_top_k(query, targets, k: 5)
321
+ top_k(query, targets, Scorer::JARO, k: k)
322
+ end
323
+
324
+ def jaro_winkler_top_k(query, targets, k: 5, **kwargs)
325
+ top_k(query, targets, Scorer::JARO_WINKLER, k: k, **kwargs)
326
+ end
327
+
328
+ def smith_waterman_top_k(query, targets, k: 5, **kwargs)
329
+ top_k(query, targets, Scorer::SMITH_WATERMAN, k: k, **kwargs)
330
+ end
331
+
332
+ def levenshtein_best(query, targets)
333
+ best(query, targets, Scorer::LEVENSHTEIN)
334
+ end
335
+
336
+ def levenshtein_normalized_best(query, targets)
337
+ best(query, targets, Scorer::LEVENSHTEIN_NORMALIZED)
338
+ end
339
+
340
+ def damerau_levenshtein_best(query, targets)
341
+ best(query, targets, Scorer::DAMERAU_LEVENSHTEIN)
342
+ end
343
+
344
+ def damerau_levenshtein_normalized_best(query, targets)
345
+ best(query, targets, Scorer::DAMERAU_LEVENSHTEIN_NORMALIZED)
346
+ end
347
+
348
+ def true_damerau_levenshtein_best(query, targets)
349
+ best(query, targets, Scorer::TRUE_DAMERAU_LEVENSHTEIN)
350
+ end
351
+
352
+ def true_damerau_levenshtein_normalized_best(query, targets)
353
+ best(query, targets, Scorer::TRUE_DAMERAU_LEVENSHTEIN_NORMALIZED)
354
+ end
355
+
356
+ def indel_best(query, targets)
357
+ best(query, targets, Scorer::INDEL)
358
+ end
359
+
360
+ def indel_normalized_best(query, targets)
361
+ best(query, targets, Scorer::INDEL_NORMALIZED)
362
+ end
363
+
364
+ def hamming_best(query, targets)
365
+ best(query, targets, Scorer::HAMMING)
366
+ end
367
+
368
+ def hamming_normalized_best(query, targets)
369
+ best(query, targets, Scorer::HAMMING_NORMALIZED)
370
+ end
371
+
372
+ def jaro_best(query, targets)
373
+ best(query, targets, Scorer::JARO)
374
+ end
375
+
376
+ def jaro_winkler_best(query, targets, **kwargs)
377
+ best(query, targets, Scorer::JARO_WINKLER, **kwargs)
378
+ end
379
+
380
+ def smith_waterman_best(query, targets, **kwargs)
381
+ best(query, targets, Scorer::SMITH_WATERMAN, **kwargs)
382
+ end
383
+
384
+ def extract(query, targets, scorer, k: 5, **kwargs)
385
+ top_k(query, targets, scorer, k: k, **kwargs)
386
+ end
387
+
388
+ def extract_best(query, targets, scorer, **kwargs)
389
+ best(query, targets, scorer, **kwargs)
390
+ end
391
+
392
+ def cdist(queries, targets, scorer: nil, matrix: nil,
393
+ match_score: 2, mismatch_score: -1, gap_score: -1,
394
+ gap_open_score: nil, gap_extend_score: nil, width: nil,
395
+ tqdm: nil, cpu_count: 0, prefix_weight: 0.1,
396
+ prefix_threshold: 0.7, prefix_cap: 4)
397
+ _ = cpu_count
398
+ queries = target_array(queries)
399
+ targets = target_array(targets)
400
+ if scorer.nil?
401
+ raise ArgumentError, "cdist requires either scorer or matrix" if matrix.nil?
402
+ scorer = Scorer::SMITH_WATERMAN
403
+ end
404
+ entry = resolve_scorer(scorer)
405
+ if matrix && ![Scorer::SMITH_WATERMAN, Scorer::NEEDLEMAN_WUNSCH].include?(entry.id)
406
+ raise ArgumentError,
407
+ "matrix cdist requires a Smith-Waterman or Needleman-Wunsch scorer"
408
+ end
409
+
410
+ progress = nil
411
+ if tqdm
412
+ raise ArgumentError, "tqdm must respond to call" unless tqdm.respond_to?(:call)
413
+ progress = tqdm.call(queries.length)
414
+ unless progress.respond_to?(:update) && progress.respond_to?(:close)
415
+ raise ArgumentError, "tqdm progress object must provide update and close"
416
+ end
417
+ end
418
+
419
+ begin
420
+ queries.map do |query|
421
+ options = {}
422
+ if (12..15).cover?(entry.id)
423
+ options.merge!(
424
+ match_score: match_score, mismatch_score: mismatch_score,
425
+ gap_score: gap_score, gap_open_score: gap_open_score,
426
+ gap_extend_score: gap_extend_score, width: width
427
+ )
428
+ options[:matrix] = matrix if matrix
429
+ elsif entry.id == Scorer::JARO_WINKLER
430
+ options.merge!(
431
+ prefix_weight: prefix_weight,
432
+ prefix_threshold: prefix_threshold,
433
+ prefix_cap: prefix_cap
434
+ )
435
+ end
436
+ row = if options.empty?
437
+ public_send(entry.batch_method, query, targets)
438
+ else
439
+ public_send(entry.batch_method, query, targets, **options)
440
+ end
441
+ progress.update(1) if progress
442
+ row
443
+ end
444
+ ensure
445
+ progress.close if progress
446
+ end
447
+ end
448
+
449
+ def cdist_above_threshold(queries, targets, scorer: nil, threshold:,
450
+ matrix: nil, **kwargs)
451
+ if scorer.nil? && matrix.nil?
452
+ raise ArgumentError, "cdist_above_threshold requires either scorer or matrix"
453
+ end
454
+ resolved = scorer.nil? ? Scorer::SMITH_WATERMAN : scorer
455
+ entry = resolve_scorer(resolved)
456
+ if matrix.nil? && !entry.normalized
457
+ raise ArgumentError,
458
+ "cdist_above_threshold requires a normalized or similarity scorer"
459
+ end
460
+ finite_number(threshold, "threshold")
461
+ if matrix.nil? && !threshold.between?(0, 1)
462
+ raise ArgumentError, "threshold must be between 0 and 1"
463
+ end
464
+ clean_queries = target_array(queries)
465
+ clean_targets = target_array(targets)
466
+ scores = cdist(
467
+ clean_queries, clean_targets, scorer: resolved, matrix: matrix, **kwargs
468
+ )
469
+ output = []
470
+ scores.each_with_index do |row, query_index|
471
+ row.each_with_index do |score, target_index|
472
+ next if score.nil? || score < threshold
473
+ output << {
474
+ score: score, query: clean_queries[query_index],
475
+ target: clean_targets[target_index], query_index: query_index,
476
+ target_index: target_index
477
+ }
478
+ end
479
+ end
480
+ output
481
+ end
482
+
483
+ def cdist_top_k(queries, targets, scorer: nil, k:, matrix: nil,
484
+ reject_duplicates: false, **kwargs)
485
+ if scorer.nil? && matrix.nil?
486
+ raise ArgumentError, "cdist_top_k requires either scorer or matrix"
487
+ end
488
+ resolved = scorer.nil? ? Scorer::SMITH_WATERMAN : scorer
489
+ entry = resolve_scorer(resolved)
490
+ if matrix.nil? && !entry.normalized
491
+ raise ArgumentError, "cdist_top_k requires a normalized or similarity scorer"
492
+ end
493
+ if matrix && reject_duplicates
494
+ raise ArgumentError, "reject_duplicates is not supported with matrix"
495
+ end
496
+ k = non_negative_integer(k, "k")
497
+ clean_queries = target_array(queries)
498
+ clean_targets = target_array(targets)
499
+ scores = cdist(
500
+ clean_queries, clean_targets, scorer: resolved, matrix: matrix, **kwargs
501
+ )
502
+ values = []
503
+ scores.each_with_index do |row, query_index|
504
+ row.each_with_index do |score, target_index|
505
+ next if score.nil?
506
+ next if reject_duplicates && clean_queries[query_index] == clean_targets[target_index]
507
+ values << {
508
+ score: score, query: clean_queries[query_index],
509
+ target: clean_targets[target_index], query_index: query_index,
510
+ target_index: target_index
511
+ }
512
+ end
513
+ end
514
+ values.sort_by { |item| [-item[:score], item[:query_index], item[:target_index]] }
515
+ .first(k)
516
+ end
517
+
518
+ def cdist_top_k_per_query(queries, targets, scorer:, k: 5,
519
+ pruning: false, cpu_count: 0,
520
+ prefix_weight: 0.1,
521
+ prefix_threshold: 0.7, prefix_cap: 4)
522
+ _ = [pruning, cpu_count]
523
+ clean_queries = target_array(queries)
524
+ clean_targets = target_array(targets)
525
+ entry = resolve_scorer(scorer)
526
+ unless entry.normalized
527
+ raise ArgumentError,
528
+ "cdist_top_k_per_query requires a normalized or similarity scorer"
529
+ end
530
+ options = entry.id == Scorer::JARO_WINKLER ? {
531
+ prefix_weight: prefix_weight,
532
+ prefix_threshold: prefix_threshold,
533
+ prefix_cap: prefix_cap
534
+ } : {}
535
+ clean_queries.each_with_object({}) do |query, output|
536
+ if entry.id == Scorer::HAMMING_NORMALIZED
537
+ valid = clean_targets.each_index.select do |index|
538
+ !query.nil? && !clean_targets[index].nil? &&
539
+ clean_targets[index].each_char.count == query.each_char.count
540
+ end
541
+ subset = valid.map { |index| clean_targets[index] }
542
+ ranked = ranked(
543
+ subset, hamming_normalized_scores(query, subset), k, true
544
+ )
545
+ ranked.each { |item| item[:index] = valid[item[:index]] }
546
+ output[query] = ranked
547
+ else
548
+ output[query] = top_k(query, clean_targets, scorer, k: k, **options)
549
+ end
550
+ end
551
+ end
552
+
553
+ private
554
+
555
+ def matrix_kwargs_clean(match_score, mismatch_score, width)
556
+ return if match_score == 2 && mismatch_score == -1 && width.nil?
557
+
558
+ raise ArgumentError,
559
+ "match_score, mismatch_score, and width cannot be used with matrix"
560
+ end
561
+
562
+ def alignment_score(query, target, local, match_score, mismatch_score,
563
+ gap_score, gap_open_score, gap_extend_score)
564
+ open_score, extend_score = resolve_gaps(
565
+ gap_score, gap_open_score, gap_extend_score
566
+ )
567
+ if open_score == extend_score
568
+ return local ?
569
+ stride_smith_waterman(
570
+ query, target, match_score: match_score,
571
+ mismatch_score: mismatch_score, gap_score: open_score
572
+ ) :
573
+ stride_needleman_wunsch(
574
+ query, target, match_score: match_score,
575
+ mismatch_score: mismatch_score, gap_score: open_score
576
+ )
577
+ end
578
+ local ?
579
+ stride_smith_waterman_affine(
580
+ query, target, match_score: match_score,
581
+ mismatch_score: mismatch_score, gap_open_score: open_score,
582
+ gap_extend_score: extend_score
583
+ ) :
584
+ stride_needleman_wunsch_affine(
585
+ query, target, match_score: match_score,
586
+ mismatch_score: mismatch_score, gap_open_score: open_score,
587
+ gap_extend_score: extend_score
588
+ )
589
+ end
590
+
591
+ def normalize_alignment_result(raw, query, target, local, match_score)
592
+ unless match_score.is_a?(Numeric) && match_score.finite? && match_score.positive?
593
+ raise ArgumentError, "match_score must be positive for normalized scores"
594
+ end
595
+ unless raw.is_a?(Array)
596
+ return normalize_one_alignment(raw, query, target, local, match_score)
597
+ end
598
+
599
+ left_values = query.is_a?(Array) ? query : [query]
600
+ right_values = target.is_a?(Array) ? target : [target]
601
+ raw.each_with_index.map do |score, index|
602
+ left = left_values[left_values.length == 1 ? 0 : index]
603
+ right = right_values[right_values.length == 1 ? 0 : index]
604
+ normalize_one_alignment(score, left, right, local, match_score)
605
+ end
606
+ end
607
+
608
+ def normalize_one_alignment(raw, query, target, local, match_score)
609
+ return nil if raw.nil? || query.nil? || target.nil?
610
+ query_length = query.each_char.count
611
+ target_length = target.each_char.count
612
+ denominator = (local ? [query_length, target_length].min :
613
+ [query_length, target_length].max) * match_score
614
+ return 1.0 if query_length.zero? && target_length.zero?
615
+ return 0.0 if denominator <= 0
616
+ [[raw.to_f / denominator, 0.0].max, 1.0].min
617
+ end
618
+
619
+ def one_to_many(query, targets)
620
+ query = scalar_query(query)
621
+ targets = target_array(targets)
622
+ targets.map { |target| yield(query, target) }
623
+ end
624
+
625
+ def scorer_table
626
+ @scorer_table ||= [
627
+ ScorerEntry.new(names: %w[levenshtein levenshtein_score], id: 0,
628
+ batch_method: :levenshtein_scores, higher: false, normalized: false),
629
+ ScorerEntry.new(names: %w[levenshtein_normalized levenshtein_normalized_score], id: 1,
630
+ batch_method: :levenshtein_normalized_scores, higher: true, normalized: true),
631
+ ScorerEntry.new(names: %w[damerau_levenshtein osa damerau_levenshtein_score], id: 2,
632
+ batch_method: :damerau_levenshtein_scores, higher: false, normalized: false),
633
+ ScorerEntry.new(names: %w[damerau_levenshtein_normalized osa_normalized], id: 3,
634
+ batch_method: :damerau_levenshtein_normalized_scores, higher: true, normalized: true),
635
+ ScorerEntry.new(names: %w[hamming hamming_score], id: 4,
636
+ batch_method: :hamming_scores, higher: false, normalized: false),
637
+ ScorerEntry.new(names: %w[hamming_normalized hamming_normalized_score], id: 5,
638
+ batch_method: :hamming_normalized_scores, higher: true, normalized: true),
639
+ ScorerEntry.new(names: %w[jaro jaro_similarity], id: 6,
640
+ batch_method: :jaro_similarities, higher: true, normalized: true),
641
+ ScorerEntry.new(names: %w[jaro_winkler jaro_winkler_similarity], id: 7,
642
+ batch_method: :jaro_winkler_similarities, higher: true, normalized: true),
643
+ ScorerEntry.new(names: %w[indel indel_score], id: 8,
644
+ batch_method: :indel_scores, higher: false, normalized: false),
645
+ ScorerEntry.new(names: %w[indel_normalized indel_normalized_score], id: 9,
646
+ batch_method: :indel_normalized_scores, higher: true, normalized: true),
647
+ ScorerEntry.new(names: %w[true_damerau_levenshtein true_damerau_levenshtein_score], id: 10,
648
+ batch_method: :true_damerau_levenshtein_scores, higher: false, normalized: false),
649
+ ScorerEntry.new(names: %w[true_damerau_levenshtein_normalized], id: 11,
650
+ batch_method: :true_damerau_levenshtein_normalized_scores, higher: true, normalized: true),
651
+ ScorerEntry.new(names: %w[sw smith_waterman smith-waterman local smith_waterman_score], id: 12,
652
+ batch_method: :smith_waterman_scores, higher: true, normalized: false),
653
+ ScorerEntry.new(names: %w[smith_waterman_normalized], id: 13,
654
+ batch_method: :smith_waterman_normalized_scores, higher: true, normalized: true),
655
+ ScorerEntry.new(names: %w[nw needleman_wunsch needleman-wunsch global needleman_wunsch_score], id: 14,
656
+ batch_method: :needleman_wunsch_scores, higher: true, normalized: false),
657
+ ScorerEntry.new(names: %w[needleman_wunsch_normalized], id: 15,
658
+ batch_method: :needleman_wunsch_normalized_scores, higher: true, normalized: true)
659
+ ].freeze
660
+ end
661
+
662
+ def resolve_scorer(scorer)
663
+ if scorer.respond_to?(:call)
664
+ name = scorer.respond_to?(:name) ? scorer.name.to_s : ""
665
+ entry = scorer_table.find { |candidate| candidate.batch_method.to_s == name }
666
+ return entry if entry
667
+ raise ArgumentError, "unknown stride-align scorer function"
668
+ end
669
+ entry = if scorer.is_a?(Numeric)
670
+ scorer_table.find { |candidate| candidate.id == scorer.to_i }
671
+ else
672
+ name = scorer.to_s.downcase
673
+ scorer_table.find { |candidate| candidate.names.include?(name) }
674
+ end
675
+ entry || raise(ArgumentError, "unknown stride-align scorer")
676
+ end
677
+
678
+ def ranked(targets, scores, k, higher)
679
+ k = non_negative_integer(k, "k")
680
+ values = targets.each_index.each_with_object([]) do |index, output|
681
+ next if scores[index].nil?
682
+ output << { target: targets[index], score: scores[index], index: index }
683
+ end
684
+ values.sort_by do |item|
685
+ [higher ? -item[:score] : item[:score], item[:index]]
686
+ end.first(k)
687
+ end
688
+
689
+ def top_k(query, targets, scorer, k: 5, **kwargs)
690
+ query = scalar_query(query)
691
+ targets = target_array(targets)
692
+ entry = resolve_scorer(scorer)
693
+ scores = if kwargs.empty?
694
+ public_send(entry.batch_method, query, targets)
695
+ else
696
+ public_send(entry.batch_method, query, targets, **kwargs)
697
+ end
698
+ ranked(targets, scores, k, entry.higher)
699
+ end
700
+
701
+ def best(query, targets, scorer, **kwargs)
702
+ top_k(query, targets, scorer, k: 1, **kwargs).first
703
+ end
704
+ end
705
+ end