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,300 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Dynamic Time Warping — scalar reference implementation.
|
|
4
|
+
//
|
|
5
|
+
// This is the correctness oracle and the fallback for backends that
|
|
6
|
+
// don't ship a SIMD specialisation. The SIMD batch kernel (phase
|
|
7
|
+
// C.1b) will live in src/cpp/dtw_simd.hpp and produce identical
|
|
8
|
+
// scores; the test suite cross-checks both against this scalar
|
|
9
|
+
// reference.
|
|
10
|
+
//
|
|
11
|
+
// Algorithm: full DTW with optional Sakoe-Chiba band.
|
|
12
|
+
// Distance functions: L1 (|x - y|) and L2-squared ((x - y)^2).
|
|
13
|
+
//
|
|
14
|
+
// Token / Cell separation mirrors the existing wide-Farrar pattern:
|
|
15
|
+
// `Token` is the input ndarray dtype; `Cell` is the DP accumulator
|
|
16
|
+
// type. For int16 inputs we widen the cell to int32 so the running
|
|
17
|
+
// sum can't overflow on reasonable-length sequences.
|
|
18
|
+
|
|
19
|
+
#include <algorithm>
|
|
20
|
+
#include <cmath>
|
|
21
|
+
#include <cstddef>
|
|
22
|
+
#include <cstdint>
|
|
23
|
+
#include <limits>
|
|
24
|
+
#include <optional>
|
|
25
|
+
#include <span>
|
|
26
|
+
#include <type_traits>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
namespace stride_align::dtw {
|
|
30
|
+
|
|
31
|
+
enum class DistanceKind {
|
|
32
|
+
kL1,
|
|
33
|
+
kL2Squared,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
namespace detail {
|
|
37
|
+
|
|
38
|
+
// Per-cell distance. Widens to Cell before the subtract / abs to
|
|
39
|
+
// keep the int16 path from wrapping; the float paths compile to a
|
|
40
|
+
// straight subtract.
|
|
41
|
+
template <typename Token, typename Cell>
|
|
42
|
+
inline Cell cell_distance(Token x, Token y, DistanceKind dist) noexcept {
|
|
43
|
+
if constexpr (std::is_floating_point_v<Cell>) {
|
|
44
|
+
const Cell d = static_cast<Cell>(x) - static_cast<Cell>(y);
|
|
45
|
+
return (dist == DistanceKind::kL2Squared) ? d * d : std::fabs(d);
|
|
46
|
+
} else {
|
|
47
|
+
const Cell xc = static_cast<Cell>(x);
|
|
48
|
+
const Cell yc = static_cast<Cell>(y);
|
|
49
|
+
const Cell d = xc - yc;
|
|
50
|
+
if (dist == DistanceKind::kL2Squared) {
|
|
51
|
+
return d * d;
|
|
52
|
+
}
|
|
53
|
+
// For signed integers, fabs would do a float cast. std::abs on
|
|
54
|
+
// a wide-enough Cell is exact and lossless.
|
|
55
|
+
return d < 0 ? -d : d;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
} // namespace detail
|
|
60
|
+
|
|
61
|
+
// Resolve a window kwarg into an absolute Sakoe-Chiba radius in
|
|
62
|
+
// samples. A nullopt window means "no constraint" — returned as
|
|
63
|
+
// the maximum DP off-diagonal so the band check is a no-op.
|
|
64
|
+
//
|
|
65
|
+
// Caller responsibility:
|
|
66
|
+
// * window_samples == nullopt -> unconstrained
|
|
67
|
+
// * window_samples == 0 -> diagonal only (still valid)
|
|
68
|
+
inline std::size_t resolve_band_radius(
|
|
69
|
+
std::size_t query_size,
|
|
70
|
+
std::size_t target_size,
|
|
71
|
+
std::optional<std::size_t> window_samples) noexcept {
|
|
72
|
+
if (!window_samples.has_value()) {
|
|
73
|
+
return std::max(query_size, target_size);
|
|
74
|
+
}
|
|
75
|
+
// Even a 0-sample band is meaningful (diagonal only); preserve it.
|
|
76
|
+
const std::size_t hard_max = std::max(query_size, target_size);
|
|
77
|
+
return std::min(*window_samples, hard_max);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Keogh lower bound (LB_Keogh) for equal-length series under a
|
|
81
|
+
// Sakoe-Chiba radius ``r``. Builds the query envelope once and sums
|
|
82
|
+
// the distance of each target sample to the nearest point in
|
|
83
|
+
// [L_i, U_i]. Local costs are non-negative, so LB ≤ true DTW.
|
|
84
|
+
//
|
|
85
|
+
// Returns +inf if ``score_cutoff`` is set and LB already exceeds it
|
|
86
|
+
// (early reject). Unequal lengths return 0 (no bound applied here —
|
|
87
|
+
// callers may still use the band |m−n| > r impossibility check).
|
|
88
|
+
template <typename Token, typename Cell>
|
|
89
|
+
inline double lb_keogh(
|
|
90
|
+
std::span<const Token> query,
|
|
91
|
+
std::span<const Token> target,
|
|
92
|
+
DistanceKind dist,
|
|
93
|
+
std::size_t radius,
|
|
94
|
+
std::optional<double> score_cutoff = std::nullopt) {
|
|
95
|
+
const std::size_t n = query.size();
|
|
96
|
+
if (n == 0 || n != target.size()) {
|
|
97
|
+
return 0.0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Envelope of the query: U[i] = max Q[i-r .. i+r], L similarly.
|
|
101
|
+
std::vector<Cell> upper(n);
|
|
102
|
+
std::vector<Cell> lower(n);
|
|
103
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
104
|
+
const std::size_t lo = (i > radius) ? (i - radius) : 0U;
|
|
105
|
+
const std::size_t hi = std::min(n - 1U, i + radius);
|
|
106
|
+
Cell u = static_cast<Cell>(query[lo]);
|
|
107
|
+
Cell l = u;
|
|
108
|
+
for (std::size_t k = lo + 1U; k <= hi; ++k) {
|
|
109
|
+
const Cell v = static_cast<Cell>(query[k]);
|
|
110
|
+
if (v > u) u = v;
|
|
111
|
+
if (v < l) l = v;
|
|
112
|
+
}
|
|
113
|
+
upper[i] = u;
|
|
114
|
+
lower[i] = l;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Cell sum = Cell{0};
|
|
118
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
119
|
+
const Cell c = static_cast<Cell>(target[i]);
|
|
120
|
+
Cell d = Cell{0};
|
|
121
|
+
if (c > upper[i]) {
|
|
122
|
+
d = detail::cell_distance<Cell, Cell>(c, upper[i], dist);
|
|
123
|
+
} else if (c < lower[i]) {
|
|
124
|
+
d = detail::cell_distance<Cell, Cell>(c, lower[i], dist);
|
|
125
|
+
}
|
|
126
|
+
sum = static_cast<Cell>(sum + d);
|
|
127
|
+
if (score_cutoff.has_value() &&
|
|
128
|
+
static_cast<double>(sum) > *score_cutoff) {
|
|
129
|
+
return std::numeric_limits<double>::infinity();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return static_cast<double>(sum);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Build query envelope once for a 1-vs-N batch (equal-length targets).
|
|
136
|
+
template <typename Token, typename Cell>
|
|
137
|
+
struct QueryEnvelope {
|
|
138
|
+
std::vector<Cell> upper;
|
|
139
|
+
std::vector<Cell> lower;
|
|
140
|
+
std::size_t n = 0;
|
|
141
|
+
std::size_t radius = 0;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
template <typename Token, typename Cell>
|
|
145
|
+
inline QueryEnvelope<Token, Cell> build_query_envelope(
|
|
146
|
+
std::span<const Token> query,
|
|
147
|
+
std::size_t radius) {
|
|
148
|
+
QueryEnvelope<Token, Cell> env;
|
|
149
|
+
env.n = query.size();
|
|
150
|
+
env.radius = radius;
|
|
151
|
+
env.upper.resize(env.n);
|
|
152
|
+
env.lower.resize(env.n);
|
|
153
|
+
for (std::size_t i = 0; i < env.n; ++i) {
|
|
154
|
+
const std::size_t lo = (i > radius) ? (i - radius) : 0U;
|
|
155
|
+
const std::size_t hi = std::min(env.n - 1U, i + radius);
|
|
156
|
+
Cell u = static_cast<Cell>(query[lo]);
|
|
157
|
+
Cell l = u;
|
|
158
|
+
for (std::size_t k = lo + 1U; k <= hi; ++k) {
|
|
159
|
+
const Cell v = static_cast<Cell>(query[k]);
|
|
160
|
+
if (v > u) u = v;
|
|
161
|
+
if (v < l) l = v;
|
|
162
|
+
}
|
|
163
|
+
env.upper[i] = u;
|
|
164
|
+
env.lower[i] = l;
|
|
165
|
+
}
|
|
166
|
+
return env;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// LB_Keogh against a pre-built query envelope (equal length only).
|
|
170
|
+
template <typename Token, typename Cell>
|
|
171
|
+
inline double lb_keogh_with_envelope(
|
|
172
|
+
const QueryEnvelope<Token, Cell>& env,
|
|
173
|
+
std::span<const Token> target,
|
|
174
|
+
DistanceKind dist,
|
|
175
|
+
std::optional<double> score_cutoff = std::nullopt) {
|
|
176
|
+
if (env.n == 0 || env.n != target.size()) {
|
|
177
|
+
return 0.0;
|
|
178
|
+
}
|
|
179
|
+
Cell sum = Cell{0};
|
|
180
|
+
for (std::size_t i = 0; i < env.n; ++i) {
|
|
181
|
+
const Cell c = static_cast<Cell>(target[i]);
|
|
182
|
+
Cell d = Cell{0};
|
|
183
|
+
if (c > env.upper[i]) {
|
|
184
|
+
d = detail::cell_distance<Cell, Cell>(c, env.upper[i], dist);
|
|
185
|
+
} else if (c < env.lower[i]) {
|
|
186
|
+
d = detail::cell_distance<Cell, Cell>(c, env.lower[i], dist);
|
|
187
|
+
}
|
|
188
|
+
sum = static_cast<Cell>(sum + d);
|
|
189
|
+
if (score_cutoff.has_value() &&
|
|
190
|
+
static_cast<double>(sum) > *score_cutoff) {
|
|
191
|
+
return std::numeric_limits<double>::infinity();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return static_cast<double>(sum);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Empty-input policy: the dispatcher raises ValueError before
|
|
198
|
+
// calling the kernel. The scalar reference still guards defensively
|
|
199
|
+
// against an empty span (returns +inf in DistanceKind units) so a
|
|
200
|
+
// misuse from a unit test surfaces as a sentinel instead of UB.
|
|
201
|
+
//
|
|
202
|
+
// ``score_cutoff``: when set, return +inf as soon as we can prove the
|
|
203
|
+
// final DTW exceeds the cutoff (LB_Keogh pre-filter for equal lengths,
|
|
204
|
+
// band impossibility, and per-row early abandon in the DP).
|
|
205
|
+
template <typename Token, typename Cell>
|
|
206
|
+
inline double dtw_score_scalar(
|
|
207
|
+
std::span<const Token> query,
|
|
208
|
+
std::span<const Token> target,
|
|
209
|
+
DistanceKind dist,
|
|
210
|
+
std::optional<std::size_t> window_samples,
|
|
211
|
+
std::optional<double> score_cutoff = std::nullopt) {
|
|
212
|
+
const std::size_t m = query.size();
|
|
213
|
+
const std::size_t n = target.size();
|
|
214
|
+
if (m == 0 || n == 0) {
|
|
215
|
+
return std::numeric_limits<double>::infinity();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const std::size_t radius = resolve_band_radius(m, n, window_samples);
|
|
219
|
+
// Sakoe-Chiba impossibility: warping path cannot connect ends.
|
|
220
|
+
if (m > n + radius || n > m + radius) {
|
|
221
|
+
return std::numeric_limits<double>::infinity();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// LB_Keogh (equal length): cheap reject under a finite cutoff.
|
|
225
|
+
if (score_cutoff.has_value() && m == n) {
|
|
226
|
+
const double lb =
|
|
227
|
+
lb_keogh<Token, Cell>(query, target, dist, radius, score_cutoff);
|
|
228
|
+
if (lb > *score_cutoff) {
|
|
229
|
+
return std::numeric_limits<double>::infinity();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const Cell inf =
|
|
234
|
+
std::numeric_limits<Cell>::has_infinity
|
|
235
|
+
? std::numeric_limits<Cell>::infinity()
|
|
236
|
+
: std::numeric_limits<Cell>::max();
|
|
237
|
+
const bool has_cutoff = score_cutoff.has_value();
|
|
238
|
+
const Cell cutoff_cell = has_cutoff
|
|
239
|
+
? static_cast<Cell>(*score_cutoff)
|
|
240
|
+
: Cell{0};
|
|
241
|
+
|
|
242
|
+
// Two rolling rows. prev[j] = D(i-1, j); curr[j] = D(i, j).
|
|
243
|
+
// Size n + 1: index 0 is the "left of the array" sentinel.
|
|
244
|
+
std::vector<Cell> prev(n + 1, inf);
|
|
245
|
+
std::vector<Cell> curr(n + 1, inf);
|
|
246
|
+
prev[0] = Cell{0};
|
|
247
|
+
|
|
248
|
+
for (std::size_t i = 1; i <= m; ++i) {
|
|
249
|
+
curr[0] = inf;
|
|
250
|
+
// Sakoe-Chiba band: only walk j in [max(1, i - radius),
|
|
251
|
+
// min(n, i + radius)]. Outside the band stays +inf so the min
|
|
252
|
+
// never picks it.
|
|
253
|
+
const std::size_t j_lo = (i > radius) ? (i - radius) : 1;
|
|
254
|
+
const std::size_t j_hi = std::min(n, i + radius);
|
|
255
|
+
Cell row_min = inf;
|
|
256
|
+
for (std::size_t j = j_lo; j <= j_hi; ++j) {
|
|
257
|
+
const Cell d =
|
|
258
|
+
detail::cell_distance<Token, Cell>(query[i - 1], target[j - 1], dist);
|
|
259
|
+
const Cell candidate_diag = prev[j - 1];
|
|
260
|
+
const Cell candidate_up = prev[j];
|
|
261
|
+
const Cell candidate_left = curr[j - 1];
|
|
262
|
+
const Cell prev_min =
|
|
263
|
+
std::min(std::min(candidate_diag, candidate_up), candidate_left);
|
|
264
|
+
// Saturate-on-overflow guard for ints: if prev_min is already
|
|
265
|
+
// at the sentinel (inf for float, INT_MAX for int) we keep it
|
|
266
|
+
// there rather than wrapping.
|
|
267
|
+
if (prev_min == inf) {
|
|
268
|
+
curr[j] = inf;
|
|
269
|
+
} else {
|
|
270
|
+
curr[j] = d + prev_min;
|
|
271
|
+
}
|
|
272
|
+
if (curr[j] < row_min) {
|
|
273
|
+
row_min = curr[j];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Local costs ≥ 0 ⇒ if every live cell already exceeds cutoff,
|
|
277
|
+
// the final score cannot recover.
|
|
278
|
+
if (has_cutoff && row_min > cutoff_cell && row_min != inf) {
|
|
279
|
+
return std::numeric_limits<double>::infinity();
|
|
280
|
+
}
|
|
281
|
+
std::swap(prev, curr);
|
|
282
|
+
// Reset the row we're about to fill so the band's left/right
|
|
283
|
+
// edges retain their +inf sentinel on the next pass.
|
|
284
|
+
std::fill(curr.begin(), curr.end(), inf);
|
|
285
|
+
curr[0] = inf;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// After the final swap, prev holds row m.
|
|
289
|
+
const Cell final_cell = prev[n];
|
|
290
|
+
if (final_cell == inf) {
|
|
291
|
+
return std::numeric_limits<double>::infinity();
|
|
292
|
+
}
|
|
293
|
+
const double result = static_cast<double>(final_cell);
|
|
294
|
+
if (has_cutoff && result > *score_cutoff) {
|
|
295
|
+
return std::numeric_limits<double>::infinity();
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
} // namespace stride_align::dtw
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <limits>
|
|
7
|
+
#include <span>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
#include <string_view>
|
|
10
|
+
#include <utility>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
#include "stride_align/utf8.hpp"
|
|
14
|
+
|
|
15
|
+
namespace stride_align::encoded {
|
|
16
|
+
|
|
17
|
+
// A host adapter supplies the character width reported by its native
|
|
18
|
+
// encoding implementation. No conversion table is needed: stride-align only
|
|
19
|
+
// needs stable equality tokens, so a multibyte character is represented by
|
|
20
|
+
// its original bytes. PostgreSQL text cannot contain NUL, which makes the
|
|
21
|
+
// big-endian packing below collision-free for its one-to-four-byte server
|
|
22
|
+
// encodings (shorter encodings cannot alias a longer sequence padded by NUL).
|
|
23
|
+
struct EncodingProfile {
|
|
24
|
+
// One means that every byte is one character. Two and four are supported
|
|
25
|
+
// for fixed-width hosts even though PostgreSQL 18 has no such server
|
|
26
|
+
// encoding. Zero selects the variable-width reader.
|
|
27
|
+
std::size_t fixed_width = 0;
|
|
28
|
+
std::size_t max_width = 4;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct TokenizedText {
|
|
32
|
+
std::vector<std::uint32_t> tokens;
|
|
33
|
+
// Byte offsets has tokens.size() + 1 entries and always ends at input.size().
|
|
34
|
+
std::vector<std::size_t> byte_offsets;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
namespace detail {
|
|
38
|
+
|
|
39
|
+
inline std::uint32_t native_token(
|
|
40
|
+
const char* input,
|
|
41
|
+
std::size_t width) {
|
|
42
|
+
if (width == 0U || width > sizeof(std::uint32_t)) {
|
|
43
|
+
throw std::invalid_argument(
|
|
44
|
+
"native character width must be between one and four bytes");
|
|
45
|
+
}
|
|
46
|
+
std::uint32_t token = 0;
|
|
47
|
+
for (std::size_t index = 0; index < width; ++index) {
|
|
48
|
+
const auto byte = static_cast<std::uint8_t>(input[index]);
|
|
49
|
+
if (byte == 0U) {
|
|
50
|
+
throw std::invalid_argument("native text contains an embedded NUL byte");
|
|
51
|
+
}
|
|
52
|
+
token = (token << 8U) | byte;
|
|
53
|
+
}
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
template <typename CharacterWidth>
|
|
58
|
+
inline std::size_t character_width(
|
|
59
|
+
std::string_view remaining,
|
|
60
|
+
const EncodingProfile& profile,
|
|
61
|
+
CharacterWidth&& variable_width) {
|
|
62
|
+
const std::size_t width = profile.fixed_width != 0U
|
|
63
|
+
? profile.fixed_width
|
|
64
|
+
: static_cast<std::size_t>(variable_width(remaining));
|
|
65
|
+
if (width == 0U || width > profile.max_width || width > remaining.size() ||
|
|
66
|
+
width > sizeof(std::uint32_t)) {
|
|
67
|
+
throw std::invalid_argument("invalid character boundary in native encoding");
|
|
68
|
+
}
|
|
69
|
+
return width;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
template <typename Token>
|
|
73
|
+
inline std::vector<Token> fixed_tokens(
|
|
74
|
+
std::string_view input,
|
|
75
|
+
std::size_t width) {
|
|
76
|
+
if (width == 0U || input.size() % width != 0U) {
|
|
77
|
+
throw std::invalid_argument("truncated fixed-width native string");
|
|
78
|
+
}
|
|
79
|
+
std::vector<Token> output;
|
|
80
|
+
output.reserve(input.size() / width);
|
|
81
|
+
for (std::size_t offset = 0; offset < input.size(); offset += width) {
|
|
82
|
+
output.push_back(static_cast<Token>(native_token(input.data() + offset, width)));
|
|
83
|
+
}
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} // namespace detail
|
|
88
|
+
|
|
89
|
+
template <typename CharacterWidth>
|
|
90
|
+
inline TokenizedText tokenize(
|
|
91
|
+
std::string_view input,
|
|
92
|
+
const EncodingProfile& profile,
|
|
93
|
+
CharacterWidth&& variable_width) {
|
|
94
|
+
TokenizedText output;
|
|
95
|
+
output.tokens.reserve(input.size());
|
|
96
|
+
output.byte_offsets.reserve(input.size() + 1U);
|
|
97
|
+
std::size_t offset = 0;
|
|
98
|
+
while (offset < input.size()) {
|
|
99
|
+
output.byte_offsets.push_back(offset);
|
|
100
|
+
const std::string_view remaining = input.substr(offset);
|
|
101
|
+
const std::size_t width = detail::character_width(
|
|
102
|
+
remaining, profile, variable_width);
|
|
103
|
+
output.tokens.push_back(detail::native_token(input.data() + offset, width));
|
|
104
|
+
offset += width;
|
|
105
|
+
}
|
|
106
|
+
output.byte_offsets.push_back(input.size());
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
template <typename CharacterWidth>
|
|
111
|
+
inline std::size_t character_count(
|
|
112
|
+
std::string_view input,
|
|
113
|
+
const EncodingProfile& profile,
|
|
114
|
+
CharacterWidth&& variable_width) {
|
|
115
|
+
if (profile.fixed_width != 0U) {
|
|
116
|
+
if (input.size() % profile.fixed_width != 0U) {
|
|
117
|
+
throw std::invalid_argument("truncated fixed-width native string");
|
|
118
|
+
}
|
|
119
|
+
return input.size() / profile.fixed_width;
|
|
120
|
+
}
|
|
121
|
+
std::size_t count = 0;
|
|
122
|
+
std::size_t offset = 0;
|
|
123
|
+
while (offset < input.size()) {
|
|
124
|
+
const std::size_t width = detail::character_width(
|
|
125
|
+
input.substr(offset), profile, variable_width);
|
|
126
|
+
offset += width;
|
|
127
|
+
++count;
|
|
128
|
+
}
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
template <typename CharacterWidth>
|
|
133
|
+
inline utf8::PreparedPair prepare_pair(
|
|
134
|
+
std::string_view query,
|
|
135
|
+
std::string_view target,
|
|
136
|
+
const EncodingProfile& profile,
|
|
137
|
+
CharacterWidth&& variable_width,
|
|
138
|
+
std::size_t pack_threshold = 64U) {
|
|
139
|
+
const auto bytes = [](std::string_view input) {
|
|
140
|
+
return std::span<const std::uint8_t>(
|
|
141
|
+
reinterpret_cast<const std::uint8_t*>(input.data()), input.size());
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Every supported single-byte encoding, including SQL_ASCII and LATIN1,
|
|
145
|
+
// is already the ideal 8-bit representation. High-bit bytes are characters,
|
|
146
|
+
// not a reason to transcode or promote.
|
|
147
|
+
if (profile.fixed_width == 1U) {
|
|
148
|
+
return {utf8::TokenWidth::u8, true, false, bytes(query), bytes(target)};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (profile.fixed_width == 2U) {
|
|
152
|
+
return {
|
|
153
|
+
utf8::TokenWidth::u16,
|
|
154
|
+
false,
|
|
155
|
+
false,
|
|
156
|
+
detail::fixed_tokens<std::uint16_t>(query, 2U),
|
|
157
|
+
detail::fixed_tokens<std::uint16_t>(target, 2U)};
|
|
158
|
+
}
|
|
159
|
+
if (profile.fixed_width == 4U) {
|
|
160
|
+
return {
|
|
161
|
+
utf8::TokenWidth::u32,
|
|
162
|
+
false,
|
|
163
|
+
false,
|
|
164
|
+
detail::fixed_tokens<std::uint32_t>(query, 4U),
|
|
165
|
+
detail::fixed_tokens<std::uint32_t>(target, 4U)};
|
|
166
|
+
}
|
|
167
|
+
if (profile.fixed_width != 0U) {
|
|
168
|
+
throw std::invalid_argument("unsupported fixed native character width");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Variable-width multibyte databases still get the zero-copy ASCII path.
|
|
172
|
+
if (utf8::is_ascii(query) && utf8::is_ascii(target)) {
|
|
173
|
+
return {utf8::TokenWidth::u8, true, false, bytes(query), bytes(target)};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Variable-width encodings use opaque native character keys, so equality
|
|
177
|
+
// remains exact without translating the source text to UTF-8.
|
|
178
|
+
auto query_tokens = tokenize(query, profile, variable_width).tokens;
|
|
179
|
+
auto target_tokens = tokenize(target, profile, variable_width).tokens;
|
|
180
|
+
|
|
181
|
+
// Match the UTF-8 appliance: short pairs avoid building a hash table and
|
|
182
|
+
// use the smallest fixed-width lane that can hold their native character
|
|
183
|
+
// keys. Long pairs amortize the Swiss-table pass and benefit from dense
|
|
184
|
+
// cardinality packing.
|
|
185
|
+
if (query_tokens.size() + target_tokens.size() < pack_threshold) {
|
|
186
|
+
std::uint32_t maximum = 0;
|
|
187
|
+
for (const std::uint32_t token : query_tokens) {
|
|
188
|
+
maximum = std::max(maximum, token);
|
|
189
|
+
}
|
|
190
|
+
for (const std::uint32_t token : target_tokens) {
|
|
191
|
+
maximum = std::max(maximum, token);
|
|
192
|
+
}
|
|
193
|
+
if (maximum <= std::numeric_limits<std::uint16_t>::max()) {
|
|
194
|
+
return {
|
|
195
|
+
utf8::TokenWidth::u16,
|
|
196
|
+
false,
|
|
197
|
+
false,
|
|
198
|
+
utf8::detail::narrow<std::uint16_t>(query_tokens),
|
|
199
|
+
utf8::detail::narrow<std::uint16_t>(target_tokens)};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
utf8::TokenWidth::u32,
|
|
203
|
+
false,
|
|
204
|
+
false,
|
|
205
|
+
std::move(query_tokens),
|
|
206
|
+
std::move(target_tokens)};
|
|
207
|
+
}
|
|
208
|
+
auto packed = utf8::detail::pack_pair(query_tokens, target_tokens);
|
|
209
|
+
if (packed.distinct <=
|
|
210
|
+
static_cast<std::size_t>(std::numeric_limits<std::uint8_t>::max())) {
|
|
211
|
+
return {
|
|
212
|
+
utf8::TokenWidth::u8,
|
|
213
|
+
false,
|
|
214
|
+
true,
|
|
215
|
+
utf8::detail::narrow<std::uint8_t>(packed.query),
|
|
216
|
+
utf8::detail::narrow<std::uint8_t>(packed.target)};
|
|
217
|
+
}
|
|
218
|
+
if (packed.distinct <=
|
|
219
|
+
static_cast<std::size_t>(std::numeric_limits<std::uint16_t>::max())) {
|
|
220
|
+
return {
|
|
221
|
+
utf8::TokenWidth::u16,
|
|
222
|
+
false,
|
|
223
|
+
true,
|
|
224
|
+
utf8::detail::narrow<std::uint16_t>(packed.query),
|
|
225
|
+
utf8::detail::narrow<std::uint16_t>(packed.target)};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
utf8::TokenWidth::u32,
|
|
229
|
+
false,
|
|
230
|
+
true,
|
|
231
|
+
std::move(packed.query),
|
|
232
|
+
std::move(packed.target)};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
} // namespace stride_align::encoded
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Hamming distance — count of positions where two equal-length sequences
|
|
4
|
+
// differ. Unlike Levenshtein, this requires identical lengths; host adapters
|
|
5
|
+
// decide how to report a length mismatch.
|
|
6
|
+
//
|
|
7
|
+
// No DP recurrence, no carry chains: every position is independent. That
|
|
8
|
+
// makes it the most SIMD-friendly distance metric in this library —
|
|
9
|
+
// see src/cpp/hamming_simd.hpp for the within-string and across-target
|
|
10
|
+
// kernels.
|
|
11
|
+
|
|
12
|
+
#include <cstddef>
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
#include <span>
|
|
15
|
+
|
|
16
|
+
namespace stride_align::hamming {
|
|
17
|
+
|
|
18
|
+
// Contiguous byte fast path. The deliberately simple reduction is easier for
|
|
19
|
+
// compilers to vectorize than a hand-written per-ISA implementation on every
|
|
20
|
+
// backend we support.
|
|
21
|
+
inline std::size_t hamming_u8(
|
|
22
|
+
std::span<const std::uint8_t> a,
|
|
23
|
+
std::span<const std::uint8_t> b) noexcept {
|
|
24
|
+
std::size_t distance = a.size();
|
|
25
|
+
for (std::size_t index = 0; index < a.size(); ++index) {
|
|
26
|
+
distance -= static_cast<std::size_t>(a[index] == b[index]);
|
|
27
|
+
}
|
|
28
|
+
return distance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Scalar reference. Used as correctness oracle and as the fallback when
|
|
32
|
+
// inputs aren't byte-compatible (wider unicode etc.). Caller guarantees
|
|
33
|
+
// `a.size() == b.size()`.
|
|
34
|
+
template <typename Token>
|
|
35
|
+
inline std::size_t hamming_scalar(
|
|
36
|
+
std::span<const Token> a,
|
|
37
|
+
std::span<const Token> b) noexcept {
|
|
38
|
+
const std::size_t n = a.size();
|
|
39
|
+
std::size_t count = 0;
|
|
40
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
41
|
+
if (a[i] != b[i]) {
|
|
42
|
+
++count;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
inline double normalize(std::size_t distance, std::size_t n) noexcept {
|
|
49
|
+
if (n == 0U) {
|
|
50
|
+
return 1.0;
|
|
51
|
+
}
|
|
52
|
+
return 1.0 - (static_cast<double>(distance) / static_cast<double>(n));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} // namespace stride_align::hamming
|