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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +255 -0
- data/README.md +107 -0
- data/data/bmpm_data/gen_approx_any.txt +131 -0
- data/data/bmpm_data/gen_approx_arabic.txt +26 -0
- data/data/bmpm_data/gen_approx_common.txt +233 -0
- data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_approx_czech.txt +18 -0
- data/data/bmpm_data/gen_approx_dutch.txt +18 -0
- data/data/bmpm_data/gen_approx_english.txt +47 -0
- data/data/bmpm_data/gen_approx_french.txt +25 -0
- data/data/bmpm_data/gen_approx_german.txt +73 -0
- data/data/bmpm_data/gen_approx_greek.txt +18 -0
- data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
- data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
- data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
- data/data/bmpm_data/gen_approx_italian.txt +18 -0
- data/data/bmpm_data/gen_approx_polish.txt +84 -0
- data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
- data/data/bmpm_data/gen_approx_romanian.txt +18 -0
- data/data/bmpm_data/gen_approx_russian.txt +48 -0
- data/data/bmpm_data/gen_approx_spanish.txt +21 -0
- data/data/bmpm_data/gen_approx_turkish.txt +18 -0
- data/data/bmpm_data/gen_exact_any.txt +40 -0
- data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
- data/data/bmpm_data/gen_exact_arabic.txt +18 -0
- data/data/bmpm_data/gen_exact_common.txt +32 -0
- data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_exact_czech.txt +18 -0
- data/data/bmpm_data/gen_exact_dutch.txt +18 -0
- data/data/bmpm_data/gen_exact_english.txt +18 -0
- data/data/bmpm_data/gen_exact_french.txt +18 -0
- data/data/bmpm_data/gen_exact_german.txt +18 -0
- data/data/bmpm_data/gen_exact_greek.txt +18 -0
- data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
- data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
- data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
- data/data/bmpm_data/gen_exact_italian.txt +18 -0
- data/data/bmpm_data/gen_exact_polish.txt +23 -0
- data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
- data/data/bmpm_data/gen_exact_romanian.txt +18 -0
- data/data/bmpm_data/gen_exact_russian.txt +19 -0
- data/data/bmpm_data/gen_exact_spanish.txt +19 -0
- data/data/bmpm_data/gen_exact_turkish.txt +18 -0
- data/data/bmpm_data/gen_hebrew_common.txt +113 -0
- data/data/bmpm_data/gen_lang.txt +295 -0
- data/data/bmpm_data/gen_languages.txt +36 -0
- data/data/bmpm_data/gen_rules_any.txt +367 -0
- data/data/bmpm_data/gen_rules_arabic.txt +76 -0
- data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
- data/data/bmpm_data/gen_rules_czech.txt +67 -0
- data/data/bmpm_data/gen_rules_dutch.txt +78 -0
- data/data/bmpm_data/gen_rules_english.txt +113 -0
- data/data/bmpm_data/gen_rules_french.txt +114 -0
- data/data/bmpm_data/gen_rules_german.txt +129 -0
- data/data/bmpm_data/gen_rules_greek.txt +97 -0
- data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
- data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
- data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
- data/data/bmpm_data/gen_rules_italian.txt +77 -0
- data/data/bmpm_data/gen_rules_polish.txt +185 -0
- data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
- data/data/bmpm_data/gen_rules_romanian.txt +64 -0
- data/data/bmpm_data/gen_rules_russian.txt +142 -0
- data/data/bmpm_data/gen_rules_spanish.txt +85 -0
- data/data/bmpm_data/gen_rules_turkish.txt +50 -0
- data/data/keyboard_data/qwerty.npy +0 -0
- data/data/matrix_data/BLOSUM100 +31 -0
- data/data/matrix_data/BLOSUM30 +31 -0
- data/data/matrix_data/BLOSUM35 +31 -0
- data/data/matrix_data/BLOSUM40 +31 -0
- data/data/matrix_data/BLOSUM45 +25 -0
- data/data/matrix_data/BLOSUM50 +25 -0
- data/data/matrix_data/BLOSUM55 +31 -0
- data/data/matrix_data/BLOSUM60 +31 -0
- data/data/matrix_data/BLOSUM62 +25 -0
- data/data/matrix_data/BLOSUM65 +31 -0
- data/data/matrix_data/BLOSUM70 +31 -0
- data/data/matrix_data/BLOSUM75 +31 -0
- data/data/matrix_data/BLOSUM80 +25 -0
- data/data/matrix_data/BLOSUM85 +31 -0
- data/data/matrix_data/BLOSUM90 +25 -0
- data/data/matrix_data/NUC.4.4 +25 -0
- data/data/matrix_data/PAM10 +34 -0
- data/data/matrix_data/PAM100 +34 -0
- data/data/matrix_data/PAM110 +34 -0
- data/data/matrix_data/PAM120 +34 -0
- data/data/matrix_data/PAM130 +34 -0
- data/data/matrix_data/PAM140 +34 -0
- data/data/matrix_data/PAM150 +34 -0
- data/data/matrix_data/PAM160 +34 -0
- data/data/matrix_data/PAM170 +34 -0
- data/data/matrix_data/PAM180 +34 -0
- data/data/matrix_data/PAM190 +34 -0
- data/data/matrix_data/PAM20 +34 -0
- data/data/matrix_data/PAM200 +34 -0
- data/data/matrix_data/PAM210 +34 -0
- data/data/matrix_data/PAM220 +34 -0
- data/data/matrix_data/PAM230 +34 -0
- data/data/matrix_data/PAM240 +34 -0
- data/data/matrix_data/PAM250 +25 -0
- data/data/matrix_data/PAM260 +34 -0
- data/data/matrix_data/PAM270 +34 -0
- data/data/matrix_data/PAM280 +34 -0
- data/data/matrix_data/PAM290 +34 -0
- data/data/matrix_data/PAM30 +25 -0
- data/data/matrix_data/PAM300 +34 -0
- data/data/matrix_data/PAM310 +34 -0
- data/data/matrix_data/PAM320 +34 -0
- data/data/matrix_data/PAM330 +34 -0
- data/data/matrix_data/PAM340 +34 -0
- data/data/matrix_data/PAM350 +34 -0
- data/data/matrix_data/PAM360 +34 -0
- data/data/matrix_data/PAM370 +34 -0
- data/data/matrix_data/PAM380 +34 -0
- data/data/matrix_data/PAM390 +34 -0
- data/data/matrix_data/PAM40 +34 -0
- data/data/matrix_data/PAM400 +34 -0
- data/data/matrix_data/PAM410 +34 -0
- data/data/matrix_data/PAM420 +34 -0
- data/data/matrix_data/PAM430 +34 -0
- data/data/matrix_data/PAM440 +34 -0
- data/data/matrix_data/PAM450 +34 -0
- data/data/matrix_data/PAM460 +34 -0
- data/data/matrix_data/PAM470 +34 -0
- data/data/matrix_data/PAM480 +34 -0
- data/data/matrix_data/PAM490 +34 -0
- data/data/matrix_data/PAM50 +34 -0
- data/data/matrix_data/PAM500 +34 -0
- data/data/matrix_data/PAM60 +34 -0
- data/data/matrix_data/PAM70 +25 -0
- data/data/matrix_data/PAM80 +34 -0
- data/data/matrix_data/PAM90 +34 -0
- data/ext/stride_align/backend_avx2.cpp +2 -0
- data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
- data/ext/stride_align/backend_generic.cpp +3 -0
- data/ext/stride_align/backend_impl.hpp +983 -0
- data/ext/stride_align/backend_lasx.cpp +2 -0
- data/ext/stride_align/backend_lsx.cpp +2 -0
- data/ext/stride_align/backend_neon.cpp +2 -0
- data/ext/stride_align/backend_rvv.cpp +2 -0
- data/ext/stride_align/backend_sse41.cpp +2 -0
- data/ext/stride_align/backend_sve.cpp +2 -0
- data/ext/stride_align/backend_sve2.cpp +2 -0
- data/ext/stride_align/backend_vsx.cpp +2 -0
- data/ext/stride_align/beider_morse_impl.cpp +5 -0
- data/ext/stride_align/cpu_detect.cpp +172 -0
- data/ext/stride_align/cpu_detect.hpp +6 -0
- data/ext/stride_align/extconf.rb +114 -0
- data/ext/stride_align/target_profile.hpp +82 -0
- data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
- data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
- data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
- data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
- data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
- data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
- data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
- data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
- data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
- data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
- data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
- data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
- data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
- data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
- data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
- data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
- data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
- data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
- data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
- data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
- data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
- data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
- data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
- data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
- data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
- data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
- data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
- data/lib/stride_align/algorithms.rb +296 -0
- data/lib/stride_align/alignment_path.rb +217 -0
- data/lib/stride_align/backend.rb +47 -0
- data/lib/stride_align/batch.rb +705 -0
- data/lib/stride_align/core.rb +180 -0
- data/lib/stride_align/keyboard.rb +200 -0
- data/lib/stride_align/matrices.rb +403 -0
- data/lib/stride_align/version.rb +5 -0
- data/lib/stride_align.rb +87 -0
- metadata +231 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cmath>
|
|
5
|
+
#include <cstddef>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <limits>
|
|
8
|
+
#include <optional>
|
|
9
|
+
#include <span>
|
|
10
|
+
#include <stdexcept>
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <string_view>
|
|
13
|
+
#include <utility>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
#include "stride_align/core.hpp"
|
|
17
|
+
|
|
18
|
+
namespace stride_align::batch {
|
|
19
|
+
|
|
20
|
+
// This is the host-neutral scorer catalog shared by language adapters. The
|
|
21
|
+
// numeric values intentionally match the Python Scorer enum.
|
|
22
|
+
enum class Scorer : std::uint8_t {
|
|
23
|
+
levenshtein = 0,
|
|
24
|
+
levenshtein_normalized = 1,
|
|
25
|
+
damerau_levenshtein = 2,
|
|
26
|
+
damerau_levenshtein_normalized = 3,
|
|
27
|
+
hamming = 4,
|
|
28
|
+
hamming_normalized = 5,
|
|
29
|
+
jaro = 6,
|
|
30
|
+
jaro_winkler = 7,
|
|
31
|
+
indel = 8,
|
|
32
|
+
indel_normalized = 9,
|
|
33
|
+
true_damerau_levenshtein = 10,
|
|
34
|
+
true_damerau_levenshtein_normalized = 11,
|
|
35
|
+
smith_waterman = 12,
|
|
36
|
+
smith_waterman_normalized = 13,
|
|
37
|
+
needleman_wunsch = 14,
|
|
38
|
+
needleman_wunsch_normalized = 15,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
struct ScoreOptions {
|
|
42
|
+
Score match_score = 2;
|
|
43
|
+
Score mismatch_score = -1;
|
|
44
|
+
Score gap_open_score = -1;
|
|
45
|
+
Score gap_extend_score = -1;
|
|
46
|
+
double prefix_weight = 0.1;
|
|
47
|
+
double prefix_threshold = 0.7;
|
|
48
|
+
std::size_t prefix_cap = 4;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
inline std::string canonical_scorer_name(std::string_view value) {
|
|
52
|
+
std::string result;
|
|
53
|
+
result.reserve(value.size());
|
|
54
|
+
for (const char character : value) {
|
|
55
|
+
if (character == '-') {
|
|
56
|
+
result.push_back('_');
|
|
57
|
+
} else if (character >= 'A' && character <= 'Z') {
|
|
58
|
+
result.push_back(static_cast<char>(character - 'A' + 'a'));
|
|
59
|
+
} else {
|
|
60
|
+
result.push_back(character);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (result.starts_with("stride_")) result.erase(0, 7);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
inline Scorer parse_scorer(std::string_view value) {
|
|
68
|
+
const std::string name = canonical_scorer_name(value);
|
|
69
|
+
if (name == "levenshtein" || name == "levenshtein_score" || name == "0") {
|
|
70
|
+
return Scorer::levenshtein;
|
|
71
|
+
}
|
|
72
|
+
if (name == "levenshtein_normalized" ||
|
|
73
|
+
name == "levenshtein_normalized_score" || name == "1") {
|
|
74
|
+
return Scorer::levenshtein_normalized;
|
|
75
|
+
}
|
|
76
|
+
if (name == "damerau_levenshtein" || name == "damerau_levenshtein_score" ||
|
|
77
|
+
name == "osa" || name == "2") {
|
|
78
|
+
return Scorer::damerau_levenshtein;
|
|
79
|
+
}
|
|
80
|
+
if (name == "damerau_levenshtein_normalized" ||
|
|
81
|
+
name == "damerau_levenshtein_normalized_score" ||
|
|
82
|
+
name == "osa_normalized" || name == "3") {
|
|
83
|
+
return Scorer::damerau_levenshtein_normalized;
|
|
84
|
+
}
|
|
85
|
+
if (name == "hamming" || name == "hamming_score" || name == "4") {
|
|
86
|
+
return Scorer::hamming;
|
|
87
|
+
}
|
|
88
|
+
if (name == "hamming_normalized" || name == "hamming_normalized_score" ||
|
|
89
|
+
name == "5") {
|
|
90
|
+
return Scorer::hamming_normalized;
|
|
91
|
+
}
|
|
92
|
+
if (name == "jaro" || name == "jaro_similarity" || name == "6") {
|
|
93
|
+
return Scorer::jaro;
|
|
94
|
+
}
|
|
95
|
+
if (name == "jaro_winkler" || name == "jaro_winkler_similarity" ||
|
|
96
|
+
name == "7") {
|
|
97
|
+
return Scorer::jaro_winkler;
|
|
98
|
+
}
|
|
99
|
+
if (name == "indel" || name == "indel_score" || name == "8") {
|
|
100
|
+
return Scorer::indel;
|
|
101
|
+
}
|
|
102
|
+
if (name == "indel_normalized" || name == "indel_normalized_score" ||
|
|
103
|
+
name == "9") {
|
|
104
|
+
return Scorer::indel_normalized;
|
|
105
|
+
}
|
|
106
|
+
if (name == "true_damerau_levenshtein" ||
|
|
107
|
+
name == "true_damerau_levenshtein_score" || name == "10") {
|
|
108
|
+
return Scorer::true_damerau_levenshtein;
|
|
109
|
+
}
|
|
110
|
+
if (name == "true_damerau_levenshtein_normalized" ||
|
|
111
|
+
name == "true_damerau_levenshtein_normalized_score" || name == "11") {
|
|
112
|
+
return Scorer::true_damerau_levenshtein_normalized;
|
|
113
|
+
}
|
|
114
|
+
if (name == "smith_waterman" || name == "smith_waterman_score" ||
|
|
115
|
+
name == "sw" || name == "local" || name == "12") {
|
|
116
|
+
return Scorer::smith_waterman;
|
|
117
|
+
}
|
|
118
|
+
if (name == "smith_waterman_normalized" ||
|
|
119
|
+
name == "smith_waterman_normalized_score" || name == "13") {
|
|
120
|
+
return Scorer::smith_waterman_normalized;
|
|
121
|
+
}
|
|
122
|
+
if (name == "needleman_wunsch" || name == "needleman_wunsch_score" ||
|
|
123
|
+
name == "nw" || name == "global" || name == "14") {
|
|
124
|
+
return Scorer::needleman_wunsch;
|
|
125
|
+
}
|
|
126
|
+
if (name == "needleman_wunsch_normalized" ||
|
|
127
|
+
name == "needleman_wunsch_normalized_score" || name == "15") {
|
|
128
|
+
return Scorer::needleman_wunsch_normalized;
|
|
129
|
+
}
|
|
130
|
+
throw std::invalid_argument("unknown stride-align scorer: " + std::string(value));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
inline bool is_normalized_or_similarity(Scorer scorer) noexcept {
|
|
134
|
+
switch (scorer) {
|
|
135
|
+
case Scorer::levenshtein_normalized:
|
|
136
|
+
case Scorer::damerau_levenshtein_normalized:
|
|
137
|
+
case Scorer::hamming_normalized:
|
|
138
|
+
case Scorer::jaro:
|
|
139
|
+
case Scorer::jaro_winkler:
|
|
140
|
+
case Scorer::indel_normalized:
|
|
141
|
+
case Scorer::true_damerau_levenshtein_normalized:
|
|
142
|
+
case Scorer::smith_waterman_normalized:
|
|
143
|
+
case Scorer::needleman_wunsch_normalized:
|
|
144
|
+
return true;
|
|
145
|
+
default:
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
inline bool higher_is_better(Scorer scorer) noexcept {
|
|
151
|
+
switch (scorer) {
|
|
152
|
+
case Scorer::levenshtein:
|
|
153
|
+
case Scorer::damerau_levenshtein:
|
|
154
|
+
case Scorer::hamming:
|
|
155
|
+
case Scorer::indel:
|
|
156
|
+
case Scorer::true_damerau_levenshtein:
|
|
157
|
+
return false;
|
|
158
|
+
default:
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
inline bool is_integral(Scorer scorer) noexcept {
|
|
164
|
+
switch (scorer) {
|
|
165
|
+
case Scorer::levenshtein:
|
|
166
|
+
case Scorer::damerau_levenshtein:
|
|
167
|
+
case Scorer::hamming:
|
|
168
|
+
case Scorer::indel:
|
|
169
|
+
case Scorer::true_damerau_levenshtein:
|
|
170
|
+
case Scorer::smith_waterman:
|
|
171
|
+
case Scorer::needleman_wunsch:
|
|
172
|
+
return true;
|
|
173
|
+
default:
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
inline bool is_hamming(Scorer scorer) noexcept {
|
|
179
|
+
return scorer == Scorer::hamming || scorer == Scorer::hamming_normalized;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
inline double normalized_alignment(
|
|
183
|
+
Score raw,
|
|
184
|
+
std::size_t query_size,
|
|
185
|
+
std::size_t target_size,
|
|
186
|
+
bool local,
|
|
187
|
+
Score match_score) {
|
|
188
|
+
if (match_score <= 0) {
|
|
189
|
+
throw std::invalid_argument(
|
|
190
|
+
"match_score must be positive for normalized alignment scores");
|
|
191
|
+
}
|
|
192
|
+
if (query_size == 0U && target_size == 0U) return 1.0;
|
|
193
|
+
const std::size_t length = local
|
|
194
|
+
? std::min(query_size, target_size)
|
|
195
|
+
: std::max(query_size, target_size);
|
|
196
|
+
if (length == 0U) return 0.0;
|
|
197
|
+
const double denominator =
|
|
198
|
+
static_cast<double>(length) * static_cast<double>(match_score);
|
|
199
|
+
return std::clamp(static_cast<double>(raw) / denominator, 0.0, 1.0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
inline std::size_t unicode_length(std::string_view text) {
|
|
203
|
+
if (utf8::is_ascii(text)) return text.size();
|
|
204
|
+
return utf8::prepare_streaming(text).size();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Convert a normalized-similarity floor into an inclusive integer distance
|
|
208
|
+
// cutoff. The epsilon protects exact integer boundaries such as
|
|
209
|
+
// (1 - 0.8) * 10 from binary floating-point rounding to 1.999999....
|
|
210
|
+
inline std::size_t normalized_distance_cutoff(
|
|
211
|
+
double similarity,
|
|
212
|
+
std::size_t maximum_distance) noexcept {
|
|
213
|
+
if (similarity <= 0.0) return maximum_distance;
|
|
214
|
+
if (similarity >= 1.0) return 0U;
|
|
215
|
+
const double allowed =
|
|
216
|
+
(1.0 - similarity) * static_cast<double>(maximum_distance);
|
|
217
|
+
return static_cast<std::size_t>(std::floor(allowed + 1e-9));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
struct Text {
|
|
221
|
+
std::string bytes;
|
|
222
|
+
std::size_t length = 0;
|
|
223
|
+
|
|
224
|
+
Text() = default;
|
|
225
|
+
explicit Text(std::string value)
|
|
226
|
+
: bytes(std::move(value)), length(unicode_length(bytes)) {}
|
|
227
|
+
Text(std::string value, std::size_t character_length)
|
|
228
|
+
: bytes(std::move(value)), length(character_length) {}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
inline double score_prepared(
|
|
232
|
+
Scorer scorer,
|
|
233
|
+
const utf8::PreparedPair& pair,
|
|
234
|
+
const ScoreOptions& options = {},
|
|
235
|
+
std::optional<double> similarity_cutoff = std::nullopt,
|
|
236
|
+
std::optional<std::size_t> distance_cutoff = std::nullopt) {
|
|
237
|
+
switch (scorer) {
|
|
238
|
+
case Scorer::levenshtein:
|
|
239
|
+
return static_cast<double>(core::levenshtein_distance(
|
|
240
|
+
pair, distance_cutoff.value_or(levenshtein::kNoCutoff)));
|
|
241
|
+
case Scorer::levenshtein_normalized: {
|
|
242
|
+
std::size_t cutoff = levenshtein::kNoCutoff;
|
|
243
|
+
if (similarity_cutoff.has_value()) {
|
|
244
|
+
cutoff = normalized_distance_cutoff(
|
|
245
|
+
*similarity_cutoff,
|
|
246
|
+
std::max(pair.query_size(), pair.target_size()));
|
|
247
|
+
}
|
|
248
|
+
return levenshtein::normalize(
|
|
249
|
+
core::levenshtein_distance(pair, cutoff),
|
|
250
|
+
pair.query_size(), pair.target_size());
|
|
251
|
+
}
|
|
252
|
+
case Scorer::damerau_levenshtein:
|
|
253
|
+
return static_cast<double>(core::osa_distance(pair));
|
|
254
|
+
case Scorer::damerau_levenshtein_normalized:
|
|
255
|
+
return core::osa_similarity(pair);
|
|
256
|
+
case Scorer::hamming:
|
|
257
|
+
return static_cast<double>(core::hamming_distance(pair));
|
|
258
|
+
case Scorer::hamming_normalized:
|
|
259
|
+
return core::hamming_similarity(pair);
|
|
260
|
+
case Scorer::jaro:
|
|
261
|
+
return core::jaro_similarity(pair);
|
|
262
|
+
case Scorer::jaro_winkler:
|
|
263
|
+
return core::jaro_winkler_similarity(
|
|
264
|
+
pair, options.prefix_weight, options.prefix_threshold,
|
|
265
|
+
options.prefix_cap);
|
|
266
|
+
case Scorer::indel:
|
|
267
|
+
return static_cast<double>(core::indel_distance(
|
|
268
|
+
pair, distance_cutoff.value_or(indel::kNoCutoff)));
|
|
269
|
+
case Scorer::indel_normalized: {
|
|
270
|
+
std::size_t cutoff = indel::kNoCutoff;
|
|
271
|
+
if (similarity_cutoff.has_value()) {
|
|
272
|
+
cutoff = normalized_distance_cutoff(
|
|
273
|
+
*similarity_cutoff, pair.query_size() + pair.target_size());
|
|
274
|
+
}
|
|
275
|
+
return indel::normalize(
|
|
276
|
+
core::indel_distance(pair, cutoff),
|
|
277
|
+
pair.query_size(), pair.target_size());
|
|
278
|
+
}
|
|
279
|
+
case Scorer::true_damerau_levenshtein:
|
|
280
|
+
return static_cast<double>(core::true_damerau_levenshtein_distance(pair));
|
|
281
|
+
case Scorer::true_damerau_levenshtein_normalized:
|
|
282
|
+
return core::true_damerau_levenshtein_similarity(pair);
|
|
283
|
+
case Scorer::smith_waterman: {
|
|
284
|
+
const Score raw = options.gap_open_score == options.gap_extend_score
|
|
285
|
+
? core::smith_waterman_score(
|
|
286
|
+
pair, options.match_score, options.mismatch_score,
|
|
287
|
+
options.gap_open_score)
|
|
288
|
+
: core::smith_waterman_affine_score(
|
|
289
|
+
pair, options.match_score, options.mismatch_score,
|
|
290
|
+
options.gap_open_score, options.gap_extend_score);
|
|
291
|
+
return static_cast<double>(raw);
|
|
292
|
+
}
|
|
293
|
+
case Scorer::smith_waterman_normalized: {
|
|
294
|
+
const Score raw = options.gap_open_score == options.gap_extend_score
|
|
295
|
+
? core::smith_waterman_score(
|
|
296
|
+
pair, options.match_score, options.mismatch_score,
|
|
297
|
+
options.gap_open_score)
|
|
298
|
+
: core::smith_waterman_affine_score(
|
|
299
|
+
pair, options.match_score, options.mismatch_score,
|
|
300
|
+
options.gap_open_score, options.gap_extend_score);
|
|
301
|
+
return normalized_alignment(
|
|
302
|
+
raw, pair.query_size(), pair.target_size(), true,
|
|
303
|
+
options.match_score);
|
|
304
|
+
}
|
|
305
|
+
case Scorer::needleman_wunsch: {
|
|
306
|
+
const Score raw = options.gap_open_score == options.gap_extend_score
|
|
307
|
+
? core::needleman_wunsch_score(
|
|
308
|
+
pair, options.match_score, options.mismatch_score,
|
|
309
|
+
options.gap_open_score)
|
|
310
|
+
: core::needleman_wunsch_affine_score(
|
|
311
|
+
pair, options.match_score, options.mismatch_score,
|
|
312
|
+
options.gap_open_score, options.gap_extend_score);
|
|
313
|
+
return static_cast<double>(raw);
|
|
314
|
+
}
|
|
315
|
+
case Scorer::needleman_wunsch_normalized: {
|
|
316
|
+
const Score raw = options.gap_open_score == options.gap_extend_score
|
|
317
|
+
? core::needleman_wunsch_score(
|
|
318
|
+
pair, options.match_score, options.mismatch_score,
|
|
319
|
+
options.gap_open_score)
|
|
320
|
+
: core::needleman_wunsch_affine_score(
|
|
321
|
+
pair, options.match_score, options.mismatch_score,
|
|
322
|
+
options.gap_open_score, options.gap_extend_score);
|
|
323
|
+
return normalized_alignment(
|
|
324
|
+
raw, pair.query_size(), pair.target_size(), false,
|
|
325
|
+
options.match_score);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw std::logic_error("unsupported stride-align scorer");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
template <typename PreparePair>
|
|
332
|
+
inline double score_with(
|
|
333
|
+
Scorer scorer,
|
|
334
|
+
std::string_view query,
|
|
335
|
+
std::string_view target,
|
|
336
|
+
PreparePair&& prepare_pair,
|
|
337
|
+
const ScoreOptions& options = {},
|
|
338
|
+
std::optional<double> similarity_cutoff = std::nullopt,
|
|
339
|
+
std::optional<std::size_t> distance_cutoff = std::nullopt) {
|
|
340
|
+
const auto pair = prepare_pair(query, target);
|
|
341
|
+
return score_prepared(
|
|
342
|
+
scorer, pair, options, similarity_cutoff, distance_cutoff);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
inline double score(
|
|
346
|
+
Scorer scorer,
|
|
347
|
+
std::string_view query,
|
|
348
|
+
std::string_view target,
|
|
349
|
+
const ScoreOptions& options = {},
|
|
350
|
+
std::optional<double> similarity_cutoff = std::nullopt,
|
|
351
|
+
std::optional<std::size_t> distance_cutoff = std::nullopt) {
|
|
352
|
+
return score_with(
|
|
353
|
+
scorer, query, target,
|
|
354
|
+
[](std::string_view left, std::string_view right) {
|
|
355
|
+
return utf8::prepare_pair(left, right);
|
|
356
|
+
},
|
|
357
|
+
options, similarity_cutoff, distance_cutoff);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
struct Utf8PreparePair {
|
|
361
|
+
utf8::PreparedPair operator()(
|
|
362
|
+
std::string_view query,
|
|
363
|
+
std::string_view target) const {
|
|
364
|
+
return utf8::prepare_pair(query, target);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Cheap upper bound used before invoking a normalized scorer. Length-only
|
|
369
|
+
// bounds let threshold and top-k operations avoid preparing or analysing a
|
|
370
|
+
// pair that cannot possibly enter the result set. These are valid upper
|
|
371
|
+
// bounds; some are deliberately loose when scorer parameters are unusual.
|
|
372
|
+
inline double maximum_similarity(
|
|
373
|
+
Scorer scorer,
|
|
374
|
+
std::size_t query_size,
|
|
375
|
+
std::size_t target_size,
|
|
376
|
+
const ScoreOptions& options = {}) noexcept {
|
|
377
|
+
const std::size_t longer = std::max(query_size, target_size);
|
|
378
|
+
const std::size_t shorter = std::min(query_size, target_size);
|
|
379
|
+
if (longer == 0U) return 1.0;
|
|
380
|
+
switch (scorer) {
|
|
381
|
+
case Scorer::levenshtein_normalized:
|
|
382
|
+
case Scorer::damerau_levenshtein_normalized:
|
|
383
|
+
case Scorer::true_damerau_levenshtein_normalized:
|
|
384
|
+
return static_cast<double>(shorter) / static_cast<double>(longer);
|
|
385
|
+
case Scorer::indel_normalized: {
|
|
386
|
+
const std::size_t total = query_size + target_size;
|
|
387
|
+
return total == 0U
|
|
388
|
+
? 1.0
|
|
389
|
+
: (2.0 * static_cast<double>(shorter)) /
|
|
390
|
+
static_cast<double>(total);
|
|
391
|
+
}
|
|
392
|
+
case Scorer::hamming_normalized:
|
|
393
|
+
return query_size == target_size ? 1.0 : 0.0;
|
|
394
|
+
case Scorer::jaro:
|
|
395
|
+
if (shorter == 0U) return 0.0;
|
|
396
|
+
return (2.0 + static_cast<double>(shorter) /
|
|
397
|
+
static_cast<double>(longer)) /
|
|
398
|
+
3.0;
|
|
399
|
+
case Scorer::jaro_winkler: {
|
|
400
|
+
const double jaro_bound = maximum_similarity(
|
|
401
|
+
Scorer::jaro, query_size, target_size, options);
|
|
402
|
+
if (jaro_bound < options.prefix_threshold) return jaro_bound;
|
|
403
|
+
const double boost = static_cast<double>(
|
|
404
|
+
std::min(options.prefix_cap, shorter)) * options.prefix_weight;
|
|
405
|
+
if (boost <= 1.0) {
|
|
406
|
+
return jaro_bound + boost * (1.0 - jaro_bound);
|
|
407
|
+
}
|
|
408
|
+
// Unusually large weights can make the Winkler transform non-monotone;
|
|
409
|
+
// keep the bound conservative instead of risking an invalid prune.
|
|
410
|
+
return std::numeric_limits<double>::infinity();
|
|
411
|
+
}
|
|
412
|
+
default:
|
|
413
|
+
return 1.0;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
inline std::size_t minimum_distance(
|
|
418
|
+
Scorer scorer,
|
|
419
|
+
std::size_t query_size,
|
|
420
|
+
std::size_t target_size) noexcept {
|
|
421
|
+
const std::size_t difference = query_size > target_size
|
|
422
|
+
? query_size - target_size
|
|
423
|
+
: target_size - query_size;
|
|
424
|
+
switch (scorer) {
|
|
425
|
+
case Scorer::levenshtein:
|
|
426
|
+
case Scorer::damerau_levenshtein:
|
|
427
|
+
case Scorer::indel:
|
|
428
|
+
case Scorer::true_damerau_levenshtein:
|
|
429
|
+
return difference;
|
|
430
|
+
case Scorer::hamming:
|
|
431
|
+
return query_size == target_size
|
|
432
|
+
? 0U : std::numeric_limits<std::size_t>::max();
|
|
433
|
+
default:
|
|
434
|
+
return 0U;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
struct RankedMatch {
|
|
439
|
+
double score = 0.0;
|
|
440
|
+
std::size_t index = 0;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
inline bool better_than(
|
|
444
|
+
const RankedMatch& left,
|
|
445
|
+
const RankedMatch& right,
|
|
446
|
+
bool higher) noexcept {
|
|
447
|
+
if (left.score != right.score) {
|
|
448
|
+
return higher ? left.score > right.score : left.score < right.score;
|
|
449
|
+
}
|
|
450
|
+
return left.index < right.index;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
inline void insert_ranked(
|
|
454
|
+
std::vector<RankedMatch>& output,
|
|
455
|
+
RankedMatch candidate,
|
|
456
|
+
std::size_t k,
|
|
457
|
+
bool higher) {
|
|
458
|
+
if (k == 0U) return;
|
|
459
|
+
const auto better = [higher](
|
|
460
|
+
const RankedMatch& left,
|
|
461
|
+
const RankedMatch& right) {
|
|
462
|
+
return better_than(left, right, higher);
|
|
463
|
+
};
|
|
464
|
+
if (output.size() < k) {
|
|
465
|
+
output.push_back(candidate);
|
|
466
|
+
std::push_heap(output.begin(), output.end(), better);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// `better` makes the heap root the worst retained match.
|
|
470
|
+
if (!better(candidate, output.front())) return;
|
|
471
|
+
std::pop_heap(output.begin(), output.end(), better);
|
|
472
|
+
output.back() = candidate;
|
|
473
|
+
std::push_heap(output.begin(), output.end(), better);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
template <typename PreparePair>
|
|
477
|
+
inline std::vector<RankedMatch> top_k_with(
|
|
478
|
+
const Text& query,
|
|
479
|
+
std::span<const std::optional<Text>> targets,
|
|
480
|
+
Scorer scorer,
|
|
481
|
+
std::size_t k,
|
|
482
|
+
PreparePair&& prepare_pair,
|
|
483
|
+
const ScoreOptions& options = {},
|
|
484
|
+
bool skip_invalid_hamming = false) {
|
|
485
|
+
std::vector<RankedMatch> output;
|
|
486
|
+
if (k == 0U) return output;
|
|
487
|
+
output.reserve(std::min(k, targets.size()));
|
|
488
|
+
const bool higher = higher_is_better(scorer);
|
|
489
|
+
for (std::size_t index = 0; index < targets.size(); ++index) {
|
|
490
|
+
if (!targets[index].has_value()) continue;
|
|
491
|
+
const Text& target = *targets[index];
|
|
492
|
+
if (is_hamming(scorer) && query.length != target.length) {
|
|
493
|
+
if (skip_invalid_hamming) continue;
|
|
494
|
+
throw std::invalid_argument("Hamming distance requires equal-length strings");
|
|
495
|
+
}
|
|
496
|
+
std::optional<double> cutoff;
|
|
497
|
+
std::optional<std::size_t> distance_cutoff;
|
|
498
|
+
if (higher && output.size() == k && k != 0U) {
|
|
499
|
+
cutoff = output.front().score;
|
|
500
|
+
if (is_normalized_or_similarity(scorer) &&
|
|
501
|
+
maximum_similarity(
|
|
502
|
+
scorer, query.length, target.length, options) < *cutoff) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
} else if (!higher && output.size() == k && k != 0U) {
|
|
506
|
+
const double worst = output.front().score;
|
|
507
|
+
if (static_cast<double>(minimum_distance(
|
|
508
|
+
scorer, query.length, target.length)) > worst) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if ((scorer == Scorer::levenshtein || scorer == Scorer::indel) &&
|
|
512
|
+
worst >= 0.0 &&
|
|
513
|
+
worst < static_cast<double>(
|
|
514
|
+
std::numeric_limits<std::size_t>::max())) {
|
|
515
|
+
distance_cutoff = static_cast<std::size_t>(worst);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
insert_ranked(
|
|
519
|
+
output,
|
|
520
|
+
{score_with(
|
|
521
|
+
scorer, query.bytes, target.bytes, prepare_pair, options,
|
|
522
|
+
cutoff, distance_cutoff),
|
|
523
|
+
index},
|
|
524
|
+
k, higher);
|
|
525
|
+
}
|
|
526
|
+
std::sort(
|
|
527
|
+
output.begin(), output.end(),
|
|
528
|
+
[higher](const RankedMatch& left, const RankedMatch& right) {
|
|
529
|
+
return better_than(left, right, higher);
|
|
530
|
+
});
|
|
531
|
+
return output;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
inline std::vector<RankedMatch> top_k(
|
|
535
|
+
const Text& query,
|
|
536
|
+
std::span<const std::optional<Text>> targets,
|
|
537
|
+
Scorer scorer,
|
|
538
|
+
std::size_t k,
|
|
539
|
+
const ScoreOptions& options = {},
|
|
540
|
+
bool skip_invalid_hamming = false) {
|
|
541
|
+
return top_k_with(
|
|
542
|
+
query, targets, scorer, k, Utf8PreparePair{}, options,
|
|
543
|
+
skip_invalid_hamming);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
template <typename PreparePair>
|
|
547
|
+
inline std::vector<std::optional<double>> scores_with(
|
|
548
|
+
const Text& query,
|
|
549
|
+
std::span<const std::optional<Text>> targets,
|
|
550
|
+
Scorer scorer,
|
|
551
|
+
PreparePair&& prepare_pair,
|
|
552
|
+
const ScoreOptions& options = {}) {
|
|
553
|
+
std::vector<std::optional<double>> output;
|
|
554
|
+
output.reserve(targets.size());
|
|
555
|
+
for (const auto& target : targets) {
|
|
556
|
+
if (!target.has_value()) {
|
|
557
|
+
output.emplace_back(std::nullopt);
|
|
558
|
+
} else {
|
|
559
|
+
output.emplace_back(score_with(
|
|
560
|
+
scorer, query.bytes, target->bytes, prepare_pair, options));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return output;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
inline std::vector<std::optional<double>> scores(
|
|
567
|
+
const Text& query,
|
|
568
|
+
std::span<const std::optional<Text>> targets,
|
|
569
|
+
Scorer scorer,
|
|
570
|
+
const ScoreOptions& options = {}) {
|
|
571
|
+
return scores_with(query, targets, scorer, Utf8PreparePair{}, options);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
inline bool same_texts(
|
|
575
|
+
std::span<const std::optional<Text>> left,
|
|
576
|
+
std::span<const std::optional<Text>> right) noexcept {
|
|
577
|
+
if (left.size() != right.size()) return false;
|
|
578
|
+
for (std::size_t index = 0; index < left.size(); ++index) {
|
|
579
|
+
if (left[index].has_value() != right[index].has_value()) return false;
|
|
580
|
+
if (left[index].has_value() &&
|
|
581
|
+
left[index]->bytes != right[index]->bytes) return false;
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
using DistanceMatrix = std::vector<std::vector<std::optional<double>>>;
|
|
587
|
+
|
|
588
|
+
template <typename PreparePair>
|
|
589
|
+
inline DistanceMatrix cdist_with(
|
|
590
|
+
std::span<const std::optional<Text>> queries,
|
|
591
|
+
std::span<const std::optional<Text>> targets,
|
|
592
|
+
Scorer scorer,
|
|
593
|
+
PreparePair&& prepare_pair,
|
|
594
|
+
const ScoreOptions& options = {}) {
|
|
595
|
+
DistanceMatrix output(
|
|
596
|
+
queries.size(),
|
|
597
|
+
std::vector<std::optional<double>>(targets.size(), std::nullopt));
|
|
598
|
+
const bool symmetric = same_texts(queries, targets);
|
|
599
|
+
for (std::size_t query_index = 0; query_index < queries.size(); ++query_index) {
|
|
600
|
+
if (!queries[query_index].has_value()) continue;
|
|
601
|
+
const std::size_t target_begin = symmetric ? query_index : 0U;
|
|
602
|
+
for (std::size_t target_index = target_begin;
|
|
603
|
+
target_index < targets.size(); ++target_index) {
|
|
604
|
+
if (!targets[target_index].has_value()) continue;
|
|
605
|
+
const double value = score_with(
|
|
606
|
+
scorer, queries[query_index]->bytes,
|
|
607
|
+
targets[target_index]->bytes, prepare_pair, options);
|
|
608
|
+
output[query_index][target_index] = value;
|
|
609
|
+
if (symmetric && query_index != target_index) {
|
|
610
|
+
output[target_index][query_index] = value;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return output;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
inline DistanceMatrix cdist(
|
|
618
|
+
std::span<const std::optional<Text>> queries,
|
|
619
|
+
std::span<const std::optional<Text>> targets,
|
|
620
|
+
Scorer scorer,
|
|
621
|
+
const ScoreOptions& options = {}) {
|
|
622
|
+
return cdist_with(queries, targets, scorer, Utf8PreparePair{}, options);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
struct MatrixMatch {
|
|
626
|
+
double score = 0.0;
|
|
627
|
+
std::size_t query_index = 0;
|
|
628
|
+
std::size_t target_index = 0;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
inline bool better_than(
|
|
632
|
+
const MatrixMatch& left,
|
|
633
|
+
const MatrixMatch& right) noexcept {
|
|
634
|
+
if (left.score != right.score) return left.score > right.score;
|
|
635
|
+
if (left.query_index != right.query_index) {
|
|
636
|
+
return left.query_index < right.query_index;
|
|
637
|
+
}
|
|
638
|
+
return left.target_index < right.target_index;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
inline void insert_ranked(
|
|
642
|
+
std::vector<MatrixMatch>& output,
|
|
643
|
+
MatrixMatch candidate,
|
|
644
|
+
std::size_t k) {
|
|
645
|
+
if (k == 0U) return;
|
|
646
|
+
const auto better = [](const MatrixMatch& left, const MatrixMatch& right) {
|
|
647
|
+
return better_than(left, right);
|
|
648
|
+
};
|
|
649
|
+
if (output.size() < k) {
|
|
650
|
+
output.push_back(candidate);
|
|
651
|
+
std::push_heap(output.begin(), output.end(), better);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!better_than(candidate, output.front())) return;
|
|
655
|
+
std::pop_heap(output.begin(), output.end(), better);
|
|
656
|
+
output.back() = candidate;
|
|
657
|
+
std::push_heap(output.begin(), output.end(), better);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
template <typename PreparePair>
|
|
661
|
+
inline std::vector<MatrixMatch> cdist_above_threshold_with(
|
|
662
|
+
std::span<const std::optional<Text>> queries,
|
|
663
|
+
std::span<const std::optional<Text>> targets,
|
|
664
|
+
Scorer scorer,
|
|
665
|
+
double threshold,
|
|
666
|
+
PreparePair&& prepare_pair,
|
|
667
|
+
const ScoreOptions& options = {}) {
|
|
668
|
+
if (!is_normalized_or_similarity(scorer)) {
|
|
669
|
+
throw std::invalid_argument(
|
|
670
|
+
"cdist_above_threshold requires a normalized or similarity scorer");
|
|
671
|
+
}
|
|
672
|
+
if (!std::isfinite(threshold) || threshold < 0.0 || threshold > 1.0) {
|
|
673
|
+
throw std::invalid_argument("threshold must be between 0 and 1");
|
|
674
|
+
}
|
|
675
|
+
std::vector<MatrixMatch> output;
|
|
676
|
+
for (std::size_t query_index = 0; query_index < queries.size(); ++query_index) {
|
|
677
|
+
if (!queries[query_index].has_value()) continue;
|
|
678
|
+
for (std::size_t target_index = 0; target_index < targets.size(); ++target_index) {
|
|
679
|
+
if (!targets[target_index].has_value()) continue;
|
|
680
|
+
const Text& query = *queries[query_index];
|
|
681
|
+
const Text& target = *targets[target_index];
|
|
682
|
+
if (is_hamming(scorer) && query.length != target.length) {
|
|
683
|
+
throw std::invalid_argument("Hamming distance requires equal-length strings");
|
|
684
|
+
}
|
|
685
|
+
if (maximum_similarity(
|
|
686
|
+
scorer, query.length, target.length, options) < threshold) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const double value = score_with(
|
|
690
|
+
scorer, query.bytes, target.bytes, prepare_pair, options, threshold);
|
|
691
|
+
if (value >= threshold) {
|
|
692
|
+
output.push_back({value, query_index, target_index});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return output;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
inline std::vector<MatrixMatch> cdist_above_threshold(
|
|
700
|
+
std::span<const std::optional<Text>> queries,
|
|
701
|
+
std::span<const std::optional<Text>> targets,
|
|
702
|
+
Scorer scorer,
|
|
703
|
+
double threshold,
|
|
704
|
+
const ScoreOptions& options = {}) {
|
|
705
|
+
return cdist_above_threshold_with(
|
|
706
|
+
queries, targets, scorer, threshold, Utf8PreparePair{}, options);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
template <typename PreparePair>
|
|
710
|
+
inline std::vector<MatrixMatch> cdist_top_k_with(
|
|
711
|
+
std::span<const std::optional<Text>> queries,
|
|
712
|
+
std::span<const std::optional<Text>> targets,
|
|
713
|
+
Scorer scorer,
|
|
714
|
+
std::size_t k,
|
|
715
|
+
bool reject_duplicates,
|
|
716
|
+
PreparePair&& prepare_pair,
|
|
717
|
+
const ScoreOptions& options = {}) {
|
|
718
|
+
if (!is_normalized_or_similarity(scorer)) {
|
|
719
|
+
throw std::invalid_argument(
|
|
720
|
+
"cdist_top_k requires a normalized or similarity scorer");
|
|
721
|
+
}
|
|
722
|
+
std::vector<MatrixMatch> output;
|
|
723
|
+
if (k == 0U) return output;
|
|
724
|
+
const std::size_t maximum_pairs = targets.empty() ||
|
|
725
|
+
queries.size() <= std::numeric_limits<std::size_t>::max() / targets.size()
|
|
726
|
+
? queries.size() * targets.size()
|
|
727
|
+
: std::numeric_limits<std::size_t>::max();
|
|
728
|
+
output.reserve(std::min(k, maximum_pairs));
|
|
729
|
+
for (std::size_t query_index = 0; query_index < queries.size(); ++query_index) {
|
|
730
|
+
if (!queries[query_index].has_value()) continue;
|
|
731
|
+
for (std::size_t target_index = 0; target_index < targets.size(); ++target_index) {
|
|
732
|
+
if (!targets[target_index].has_value()) continue;
|
|
733
|
+
const Text& query = *queries[query_index];
|
|
734
|
+
const Text& target = *targets[target_index];
|
|
735
|
+
if (reject_duplicates && query.bytes == target.bytes) continue;
|
|
736
|
+
if (is_hamming(scorer) && query.length != target.length) {
|
|
737
|
+
throw std::invalid_argument("Hamming distance requires equal-length strings");
|
|
738
|
+
}
|
|
739
|
+
std::optional<double> cutoff;
|
|
740
|
+
if (output.size() == k && k != 0U) {
|
|
741
|
+
cutoff = output.front().score;
|
|
742
|
+
if (maximum_similarity(
|
|
743
|
+
scorer, query.length, target.length, options) < *cutoff) {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
insert_ranked(
|
|
748
|
+
output,
|
|
749
|
+
{score_with(
|
|
750
|
+
scorer, query.bytes, target.bytes, prepare_pair, options,
|
|
751
|
+
cutoff),
|
|
752
|
+
query_index, target_index},
|
|
753
|
+
k);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
std::sort(
|
|
757
|
+
output.begin(), output.end(),
|
|
758
|
+
[](const MatrixMatch& left, const MatrixMatch& right) {
|
|
759
|
+
return better_than(left, right);
|
|
760
|
+
});
|
|
761
|
+
return output;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
inline std::vector<MatrixMatch> cdist_top_k(
|
|
765
|
+
std::span<const std::optional<Text>> queries,
|
|
766
|
+
std::span<const std::optional<Text>> targets,
|
|
767
|
+
Scorer scorer,
|
|
768
|
+
std::size_t k,
|
|
769
|
+
bool reject_duplicates,
|
|
770
|
+
const ScoreOptions& options = {}) {
|
|
771
|
+
return cdist_top_k_with(
|
|
772
|
+
queries, targets, scorer, k, reject_duplicates,
|
|
773
|
+
Utf8PreparePair{}, options);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
template <typename PreparePair>
|
|
777
|
+
inline std::vector<std::vector<RankedMatch>> cdist_top_k_per_query_with(
|
|
778
|
+
std::span<const std::optional<Text>> queries,
|
|
779
|
+
std::span<const std::optional<Text>> targets,
|
|
780
|
+
Scorer scorer,
|
|
781
|
+
std::size_t k,
|
|
782
|
+
PreparePair&& prepare_pair,
|
|
783
|
+
const ScoreOptions& options = {}) {
|
|
784
|
+
if (!is_normalized_or_similarity(scorer)) {
|
|
785
|
+
throw std::invalid_argument(
|
|
786
|
+
"cdist_top_k_per_query requires a normalized or similarity scorer");
|
|
787
|
+
}
|
|
788
|
+
std::vector<std::vector<RankedMatch>> output;
|
|
789
|
+
output.reserve(queries.size());
|
|
790
|
+
for (const auto& query : queries) {
|
|
791
|
+
if (!query.has_value()) {
|
|
792
|
+
output.emplace_back();
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
output.push_back(top_k_with(
|
|
796
|
+
*query, targets, scorer, k, prepare_pair, options,
|
|
797
|
+
/*skip_invalid_hamming=*/true));
|
|
798
|
+
}
|
|
799
|
+
return output;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
inline std::vector<std::vector<RankedMatch>> cdist_top_k_per_query(
|
|
803
|
+
std::span<const std::optional<Text>> queries,
|
|
804
|
+
std::span<const std::optional<Text>> targets,
|
|
805
|
+
Scorer scorer,
|
|
806
|
+
std::size_t k,
|
|
807
|
+
const ScoreOptions& options = {}) {
|
|
808
|
+
return cdist_top_k_per_query_with(
|
|
809
|
+
queries, targets, scorer, k, Utf8PreparePair{}, options);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
} // namespace stride_align::batch
|