stride-align 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/NOTICE +255 -0
  4. data/README.md +107 -0
  5. data/data/bmpm_data/gen_approx_any.txt +131 -0
  6. data/data/bmpm_data/gen_approx_arabic.txt +26 -0
  7. data/data/bmpm_data/gen_approx_common.txt +233 -0
  8. data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
  9. data/data/bmpm_data/gen_approx_czech.txt +18 -0
  10. data/data/bmpm_data/gen_approx_dutch.txt +18 -0
  11. data/data/bmpm_data/gen_approx_english.txt +47 -0
  12. data/data/bmpm_data/gen_approx_french.txt +25 -0
  13. data/data/bmpm_data/gen_approx_german.txt +73 -0
  14. data/data/bmpm_data/gen_approx_greek.txt +18 -0
  15. data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
  16. data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
  17. data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
  18. data/data/bmpm_data/gen_approx_italian.txt +18 -0
  19. data/data/bmpm_data/gen_approx_polish.txt +84 -0
  20. data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
  21. data/data/bmpm_data/gen_approx_romanian.txt +18 -0
  22. data/data/bmpm_data/gen_approx_russian.txt +48 -0
  23. data/data/bmpm_data/gen_approx_spanish.txt +21 -0
  24. data/data/bmpm_data/gen_approx_turkish.txt +18 -0
  25. data/data/bmpm_data/gen_exact_any.txt +40 -0
  26. data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
  27. data/data/bmpm_data/gen_exact_arabic.txt +18 -0
  28. data/data/bmpm_data/gen_exact_common.txt +32 -0
  29. data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
  30. data/data/bmpm_data/gen_exact_czech.txt +18 -0
  31. data/data/bmpm_data/gen_exact_dutch.txt +18 -0
  32. data/data/bmpm_data/gen_exact_english.txt +18 -0
  33. data/data/bmpm_data/gen_exact_french.txt +18 -0
  34. data/data/bmpm_data/gen_exact_german.txt +18 -0
  35. data/data/bmpm_data/gen_exact_greek.txt +18 -0
  36. data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
  37. data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
  38. data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
  39. data/data/bmpm_data/gen_exact_italian.txt +18 -0
  40. data/data/bmpm_data/gen_exact_polish.txt +23 -0
  41. data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
  42. data/data/bmpm_data/gen_exact_romanian.txt +18 -0
  43. data/data/bmpm_data/gen_exact_russian.txt +19 -0
  44. data/data/bmpm_data/gen_exact_spanish.txt +19 -0
  45. data/data/bmpm_data/gen_exact_turkish.txt +18 -0
  46. data/data/bmpm_data/gen_hebrew_common.txt +113 -0
  47. data/data/bmpm_data/gen_lang.txt +295 -0
  48. data/data/bmpm_data/gen_languages.txt +36 -0
  49. data/data/bmpm_data/gen_rules_any.txt +367 -0
  50. data/data/bmpm_data/gen_rules_arabic.txt +76 -0
  51. data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
  52. data/data/bmpm_data/gen_rules_czech.txt +67 -0
  53. data/data/bmpm_data/gen_rules_dutch.txt +78 -0
  54. data/data/bmpm_data/gen_rules_english.txt +113 -0
  55. data/data/bmpm_data/gen_rules_french.txt +114 -0
  56. data/data/bmpm_data/gen_rules_german.txt +129 -0
  57. data/data/bmpm_data/gen_rules_greek.txt +97 -0
  58. data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
  59. data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
  60. data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
  61. data/data/bmpm_data/gen_rules_italian.txt +77 -0
  62. data/data/bmpm_data/gen_rules_polish.txt +185 -0
  63. data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
  64. data/data/bmpm_data/gen_rules_romanian.txt +64 -0
  65. data/data/bmpm_data/gen_rules_russian.txt +142 -0
  66. data/data/bmpm_data/gen_rules_spanish.txt +85 -0
  67. data/data/bmpm_data/gen_rules_turkish.txt +50 -0
  68. data/data/keyboard_data/qwerty.npy +0 -0
  69. data/data/matrix_data/BLOSUM100 +31 -0
  70. data/data/matrix_data/BLOSUM30 +31 -0
  71. data/data/matrix_data/BLOSUM35 +31 -0
  72. data/data/matrix_data/BLOSUM40 +31 -0
  73. data/data/matrix_data/BLOSUM45 +25 -0
  74. data/data/matrix_data/BLOSUM50 +25 -0
  75. data/data/matrix_data/BLOSUM55 +31 -0
  76. data/data/matrix_data/BLOSUM60 +31 -0
  77. data/data/matrix_data/BLOSUM62 +25 -0
  78. data/data/matrix_data/BLOSUM65 +31 -0
  79. data/data/matrix_data/BLOSUM70 +31 -0
  80. data/data/matrix_data/BLOSUM75 +31 -0
  81. data/data/matrix_data/BLOSUM80 +25 -0
  82. data/data/matrix_data/BLOSUM85 +31 -0
  83. data/data/matrix_data/BLOSUM90 +25 -0
  84. data/data/matrix_data/NUC.4.4 +25 -0
  85. data/data/matrix_data/PAM10 +34 -0
  86. data/data/matrix_data/PAM100 +34 -0
  87. data/data/matrix_data/PAM110 +34 -0
  88. data/data/matrix_data/PAM120 +34 -0
  89. data/data/matrix_data/PAM130 +34 -0
  90. data/data/matrix_data/PAM140 +34 -0
  91. data/data/matrix_data/PAM150 +34 -0
  92. data/data/matrix_data/PAM160 +34 -0
  93. data/data/matrix_data/PAM170 +34 -0
  94. data/data/matrix_data/PAM180 +34 -0
  95. data/data/matrix_data/PAM190 +34 -0
  96. data/data/matrix_data/PAM20 +34 -0
  97. data/data/matrix_data/PAM200 +34 -0
  98. data/data/matrix_data/PAM210 +34 -0
  99. data/data/matrix_data/PAM220 +34 -0
  100. data/data/matrix_data/PAM230 +34 -0
  101. data/data/matrix_data/PAM240 +34 -0
  102. data/data/matrix_data/PAM250 +25 -0
  103. data/data/matrix_data/PAM260 +34 -0
  104. data/data/matrix_data/PAM270 +34 -0
  105. data/data/matrix_data/PAM280 +34 -0
  106. data/data/matrix_data/PAM290 +34 -0
  107. data/data/matrix_data/PAM30 +25 -0
  108. data/data/matrix_data/PAM300 +34 -0
  109. data/data/matrix_data/PAM310 +34 -0
  110. data/data/matrix_data/PAM320 +34 -0
  111. data/data/matrix_data/PAM330 +34 -0
  112. data/data/matrix_data/PAM340 +34 -0
  113. data/data/matrix_data/PAM350 +34 -0
  114. data/data/matrix_data/PAM360 +34 -0
  115. data/data/matrix_data/PAM370 +34 -0
  116. data/data/matrix_data/PAM380 +34 -0
  117. data/data/matrix_data/PAM390 +34 -0
  118. data/data/matrix_data/PAM40 +34 -0
  119. data/data/matrix_data/PAM400 +34 -0
  120. data/data/matrix_data/PAM410 +34 -0
  121. data/data/matrix_data/PAM420 +34 -0
  122. data/data/matrix_data/PAM430 +34 -0
  123. data/data/matrix_data/PAM440 +34 -0
  124. data/data/matrix_data/PAM450 +34 -0
  125. data/data/matrix_data/PAM460 +34 -0
  126. data/data/matrix_data/PAM470 +34 -0
  127. data/data/matrix_data/PAM480 +34 -0
  128. data/data/matrix_data/PAM490 +34 -0
  129. data/data/matrix_data/PAM50 +34 -0
  130. data/data/matrix_data/PAM500 +34 -0
  131. data/data/matrix_data/PAM60 +34 -0
  132. data/data/matrix_data/PAM70 +25 -0
  133. data/data/matrix_data/PAM80 +34 -0
  134. data/data/matrix_data/PAM90 +34 -0
  135. data/ext/stride_align/backend_avx2.cpp +2 -0
  136. data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
  137. data/ext/stride_align/backend_generic.cpp +3 -0
  138. data/ext/stride_align/backend_impl.hpp +983 -0
  139. data/ext/stride_align/backend_lasx.cpp +2 -0
  140. data/ext/stride_align/backend_lsx.cpp +2 -0
  141. data/ext/stride_align/backend_neon.cpp +2 -0
  142. data/ext/stride_align/backend_rvv.cpp +2 -0
  143. data/ext/stride_align/backend_sse41.cpp +2 -0
  144. data/ext/stride_align/backend_sve.cpp +2 -0
  145. data/ext/stride_align/backend_sve2.cpp +2 -0
  146. data/ext/stride_align/backend_vsx.cpp +2 -0
  147. data/ext/stride_align/beider_morse_impl.cpp +5 -0
  148. data/ext/stride_align/cpu_detect.cpp +172 -0
  149. data/ext/stride_align/cpu_detect.hpp +6 -0
  150. data/ext/stride_align/extconf.rb +114 -0
  151. data/ext/stride_align/target_profile.hpp +82 -0
  152. data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
  153. data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
  154. data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
  155. data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
  156. data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
  157. data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
  158. data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
  159. data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
  160. data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
  161. data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
  162. data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
  163. data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
  164. data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
  165. data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
  166. data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
  167. data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
  168. data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
  169. data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
  170. data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
  171. data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
  172. data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
  173. data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
  174. data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
  175. data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
  176. data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
  177. data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
  178. data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
  179. data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
  180. data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
  181. data/lib/stride_align/algorithms.rb +296 -0
  182. data/lib/stride_align/alignment_path.rb +217 -0
  183. data/lib/stride_align/backend.rb +47 -0
  184. data/lib/stride_align/batch.rb +705 -0
  185. data/lib/stride_align/core.rb +180 -0
  186. data/lib/stride_align/keyboard.rb +200 -0
  187. data/lib/stride_align/matrices.rb +403 -0
  188. data/lib/stride_align/version.rb +5 -0
  189. data/lib/stride_align.rb +87 -0
  190. metadata +231 -0
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrideAlign
4
+ class SubstitutionMatrix
5
+ attr_reader :name, :alphabet, :matrix, :gap_score, :wildcard,
6
+ :gap_open, :gap_extend, :stride, :max_abs, :matrix_bytes
7
+
8
+ def initialize(name:, alphabet:, matrix:, gap_score: -4, wildcard: "X",
9
+ gap_open: nil, gap_extend: nil)
10
+ raise ArgumentError, "name must be one String" unless name.is_a?(String)
11
+ raise ArgumentError, "alphabet must be one String" unless alphabet.is_a?(String)
12
+
13
+ symbols = alphabet.each_char.to_a
14
+ raise ArgumentError, "alphabet has duplicate symbols" unless symbols.uniq.length == symbols.length
15
+ unless matrix.is_a?(Array) && matrix.length == symbols.length &&
16
+ matrix.all? { |row| row.is_a?(Array) && row.length == symbols.length }
17
+ raise ArgumentError, "matrix dimensions must match the alphabet"
18
+ end
19
+
20
+ values = matrix.map do |row|
21
+ row.map do |score|
22
+ unless score.is_a?(Numeric) && score.finite? && score == score.to_i
23
+ raise ArgumentError, "matrix must contain finite integer scores"
24
+ end
25
+ score = score.to_i
26
+ raise ArgumentError, "matrix scores must fit in signed 8-bit integers" unless (-128..127).cover?(score)
27
+ score
28
+ end.freeze
29
+ end.freeze
30
+ unless wildcard.is_a?(String) && wildcard.each_char.count == 1 && symbols.include?(wildcard)
31
+ raise ArgumentError, "wildcard must be one symbol in alphabet"
32
+ end
33
+
34
+ @name = name.dup.freeze
35
+ @alphabet = alphabet.dup.freeze
36
+ @matrix = values
37
+ @gap_score = gap_score
38
+ @wildcard = wildcard.dup.freeze
39
+ @gap_open = gap_open
40
+ @gap_extend = gap_extend
41
+ @stride = symbols.length
42
+ @max_abs = values.flatten.map(&:abs).max || 0
43
+ @matrix_bytes = values.flatten.pack("c*").freeze
44
+ @symbol_indices = symbols.each_with_index.to_h.freeze
45
+ @wildcard_index = @symbol_indices.fetch(@wildcard)
46
+ end
47
+
48
+ def encode(sequence)
49
+ StrideAlign.substitution_matrix_encode(self, sequence)
50
+ end
51
+
52
+ def score(query, target)
53
+ StrideAlign.substitution_matrix_score(self, query, target)
54
+ end
55
+
56
+ def score_step_limit(gap_score: nil, gap_open: nil, gap_extend: nil)
57
+ StrideAlign.substitution_matrix_score_step_limit(
58
+ self, gap_score: gap_score, gap_open: gap_open, gap_extend: gap_extend
59
+ )
60
+ end
61
+
62
+ def transpose(name: "#{@name}.T")
63
+ StrideAlign.substitution_matrix_transpose(self, name: name)
64
+ end
65
+
66
+ def encode_one(sequence)
67
+ raise ArgumentError, "sequence must be a String" unless sequence.is_a?(String)
68
+
69
+ sequence.encode(Encoding::UTF_8).each_char.map do |symbol|
70
+ @symbol_indices.fetch(symbol, @wildcard_index)
71
+ end
72
+ end
73
+ end
74
+
75
+ class << self
76
+ def SubstitutionMatrix(name:, alphabet:, matrix:, gap_score: -4,
77
+ wildcard: "X", gap_open: nil, gap_extend: nil)
78
+ SubstitutionMatrix.new(
79
+ name: name, alphabet: alphabet, matrix: matrix,
80
+ gap_score: gap_score, wildcard: wildcard,
81
+ gap_open: gap_open, gap_extend: gap_extend
82
+ )
83
+ end
84
+
85
+ def substitution_matrix_encode(substitution_matrix, sequence)
86
+ validate_substitution_matrix(substitution_matrix)
87
+ if sequence.is_a?(Array)
88
+ sequence.map { |value| value.nil? ? nil : substitution_matrix.encode_one(value) }
89
+ elsif sequence.nil?
90
+ nil
91
+ else
92
+ substitution_matrix.encode_one(sequence)
93
+ end
94
+ end
95
+
96
+ def substitution_matrix_score(substitution_matrix, query, target)
97
+ validate_substitution_matrix(substitution_matrix)
98
+ smith_waterman_score(
99
+ query, target, matrix: substitution_matrix,
100
+ gap_score: substitution_matrix.gap_score
101
+ )
102
+ end
103
+
104
+ def substitution_matrix_score_step_limit(substitution_matrix,
105
+ gap_score: nil,
106
+ gap_open: nil,
107
+ gap_extend: nil)
108
+ validate_substitution_matrix(substitution_matrix)
109
+ if gap_score.nil? && gap_open.nil? && gap_extend.nil?
110
+ gap_score = substitution_matrix.gap_score
111
+ gap_open = substitution_matrix.gap_open
112
+ gap_extend = substitution_matrix.gap_extend
113
+ end
114
+ [substitution_matrix.max_abs, gap_score, gap_open, gap_extend]
115
+ .compact.map(&:abs).max
116
+ end
117
+
118
+ def substitution_matrix_transpose(substitution_matrix,
119
+ name: "#{substitution_matrix.name}.T")
120
+ validate_substitution_matrix(substitution_matrix)
121
+ SubstitutionMatrix.new(
122
+ name: name,
123
+ alphabet: substitution_matrix.alphabet,
124
+ matrix: substitution_matrix.matrix.transpose,
125
+ gap_score: substitution_matrix.gap_score,
126
+ wildcard: substitution_matrix.wildcard,
127
+ gap_open: substitution_matrix.gap_open,
128
+ gap_extend: substitution_matrix.gap_extend
129
+ )
130
+ end
131
+
132
+ def substitution_matrix_from_ncbi_text(text, name: "<unnamed>",
133
+ gap_score: -4, wildcard: "X",
134
+ gap_open: nil, gap_extend: nil)
135
+ raise ArgumentError, "text must be one String" unless text.is_a?(String)
136
+
137
+ lines = text.lines.map(&:strip).reject { |line| line.empty? || line.start_with?("#") }
138
+ raise ArgumentError, "NCBI matrix text has no header or data rows" if lines.length < 2
139
+
140
+ header = lines.shift.split
141
+ rows = lines.map(&:split)
142
+ labels = rows.map(&:first)
143
+ raise ArgumentError, "NCBI matrix row labels do not match column header" unless labels == header
144
+
145
+ values = rows.map do |row|
146
+ raise ArgumentError, "NCBI matrix row has the wrong number of values" unless row.length == header.length + 1
147
+ row.drop(1).map do |token|
148
+ Integer(token, 10)
149
+ rescue ArgumentError
150
+ raise ArgumentError, "non-integer score in NCBI matrix row"
151
+ end
152
+ end
153
+ SubstitutionMatrix.new(
154
+ name: name, alphabet: header.join, matrix: values,
155
+ gap_score: gap_score, wildcard: wildcard,
156
+ gap_open: gap_open, gap_extend: gap_extend
157
+ )
158
+ end
159
+
160
+ def identity_matrix(alphabet, match: 1, mismatch: 0, wildcard: "*",
161
+ name: "IDENTITY", gap_score: -1,
162
+ gap_open: nil, gap_extend: nil)
163
+ raise ArgumentError, "alphabet must be a String" unless alphabet.is_a?(String)
164
+
165
+ symbols = alphabet.each_char.to_a
166
+ symbols << wildcard unless symbols.include?(wildcard)
167
+ values = Array.new(symbols.length) { Array.new(symbols.length, mismatch) }
168
+ symbols.length.times { |index| values[index][index] = match }
169
+ wildcard_index = symbols.index(wildcard)
170
+ symbols.length.times do |index|
171
+ values[wildcard_index][index] = mismatch
172
+ values[index][wildcard_index] = mismatch
173
+ end
174
+ SubstitutionMatrix.new(
175
+ name: name, alphabet: symbols.join, matrix: values,
176
+ gap_score: gap_score, wildcard: wildcard,
177
+ gap_open: gap_open, gap_extend: gap_extend
178
+ )
179
+ end
180
+
181
+ def ascii_matrix(match: 1, mismatch: -1, name: "ASCII", gap_score: -1,
182
+ gap_open: nil, gap_extend: nil)
183
+ symbols = (0..127).map(&:chr)
184
+ values = Array.new(128) { Array.new(128, mismatch) }
185
+ 128.times { |index| values[index][index] = match }
186
+ 128.times do |index|
187
+ values[127][index] = mismatch
188
+ values[index][127] = mismatch
189
+ end
190
+ SubstitutionMatrix.new(
191
+ name: name, alphabet: symbols.join, matrix: values,
192
+ gap_score: gap_score, wildcard: symbols[127],
193
+ gap_open: gap_open, gap_extend: gap_extend
194
+ )
195
+ end
196
+
197
+ private
198
+
199
+ def validate_substitution_matrix(matrix)
200
+ raise ArgumentError, "matrix must be a SubstitutionMatrix" unless matrix.is_a?(SubstitutionMatrix)
201
+ end
202
+
203
+ def matrix_defaults(name)
204
+ if name.start_with?("BLOSUM")
205
+ level = name.delete_prefix("BLOSUM").to_i
206
+ return [-5, -10, -1] if level >= 80
207
+ return [-5, -11, -1] if level >= 62
208
+ return [-6, -13, -2] if level >= 50
209
+ return [-6, -14, -2]
210
+ end
211
+ if name.start_with?("PAM")
212
+ level = name.delete_prefix("PAM").to_i
213
+ return [-5, -9, -1] if level <= 30
214
+ return [-6, -10, -1] if level <= 120
215
+ return [-7, -12, -1] if level <= 200
216
+ return [-8, -14, -2]
217
+ end
218
+ [-5, -10, -2]
219
+ end
220
+
221
+ def read_ncbi_matrix(filename, name = filename, wildcard = "X")
222
+ defaults = matrix_defaults(name)
223
+ path = File.join(__data_directory("matrix_data"), filename)
224
+ substitution_matrix_from_ncbi_text(
225
+ File.read(path, mode: "r:BOM|UTF-8"), name: name,
226
+ gap_score: defaults[0], wildcard: wildcard,
227
+ gap_open: defaults[1], gap_extend: defaults[2]
228
+ )
229
+ end
230
+
231
+ def matrix_catalog_load(name)
232
+ @matrix_catalog ||= {}
233
+ return @matrix_catalog[name] if @matrix_catalog.key?(name)
234
+
235
+ value = case name
236
+ when "nuc44"
237
+ read_ncbi_matrix("NUC.4.4", "NUC.4.4", "N")
238
+ when "dna_match"
239
+ identity_matrix("ACGT", match: 5, mismatch: -4,
240
+ wildcard: "N", name: "DNA_MATCH", gap_score: -5)
241
+ when "ascii_text"
242
+ ascii_matrix(name: "ASCII")
243
+ when /\Ablosum\d+\z/
244
+ label = name.upcase
245
+ read_ncbi_matrix(label, label)
246
+ when /\Apam\d+\z/
247
+ label = name.upcase
248
+ read_ncbi_matrix(label, label)
249
+ else
250
+ raise ArgumentError, "unknown substitution matrix: #{name}"
251
+ end
252
+ @matrix_catalog[name] = value
253
+ end
254
+
255
+ def matrix_pairwise(query, target, substitution_matrix, local,
256
+ gap_score: nil, gap_open_score: nil,
257
+ gap_extend_score: nil)
258
+ validate_substitution_matrix(substitution_matrix)
259
+ pairwise(query, target) do |left, right|
260
+ if left.nil? || right.nil?
261
+ nil
262
+ else
263
+ matrix_alignment(
264
+ left, right, substitution_matrix, local,
265
+ gap_score: gap_score, gap_open_score: gap_open_score,
266
+ gap_extend_score: gap_extend_score
267
+ )
268
+ end
269
+ end
270
+ end
271
+
272
+ def matrix_alignment(query, target, substitution_matrix, local,
273
+ gap_score: nil, gap_open_score: nil,
274
+ gap_extend_score: nil, traceback: false)
275
+ validate_substitution_matrix(substitution_matrix)
276
+ query = scalar_query(query)
277
+ target = scalar_query(target)
278
+ q = substitution_matrix.encode_one(query)
279
+ t = substitution_matrix.encode_one(target)
280
+ linear_gap = gap_score.nil? ? substitution_matrix.gap_score : gap_score
281
+ open_score = gap_open_score.nil? ? linear_gap : gap_open_score
282
+ extend_score = gap_extend_score.nil? ? open_score : gap_extend_score
283
+ rows = q.length + 1
284
+ columns = t.length + 1
285
+ negative_infinity = -Float::INFINITY
286
+ h = Array.new(rows) { Array.new(columns, local ? 0 : negative_infinity) }
287
+ vertical = Array.new(rows) { Array.new(columns, negative_infinity) }
288
+ horizontal = Array.new(rows) { Array.new(columns, negative_infinity) }
289
+ h[0][0] = 0
290
+ gap_cost = lambda { |length| length.zero? ? 0 : open_score + (length - 1) * extend_score }
291
+ unless local
292
+ 1.upto(rows - 1) do |row|
293
+ h[row][0] = gap_cost.call(row)
294
+ vertical[row][0] = h[row][0]
295
+ end
296
+ 1.upto(columns - 1) do |column|
297
+ h[0][column] = gap_cost.call(column)
298
+ horizontal[0][column] = h[0][column]
299
+ end
300
+ end
301
+
302
+ best_score = 0
303
+ best_row = 0
304
+ best_column = 0
305
+ 1.upto(rows - 1) do |row|
306
+ 1.upto(columns - 1) do |column|
307
+ vertical[row][column] = [
308
+ h[row - 1][column] + open_score,
309
+ vertical[row - 1][column] + extend_score
310
+ ].max
311
+ horizontal[row][column] = [
312
+ h[row][column - 1] + open_score,
313
+ horizontal[row][column - 1] + extend_score
314
+ ].max
315
+ diagonal = h[row - 1][column - 1] +
316
+ substitution_matrix.matrix[q[row - 1]][t[column - 1]]
317
+ cell = [diagonal, vertical[row][column], horizontal[row][column]].max
318
+ cell = [0, cell].max if local
319
+ h[row][column] = cell
320
+ if local && cell > best_score
321
+ best_score = cell
322
+ best_row = row
323
+ best_column = column
324
+ end
325
+ end
326
+ end
327
+ unless local
328
+ best_score = h[rows - 1][columns - 1]
329
+ best_row = rows - 1
330
+ best_column = columns - 1
331
+ end
332
+ return best_score unless traceback
333
+
334
+ row = best_row
335
+ column = best_column
336
+ query_end = row
337
+ target_end = column
338
+ operations = []
339
+ state = :h
340
+ while row.positive? || column.positive?
341
+ break if local && state == :h && h[row][column] <= 0
342
+
343
+ if state == :h
344
+ if row.positive? && column.positive?
345
+ diagonal = h[row - 1][column - 1] +
346
+ substitution_matrix.matrix[q[row - 1]][t[column - 1]]
347
+ if h[row][column] == diagonal
348
+ operations.unshift(q[row - 1] == t[column - 1] ? "=" : "X")
349
+ row -= 1
350
+ column -= 1
351
+ next
352
+ end
353
+ end
354
+ if row.positive? && h[row][column] == vertical[row][column]
355
+ state = :vertical
356
+ next
357
+ end
358
+ if column.positive? && h[row][column] == horizontal[row][column]
359
+ state = :horizontal
360
+ next
361
+ end
362
+ break
363
+ elsif state == :vertical
364
+ operations.unshift("D")
365
+ continuing = row > 1 &&
366
+ vertical[row][column] == vertical[row - 1][column] + extend_score
367
+ row -= 1
368
+ state = continuing ? :vertical : :h
369
+ else
370
+ operations.unshift("I")
371
+ continuing = column > 1 &&
372
+ horizontal[row][column] == horizontal[row][column - 1] + extend_score
373
+ column -= 1
374
+ state = continuing ? :horizontal : :h
375
+ end
376
+ end
377
+ operation_string = operations.join
378
+ cigar = operations.chunk_while { |left, right| left == right }
379
+ .map { |run| "#{run.length}#{run[0]}" }.join
380
+ AlignmentPath.new(
381
+ score: best_score, query_start: row, query_end: query_end,
382
+ target_start: column, target_end: target_end,
383
+ operations: operation_string, cigar: cigar,
384
+ matches: operations.count("="), mismatches: operations.count("X"),
385
+ insertions: operations.count("I"), deletions: operations.count("D"),
386
+ aligned_length: operations.length
387
+ )
388
+ end
389
+ end
390
+
391
+ MATRIX_NAMES = (
392
+ [30, 35, 40, 45, 50, 55, 60, 62, 65, 70, 75, 80, 85, 90, 100]
393
+ .map { |level| "blosum#{level}" } +
394
+ (10..500).step(10).map { |level| "pam#{level}" } +
395
+ %w[nuc44 dna_match ascii_text]
396
+ ).freeze
397
+
398
+ class << self
399
+ MATRIX_NAMES.each do |matrix_name|
400
+ define_method(matrix_name) { matrix_catalog_load(matrix_name) }
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrideAlign
4
+ VERSION = "0.6.0"
5
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stride_align/version"
4
+ require "stride_align/_generic"
5
+
6
+ module StrideAlign
7
+ NATIVE_MODULES = {
8
+ "generic" => "Generic",
9
+ "sse41" => "Sse41",
10
+ "avx2" => "Avx2",
11
+ "avx512bwvl" => "Avx512bwvl",
12
+ "neon" => "Neon",
13
+ "sve" => "Sve",
14
+ "sve2" => "Sve2",
15
+ "lsx" => "Lsx",
16
+ "lasx" => "Lasx",
17
+ "vsx" => "Vsx",
18
+ "rvv" => "Rvv"
19
+ }.freeze
20
+
21
+ @primary_backend = Native::Generic
22
+ @available_backend_names = @primary_backend.backend_candidates
23
+ .map { |name| name.freeze }
24
+ .freeze
25
+ requested_backend = ENV.fetch("STRIDE_ALIGN_RUBY_BACKEND", "").strip
26
+
27
+ if !requested_backend.empty? && !@available_backend_names.include?(requested_backend)
28
+ raise LoadError,
29
+ "requested stride-align backend is unavailable or CPU-incompatible: " \
30
+ "#{requested_backend}; compatible packaged backends: " \
31
+ "#{@available_backend_names.join(', ')}"
32
+ end
33
+
34
+ dispatch_order = requested_backend.empty? ? @available_backend_names : [requested_backend]
35
+ @backend_name = "generic"
36
+ @native_backend = @primary_backend
37
+
38
+ dispatch_order.each do |candidate|
39
+ break if candidate == "generic"
40
+
41
+ begin
42
+ require "stride_align/_#{candidate}"
43
+ @backend_name = candidate
44
+ @native_backend = Native.const_get(NATIVE_MODULES.fetch(candidate), false)
45
+ break
46
+ rescue LoadError
47
+ raise if !requested_backend.empty?
48
+ end
49
+ end
50
+
51
+ class << self
52
+ def __native_backend
53
+ @native_backend
54
+ end
55
+
56
+ def __primary_backend
57
+ @primary_backend
58
+ end
59
+
60
+ def __available_backend_names
61
+ @available_backend_names
62
+ end
63
+
64
+ def __data_directory(kind)
65
+ installed = File.expand_path("../data/#{kind}", __dir__)
66
+ return installed if File.directory?(installed)
67
+
68
+ repository_relative = case kind
69
+ when "keyboard_data"
70
+ "../../../src/stride_align/matrices/keyboard_data"
71
+ else
72
+ "../../../src/stride_align/#{kind}"
73
+ end
74
+ File.expand_path(repository_relative, __dir__)
75
+ end
76
+ end
77
+ end
78
+
79
+ require_relative "stride_align/backend"
80
+ require_relative "stride_align/core"
81
+ require_relative "stride_align/matrices"
82
+ require_relative "stride_align/alignment_path"
83
+ require_relative "stride_align/batch"
84
+ require_relative "stride_align/algorithms"
85
+ require_relative "stride_align/keyboard"
86
+
87
+ StrideAlign.__send__(:load_bundled_keyboards)