RapidFuzz
Loading...
Searching...
No Matches
LCSseq_impl.hpp
1/* SPDX-License-Identifier: MIT */
2/* Copyright © 2022-present Max Bachmann */
3
4#include <limits>
5#include <rapidfuzz/details/Matrix.hpp>
6#include <rapidfuzz/details/PatternMatchVector.hpp>
7#include <rapidfuzz/details/common.hpp>
8#include <rapidfuzz/details/distance.hpp>
9#include <rapidfuzz/details/intrinsics.hpp>
10#include <rapidfuzz/details/simd.hpp>
11
12#include <algorithm>
13#include <array>
14#include <rapidfuzz/details/types.hpp>
15
16namespace rapidfuzz {
17namespace detail {
18
19template <bool RecordMatrix>
20struct LCSseqResult;
21
22template <>
23struct LCSseqResult<true> {
24 ShiftedBitMatrix<uint64_t> S;
25
26 size_t sim;
27};
28
29template <>
30struct LCSseqResult<false> {
31 size_t sim;
32};
33
34template <bool RecordMatrix>
35LCSseqResult<true>& getMatrixRef(LCSseqResult<RecordMatrix>& res)
36{
37#if RAPIDFUZZ_IF_CONSTEXPR_AVAILABLE
38 return res;
39#else
40 // this is a hack since the compiler doesn't know early enough that
41 // this is never called when the types differ.
42 // On C++17 this properly uses if constexpr
43 assert(RecordMatrix);
44 return reinterpret_cast<LCSseqResult<true>&>(res);
45#endif
46}
47
48/*
49 * An encoded mbleven model table.
50 *
51 * Each 8-bit integer represents an edit sequence, with using two
52 * bits for a single operation.
53 *
54 * Each Row of 8 integers represent all possible combinations
55 * of edit sequences for a gived maximum edit distance and length
56 * difference between the two strings, that is below the maximum
57 * edit distance
58 *
59 * 0x1 = 01 = DELETE,
60 * 0x2 = 10 = INSERT
61 *
62 * 0x5 -> DEL + DEL
63 * 0x6 -> DEL + INS
64 * 0x9 -> INS + DEL
65 * 0xA -> INS + INS
66 */
67static constexpr std::array<std::array<uint8_t, 6>, 14> lcs_seq_mbleven2018_matrix = {{
68 /* max edit distance 1 */
69 {0},
70 /* case does not occur */ /* len_diff 0 */
71 {0x01}, /* len_diff 1 */
72 /* max edit distance 2 */
73 {0x09, 0x06}, /* len_diff 0 */
74 {0x01}, /* len_diff 1 */
75 {0x05}, /* len_diff 2 */
76 /* max edit distance 3 */
77 {0x09, 0x06}, /* len_diff 0 */
78 {0x25, 0x19, 0x16}, /* len_diff 1 */
79 {0x05}, /* len_diff 2 */
80 {0x15}, /* len_diff 3 */
81 /* max edit distance 4 */
82 {0x96, 0x66, 0x5A, 0x99, 0x69, 0xA5}, /* len_diff 0 */
83 {0x25, 0x19, 0x16}, /* len_diff 1 */
84 {0x65, 0x56, 0x95, 0x59}, /* len_diff 2 */
85 {0x15}, /* len_diff 3 */
86 {0x55}, /* len_diff 4 */
87}};
88
89template <typename InputIt1, typename InputIt2>
90size_t lcs_seq_mbleven2018(const Range<InputIt1>& s1, const Range<InputIt2>& s2, size_t score_cutoff)
91{
92 auto len1 = s1.size();
93 auto len2 = s2.size();
94 assert(len1 != 0);
95 assert(len2 != 0);
96
97 if (len1 < len2) return lcs_seq_mbleven2018(s2, s1, score_cutoff);
98
99 auto len_diff = len1 - len2;
100 size_t max_misses = len1 + len2 - 2 * score_cutoff;
101 size_t ops_index = (max_misses + max_misses * max_misses) / 2 + len_diff - 1;
102 auto& possible_ops = lcs_seq_mbleven2018_matrix[ops_index];
103 size_t max_len = 0;
104
105 for (uint8_t ops : possible_ops) {
106 auto iter_s1 = s1.begin();
107 auto iter_s2 = s2.begin();
108 size_t cur_len = 0;
109
110 if (!ops) break;
111
112 while (iter_s1 != s1.end() && iter_s2 != s2.end()) {
113 if (*iter_s1 != *iter_s2) {
114 if (!ops) break;
115 if (ops & 1)
116 iter_s1++;
117 else if (ops & 2)
118 iter_s2++;
119#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
120# pragma GCC diagnostic push
121# pragma GCC diagnostic ignored "-Wconversion"
122#endif
123 ops >>= 2;
124#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && __GNUC__ < 10
125# pragma GCC diagnostic pop
126#endif
127 }
128 else {
129 cur_len++;
130 iter_s1++;
131 iter_s2++;
132 }
133 }
134
135 max_len = std::max(max_len, cur_len);
136 }
137
138 return (max_len >= score_cutoff) ? max_len : 0;
139}
140
141#ifdef RAPIDFUZZ_SIMD
142template <typename VecType, typename InputIt, int _lto_hack = RAPIDFUZZ_LTO_HACK>
143void lcs_simd(Range<size_t*> scores, const BlockPatternMatchVector& block, const Range<InputIt>& s2,
144 size_t score_cutoff) noexcept
145{
146# ifdef RAPIDFUZZ_AVX2
147 using namespace simd_avx2;
148# else
149 using namespace simd_sse2;
150# endif
151 auto score_iter = scores.begin();
152 static constexpr size_t alignment = native_simd<VecType>::alignment;
153 static constexpr size_t vecs = native_simd<uint64_t>::size;
154 assert(block.size() % vecs == 0);
155
156 static constexpr size_t interleaveCount = 3;
157
158 size_t cur_vec = 0;
159 for (; cur_vec + interleaveCount * vecs <= block.size(); cur_vec += interleaveCount * vecs) {
160 std::array<native_simd<VecType>, interleaveCount> S;
161 unroll<size_t, interleaveCount>([&](size_t j) { S[j] = static_cast<VecType>(-1); });
162
163 for (const auto& ch : s2) {
164 unroll<size_t, interleaveCount>([&](size_t j) {
165 alignas(32) std::array<uint64_t, vecs> stored;
166 unroll<size_t, vecs>([&](size_t i) { stored[i] = block.get(cur_vec + j * vecs + i, ch); });
167
168 native_simd<VecType> Matches(stored.data());
169 native_simd<VecType> u = S[j] & Matches;
170 S[j] = (S[j] + u) | (S[j] - u);
171 });
172 }
173
174 unroll<size_t, interleaveCount>([&](size_t j) {
175 auto counts = popcount(~S[j]);
176 unroll<size_t, counts.size()>([&](size_t i) {
177 *score_iter = (counts[i] >= score_cutoff) ? static_cast<size_t>(counts[i]) : 0;
178 score_iter++;
179 });
180 });
181 }
182
183 for (; cur_vec < block.size(); cur_vec += vecs) {
184 native_simd<VecType> S = static_cast<VecType>(-1);
185
186 for (const auto& ch : s2) {
187 alignas(alignment) std::array<uint64_t, vecs> stored;
188 unroll<size_t, vecs>([&](size_t i) { stored[i] = block.get(cur_vec + i, ch); });
189
190 native_simd<VecType> Matches(stored.data());
191 native_simd<VecType> u = S & Matches;
192 S = (S + u) | (S - u);
193 }
194
195 auto counts = popcount(~S);
196 unroll<size_t, counts.size()>([&](size_t i) {
197 *score_iter = (counts[i] >= score_cutoff) ? static_cast<size_t>(counts[i]) : 0;
198 score_iter++;
199 });
200 }
201}
202
203#endif
204
205template <size_t N, bool RecordMatrix, typename PMV, typename InputIt1, typename InputIt2>
206auto lcs_unroll(const PMV& block, const Range<InputIt1>&, const Range<InputIt2>& s2,
207 size_t score_cutoff = 0) -> LCSseqResult<RecordMatrix>
208{
209 uint64_t S[N];
210 unroll<size_t, N>([&](size_t i) { S[i] = ~UINT64_C(0); });
211
212 LCSseqResult<RecordMatrix> res;
213 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
214 auto& res_ = getMatrixRef(res);
215 res_.S = ShiftedBitMatrix<uint64_t>(s2.size(), N, ~UINT64_C(0));
216 }
217
218 auto iter_s2 = s2.begin();
219 for (size_t i = 0; i < s2.size(); ++i) {
220 uint64_t carry = 0;
221
222 static constexpr size_t unroll_factor = 3;
223 for (unsigned int j = 0; j < N / unroll_factor; ++j) {
224 unroll<size_t, unroll_factor>([&](size_t word_) {
225 size_t word = word_ + j * unroll_factor;
226 uint64_t Matches = block.get(word, *iter_s2);
227 uint64_t u = S[word] & Matches;
228 uint64_t x = addc64(S[word], u, carry, &carry);
229 S[word] = x | (S[word] - u);
230
231 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
232 auto& res_ = getMatrixRef(res);
233 res_.S[i][word] = S[word];
234 }
235 });
236 }
237
238 unroll<size_t, N % unroll_factor>([&](size_t word_) {
239 size_t word = word_ + N / unroll_factor * unroll_factor;
240 uint64_t Matches = block.get(word, *iter_s2);
241 uint64_t u = S[word] & Matches;
242 uint64_t x = addc64(S[word], u, carry, &carry);
243 S[word] = x | (S[word] - u);
244
245 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
246 auto& res_ = getMatrixRef(res);
247 res_.S[i][word] = S[word];
248 }
249 });
250
251 iter_s2++;
252 }
253
254 res.sim = 0;
255 unroll<size_t, N>([&](size_t i) { res.sim += popcount(~S[i]); });
256
257 if (res.sim < score_cutoff) res.sim = 0;
258
259 return res;
260}
261
268template <bool RecordMatrix, typename PMV, typename InputIt1, typename InputIt2>
269auto lcs_blockwise(const PMV& PM, const Range<InputIt1>& s1, const Range<InputIt2>& s2,
270 size_t score_cutoff = 0) -> LCSseqResult<RecordMatrix>
271{
272 assert(score_cutoff <= s1.size());
273 assert(score_cutoff <= s2.size());
274
275 size_t word_size = sizeof(uint64_t) * 8;
276 size_t words = PM.size();
277 std::vector<uint64_t> S(words, ~UINT64_C(0));
278
279 size_t band_width_left = s1.size() - score_cutoff;
280 size_t band_width_right = s2.size() - score_cutoff;
281
282 LCSseqResult<RecordMatrix> res;
283 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
284 auto& res_ = getMatrixRef(res);
285 size_t full_band = band_width_left + 1 + band_width_right;
286 size_t full_band_words = std::min(words, full_band / word_size + 2);
287 res_.S = ShiftedBitMatrix<uint64_t>(s2.size(), full_band_words, ~UINT64_C(0));
288 }
289
290 /* first_block is the index of the first block in Ukkonen band. */
291 size_t first_block = 0;
292 size_t last_block = std::min(words, ceil_div(band_width_left + 1, word_size));
293
294 auto iter_s2 = s2.begin();
295 for (size_t row = 0; row < s2.size(); ++row) {
296 uint64_t carry = 0;
297
298 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
299 auto& res_ = getMatrixRef(res);
300 res_.S.set_offset(row, static_cast<ptrdiff_t>(first_block * word_size));
301 }
302
303 for (size_t word = first_block; word < last_block; ++word) {
304 const uint64_t Matches = PM.get(word, *iter_s2);
305 uint64_t Stemp = S[word];
306
307 uint64_t u = Stemp & Matches;
308
309 uint64_t x = addc64(Stemp, u, carry, &carry);
310 S[word] = x | (Stemp - u);
311
312 RAPIDFUZZ_IF_CONSTEXPR (RecordMatrix) {
313 auto& res_ = getMatrixRef(res);
314 res_.S[row][word - first_block] = S[word];
315 }
316 }
317
318 if (row > band_width_right) first_block = (row - band_width_right) / word_size;
319
320 if (row + 1 + band_width_left <= s1.size())
321 last_block = ceil_div(row + 1 + band_width_left, word_size);
322
323 iter_s2++;
324 }
325
326 res.sim = 0;
327 for (uint64_t Stemp : S)
328 res.sim += popcount(~Stemp);
329
330 if (res.sim < score_cutoff) res.sim = 0;
331
332 return res;
333}
334
335template <typename PMV, typename InputIt1, typename InputIt2>
336size_t longest_common_subsequence(const PMV& PM, const Range<InputIt1>& s1, const Range<InputIt2>& s2,
337 size_t score_cutoff)
338{
339 assert(score_cutoff <= s1.size());
340 assert(score_cutoff <= s2.size());
341
342 size_t word_size = sizeof(uint64_t) * 8;
343 size_t words = PM.size();
344 size_t band_width_left = s1.size() - score_cutoff;
345 size_t band_width_right = s2.size() - score_cutoff;
346 size_t full_band = band_width_left + 1 + band_width_right;
347 size_t full_band_words = std::min(words, full_band / word_size + 2);
348
349 if (full_band_words < words) return lcs_blockwise<false>(PM, s1, s2, score_cutoff).sim;
350
351 auto nr = ceil_div(s1.size(), 64);
352 switch (nr) {
353 case 0: return 0;
354 case 1: return lcs_unroll<1, false>(PM, s1, s2, score_cutoff).sim;
355 case 2: return lcs_unroll<2, false>(PM, s1, s2, score_cutoff).sim;
356 case 3: return lcs_unroll<3, false>(PM, s1, s2, score_cutoff).sim;
357 case 4: return lcs_unroll<4, false>(PM, s1, s2, score_cutoff).sim;
358 case 5: return lcs_unroll<5, false>(PM, s1, s2, score_cutoff).sim;
359 case 6: return lcs_unroll<6, false>(PM, s1, s2, score_cutoff).sim;
360 case 7: return lcs_unroll<7, false>(PM, s1, s2, score_cutoff).sim;
361 case 8: return lcs_unroll<8, false>(PM, s1, s2, score_cutoff).sim;
362 default: return lcs_blockwise<false>(PM, s1, s2, score_cutoff).sim;
363 }
364}
365
366template <typename InputIt1, typename InputIt2>
367size_t longest_common_subsequence(const Range<InputIt1>& s1, const Range<InputIt2>& s2, size_t score_cutoff)
368{
369 if (s1.empty()) return 0;
370 if (s1.size() <= 64) return longest_common_subsequence(PatternMatchVector(s1), s1, s2, score_cutoff);
371
372 return longest_common_subsequence(BlockPatternMatchVector(s1), s1, s2, score_cutoff);
373}
374
375template <typename InputIt1, typename InputIt2>
376size_t lcs_seq_similarity(const BlockPatternMatchVector& block, Range<InputIt1> s1, Range<InputIt2> s2,
377 size_t score_cutoff)
378{
379 auto len1 = s1.size();
380 auto len2 = s2.size();
381
382 if (score_cutoff > len1 || score_cutoff > len2) return 0;
383
384 size_t max_misses = len1 + len2 - 2 * score_cutoff;
385
386 /* no edits are allowed */
387 if (max_misses == 0 || (max_misses == 1 && len1 == len2)) return s1 == s2 ? len1 : 0;
388
389 if (max_misses < abs_diff(len1, len2)) return 0;
390
391 // do this first, since we can not remove any affix in encoded form
392 if (max_misses >= 5) return longest_common_subsequence(block, s1, s2, score_cutoff);
393
394 /* common affix does not effect Levenshtein distance */
395 StringAffix affix = remove_common_affix(s1, s2);
396 size_t lcs_sim = affix.prefix_len + affix.suffix_len;
397 if (!s1.empty() && !s2.empty()) {
398 size_t adjusted_cutoff = score_cutoff >= lcs_sim ? score_cutoff - lcs_sim : 0;
399 lcs_sim += lcs_seq_mbleven2018(s1, s2, adjusted_cutoff);
400 }
401
402 return (lcs_sim >= score_cutoff) ? lcs_sim : 0;
403}
404
405template <typename InputIt1, typename InputIt2>
406size_t lcs_seq_similarity(Range<InputIt1> s1, Range<InputIt2> s2, size_t score_cutoff)
407{
408 auto len1 = s1.size();
409 auto len2 = s2.size();
410
411 // Swapping the strings so the second string is shorter
412 if (len1 < len2) return lcs_seq_similarity(s2, s1, score_cutoff);
413
414 if (score_cutoff > len1 || score_cutoff > len2) return 0;
415
416 size_t max_misses = len1 + len2 - 2 * score_cutoff;
417
418 /* no edits are allowed */
419 if (max_misses == 0 || (max_misses == 1 && len1 == len2)) return s1 == s2 ? len1 : 0;
420
421 if (max_misses < abs_diff(len1, len2)) return 0;
422
423 /* common affix does not effect Levenshtein distance */
424 StringAffix affix = remove_common_affix(s1, s2);
425 size_t lcs_sim = affix.prefix_len + affix.suffix_len;
426 if (s1.size() && s2.size()) {
427 size_t adjusted_cutoff = score_cutoff >= lcs_sim ? score_cutoff - lcs_sim : 0;
428 if (max_misses < 5)
429 lcs_sim += lcs_seq_mbleven2018(s1, s2, adjusted_cutoff);
430 else
431 lcs_sim += longest_common_subsequence(s1, s2, adjusted_cutoff);
432 }
433
434 return (lcs_sim >= score_cutoff) ? lcs_sim : 0;
435}
436
440template <typename InputIt1, typename InputIt2>
441Editops recover_alignment(const Range<InputIt1>& s1, const Range<InputIt2>& s2,
442 const LCSseqResult<true>& matrix, StringAffix affix)
443{
444 size_t len1 = s1.size();
445 size_t len2 = s2.size();
446 size_t dist = len1 + len2 - 2 * matrix.sim;
447 Editops editops(dist);
448 editops.set_src_len(len1 + affix.prefix_len + affix.suffix_len);
449 editops.set_dest_len(len2 + affix.prefix_len + affix.suffix_len);
450
451 if (dist == 0) return editops;
452
453#ifndef NDEBUG
454 size_t band_width_right = s2.size() - matrix.sim;
455#endif
456
457 auto col = len1;
458 auto row = len2;
459
460 while (row && col) {
461 /* Deletion */
462 if (matrix.S.test_bit(row - 1, col - 1)) {
463 assert(dist > 0);
464 assert(static_cast<ptrdiff_t>(col) >=
465 static_cast<ptrdiff_t>(row) - static_cast<ptrdiff_t>(band_width_right));
466 dist--;
467 col--;
468 editops[dist].type = EditType::Delete;
469 editops[dist].src_pos = col + affix.prefix_len;
470 editops[dist].dest_pos = row + affix.prefix_len;
471 }
472 else {
473 row--;
474
475 /* Insertion */
476 if (row && !(matrix.S.test_bit(row - 1, col - 1))) {
477 assert(dist > 0);
478 dist--;
479 editops[dist].type = EditType::Insert;
480 editops[dist].src_pos = col + affix.prefix_len;
481 editops[dist].dest_pos = row + affix.prefix_len;
482 }
483 /* Match */
484 else {
485 col--;
486 assert(s1[col] == s2[row]);
487 }
488 }
489 }
490
491 while (col) {
492 dist--;
493 col--;
494 editops[dist].type = EditType::Delete;
495 editops[dist].src_pos = col + affix.prefix_len;
496 editops[dist].dest_pos = row + affix.prefix_len;
497 }
498
499 while (row) {
500 dist--;
501 row--;
502 editops[dist].type = EditType::Insert;
503 editops[dist].src_pos = col + affix.prefix_len;
504 editops[dist].dest_pos = row + affix.prefix_len;
505 }
506
507 return editops;
508}
509
510template <typename InputIt1, typename InputIt2>
511LCSseqResult<true> lcs_matrix(const Range<InputIt1>& s1, const Range<InputIt2>& s2)
512{
513 size_t nr = ceil_div(s1.size(), 64);
514 switch (nr) {
515 case 0:
516 {
517 LCSseqResult<true> res;
518 res.sim = 0;
519 return res;
520 }
521 case 1: return lcs_unroll<1, true>(PatternMatchVector(s1), s1, s2);
522 case 2: return lcs_unroll<2, true>(BlockPatternMatchVector(s1), s1, s2);
523 case 3: return lcs_unroll<3, true>(BlockPatternMatchVector(s1), s1, s2);
524 case 4: return lcs_unroll<4, true>(BlockPatternMatchVector(s1), s1, s2);
525 case 5: return lcs_unroll<5, true>(BlockPatternMatchVector(s1), s1, s2);
526 case 6: return lcs_unroll<6, true>(BlockPatternMatchVector(s1), s1, s2);
527 case 7: return lcs_unroll<7, true>(BlockPatternMatchVector(s1), s1, s2);
528 case 8: return lcs_unroll<8, true>(BlockPatternMatchVector(s1), s1, s2);
529 default: return lcs_blockwise<true>(BlockPatternMatchVector(s1), s1, s2);
530 }
531}
532
533template <typename InputIt1, typename InputIt2>
534Editops lcs_seq_editops(Range<InputIt1> s1, Range<InputIt2> s2)
535{
536 /* prefix and suffix are no-ops, which do not need to be added to the editops */
537 StringAffix affix = remove_common_affix(s1, s2);
538
539 return recover_alignment(s1, s2, lcs_matrix(s1, s2), affix);
540}
541
542class LCSseq : public SimilarityBase<LCSseq, size_t, 0, std::numeric_limits<int64_t>::max()> {
543 friend SimilarityBase<LCSseq, size_t, 0, std::numeric_limits<int64_t>::max()>;
544 friend NormalizedMetricBase<LCSseq>;
545
546 template <typename InputIt1, typename InputIt2>
547 static size_t maximum(const Range<InputIt1>& s1, const Range<InputIt2>& s2)
548 {
549 return std::max(s1.size(), s2.size());
550 }
551
552 template <typename InputIt1, typename InputIt2>
553 static size_t _similarity(const Range<InputIt1>& s1, const Range<InputIt2>& s2, size_t score_cutoff,
554 size_t)
555 {
556 return lcs_seq_similarity(s1, s2, score_cutoff);
557 }
558};
559
560} // namespace detail
561} // namespace rapidfuzz