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,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrideAlign
4
+ class << self
5
+ private
6
+
7
+ def utf8_string(value, name)
8
+ return nil if value.nil?
9
+ raise ArgumentError, "#{name} must be a String or nil" unless value.is_a?(String)
10
+
11
+ value.encode(Encoding::UTF_8)
12
+ end
13
+
14
+ def pairwise(query, target)
15
+ query_is_array = query.is_a?(Array)
16
+ target_is_array = target.is_a?(Array)
17
+ queries = query_is_array ? query : [query]
18
+ targets = target_is_array ? target : [target]
19
+
20
+ output_size = if queries.length == targets.length
21
+ queries.length
22
+ elsif queries.length == 1
23
+ targets.length
24
+ elsif targets.length == 1
25
+ queries.length
26
+ else
27
+ raise ArgumentError,
28
+ "query and target must have equal lengths, or one must have length one"
29
+ end
30
+
31
+ output = Array.new(output_size) do |index|
32
+ left = queries.length == 1 ? queries[0] : queries[index]
33
+ right = targets.length == 1 ? targets[0] : targets[index]
34
+ yield(utf8_string(left, "query"), utf8_string(right, "target"))
35
+ end
36
+ query_is_array || target_is_array ? output : output[0]
37
+ end
38
+
39
+ def unary(input)
40
+ is_array = input.is_a?(Array)
41
+ values = is_array ? input : [input]
42
+ output = values.map { |value| yield(utf8_string(value, "input")) }
43
+ is_array ? output : output[0]
44
+ end
45
+
46
+ def scalar_query(query)
47
+ return nil if query.nil?
48
+ raise ArgumentError, "query must be one String or nil" unless query.is_a?(String)
49
+
50
+ query.encode(Encoding::UTF_8)
51
+ end
52
+
53
+ def target_array(targets)
54
+ raise ArgumentError, "targets must be an Array" unless targets.is_a?(Array)
55
+
56
+ targets.map { |target| utf8_string(target, "target") }
57
+ end
58
+
59
+ def non_negative_integer(value, name)
60
+ unless value.is_a?(Numeric) && value.finite? && value >= 0 && value == value.to_i
61
+ raise ArgumentError, "#{name} must be one non-negative integer"
62
+ end
63
+ value.to_i
64
+ end
65
+
66
+ def finite_number(value, name)
67
+ unless value.is_a?(Numeric) && value.finite?
68
+ raise ArgumentError, "#{name} must be one finite number"
69
+ end
70
+ value
71
+ end
72
+
73
+ public
74
+
75
+ def stride_levenshtein(query, target)
76
+ pairwise(query, target) { |left, right| __native_backend.levenshtein(left, right) }
77
+ end
78
+
79
+ def stride_levenshtein_similarity(query, target)
80
+ pairwise(query, target) do |left, right|
81
+ __native_backend.levenshtein_similarity(left, right)
82
+ end
83
+ end
84
+
85
+ def stride_osa(query, target)
86
+ pairwise(query, target) { |left, right| __native_backend.osa(left, right) }
87
+ end
88
+
89
+ def stride_osa_similarity(query, target)
90
+ pairwise(query, target) { |left, right| __native_backend.osa_similarity(left, right) }
91
+ end
92
+
93
+ def stride_true_damerau_levenshtein(query, target)
94
+ pairwise(query, target) do |left, right|
95
+ __native_backend.true_damerau_levenshtein(left, right)
96
+ end
97
+ end
98
+
99
+ def stride_true_damerau_levenshtein_similarity(query, target)
100
+ pairwise(query, target) do |left, right|
101
+ __native_backend.true_damerau_levenshtein_similarity(left, right)
102
+ end
103
+ end
104
+
105
+ def stride_indel(query, target)
106
+ pairwise(query, target) { |left, right| __native_backend.indel(left, right) }
107
+ end
108
+
109
+ def stride_indel_similarity(query, target)
110
+ pairwise(query, target) do |left, right|
111
+ __native_backend.indel_similarity(left, right)
112
+ end
113
+ end
114
+
115
+ def stride_hamming(query, target)
116
+ pairwise(query, target) { |left, right| __native_backend.hamming(left, right) }
117
+ end
118
+
119
+ def stride_hamming_similarity(query, target)
120
+ pairwise(query, target) do |left, right|
121
+ __native_backend.hamming_similarity(left, right)
122
+ end
123
+ end
124
+
125
+ def stride_jaro(query, target)
126
+ pairwise(query, target) { |left, right| __native_backend.jaro(left, right) }
127
+ end
128
+
129
+ def stride_jaro_winkler(query, target, prefix_weight: 0.1,
130
+ prefix_threshold: 0.7, prefix_cap: 4)
131
+ pairwise(query, target) do |left, right|
132
+ __native_backend.jaro_winkler(
133
+ left, right, prefix_weight, prefix_threshold, prefix_cap
134
+ )
135
+ end
136
+ end
137
+
138
+ def stride_smith_waterman(query, target, match_score: 2,
139
+ mismatch_score: -1, gap_score: -1)
140
+ pairwise(query, target) do |left, right|
141
+ __native_backend.smith_waterman(
142
+ left, right, match_score, mismatch_score, gap_score
143
+ )
144
+ end
145
+ end
146
+
147
+ def stride_needleman_wunsch(query, target, match_score: 2,
148
+ mismatch_score: -1, gap_score: -1)
149
+ pairwise(query, target) do |left, right|
150
+ __native_backend.needleman_wunsch(
151
+ left, right, match_score, mismatch_score, gap_score
152
+ )
153
+ end
154
+ end
155
+
156
+ def stride_smith_waterman_affine(query, target, match_score: 2,
157
+ mismatch_score: -1,
158
+ gap_open_score: -2,
159
+ gap_extend_score: -1)
160
+ pairwise(query, target) do |left, right|
161
+ __native_backend.smith_waterman_affine(
162
+ left, right, match_score, mismatch_score,
163
+ gap_open_score, gap_extend_score
164
+ )
165
+ end
166
+ end
167
+
168
+ def stride_needleman_wunsch_affine(query, target, match_score: 2,
169
+ mismatch_score: -1,
170
+ gap_open_score: -2,
171
+ gap_extend_score: -1)
172
+ pairwise(query, target) do |left, right|
173
+ __native_backend.needleman_wunsch_affine(
174
+ left, right, match_score, mismatch_score,
175
+ gap_open_score, gap_extend_score
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrideAlign
4
+ module Keyboard
5
+ ASCII_CODEPOINTS = (0..127).to_a.freeze
6
+ ASCII_ALPHABET = ASCII_CODEPOINTS.map(&:chr).join.freeze
7
+ ASCII_WILDCARD = 127.chr.freeze
8
+
9
+ class << self
10
+ def from_npy(*args, **kwargs)
11
+ StrideAlign.keyboard_from_npy(*args, **kwargs)
12
+ end
13
+
14
+ def from_confusion_counts(*args, **kwargs)
15
+ StrideAlign.keyboard_from_confusion_counts(*args, **kwargs)
16
+ end
17
+
18
+ def available
19
+ StrideAlign.keyboard_available
20
+ end
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def keyboard
26
+ Keyboard
27
+ end
28
+
29
+ def keyboard_from_npy(path, name: File.basename(path, File.extname(path)),
30
+ alphabet: Keyboard::ASCII_ALPHABET,
31
+ wildcard: Keyboard::ASCII_WILDCARD,
32
+ transpose: false, gap_score: -1,
33
+ gap_open: nil, gap_extend: nil)
34
+ raise ArgumentError, "path must be one filename" unless path.is_a?(String)
35
+ grid = keyboard_grid_for_alphabet(read_npy(path), alphabet)
36
+ grid = grid.transpose if transpose
37
+ SubstitutionMatrix.new(
38
+ name: name, alphabet: alphabet, matrix: grid,
39
+ gap_score: gap_score, wildcard: wildcard,
40
+ gap_open: gap_open, gap_extend: gap_extend
41
+ )
42
+ end
43
+
44
+ def keyboard_from_confusion_counts(counts,
45
+ alphabet: Keyboard::ASCII_ALPHABET,
46
+ name: "KEYBOARD", scale: 2,
47
+ match_margin: 4, floor: nil,
48
+ wildcard: alphabet.each_char.to_a.last,
49
+ gap_score: -1, gap_open: nil,
50
+ gap_extend: nil)
51
+ grid_counts = keyboard_counts_grid(counts, alphabet)
52
+ size = grid_counts.length
53
+ total = grid_counts.sum { |row| row.sum }
54
+ grid = if total.zero?
55
+ Array.new(size) do |row|
56
+ Array.new(size) { |column| row == column ? 1 : -1 }
57
+ end
58
+ else
59
+ row_totals = grid_counts.map(&:sum)
60
+ column_totals = grid_counts.transpose.map(&:sum)
61
+ scores = Array.new(size) { Array.new(size) }
62
+ finite_scores = []
63
+ size.times do |row|
64
+ size.times do |column|
65
+ denominator = row_totals[row] * column_totals[column]
66
+ value = if grid_counts[row][column].zero? || denominator.zero?
67
+ -Float::INFINITY
68
+ else
69
+ scale * Math.log2(grid_counts[row][column] * total / denominator.to_f)
70
+ end
71
+ scores[row][column] = value
72
+ finite_scores << value if value.finite?
73
+ end
74
+ end
75
+ resolved_floor = floor.nil? ? (finite_scores.min&.floor || -1) : floor
76
+ scores.map! do |row|
77
+ row.map do |value|
78
+ value = resolved_floor unless value.finite?
79
+ [[value.round, -128].max, 127].min
80
+ end
81
+ end
82
+ size.times { |index| scores[index][index] = -128 }
83
+ best_substitution = scores.flatten.max
84
+ diagonal = [[best_substitution, 0].max + match_margin, 127].min
85
+ size.times { |index| scores[index][index] = diagonal }
86
+ scores
87
+ end
88
+ SubstitutionMatrix.new(
89
+ name: name, alphabet: alphabet, matrix: grid,
90
+ gap_score: gap_score, wildcard: wildcard,
91
+ gap_open: gap_open, gap_extend: gap_extend
92
+ )
93
+ end
94
+
95
+ def keyboard_available
96
+ directory = __data_directory("keyboard_data")
97
+ return [] unless File.directory?(directory)
98
+ Dir[File.join(directory, "*.npy")].map do |path|
99
+ File.basename(path, ".npy")
100
+ end.sort
101
+ end
102
+
103
+ private
104
+
105
+ def read_npy(path)
106
+ data = File.binread(path)
107
+ raise ArgumentError, "not a NumPy .npy file" unless data.byteslice(0, 6) == "\x93NUMPY".b
108
+ major = data.getbyte(6)
109
+ header_size, offset = case major
110
+ when 1 then [data.byteslice(8, 2).unpack1("v"), 10]
111
+ when 2, 3 then [data.byteslice(8, 4).unpack1("V"), 12]
112
+ else
113
+ raise ArgumentError, "unsupported NumPy .npy version"
114
+ end
115
+ header = data.byteslice(offset, header_size)
116
+ unless header.match?(/["']descr["']\s*:\s*["']\|i1["']/)
117
+ raise ArgumentError, "keyboard .npy matrix must use signed int8 values"
118
+ end
119
+ if header.match?(/["']fortran_order["']\s*:\s*True/)
120
+ raise ArgumentError, "Fortran-order .npy matrices are not supported"
121
+ end
122
+ shape_text = header[/["']shape["']\s*:\s*\(([^)]*)\)/, 1]
123
+ raise ArgumentError, "could not read shape from keyboard .npy file" unless shape_text
124
+ dimensions = shape_text.split(",").map(&:strip).reject(&:empty?).map do |value|
125
+ Integer(value, 10)
126
+ end
127
+ unless dimensions.length == 2
128
+ raise ArgumentError, "keyboard .npy value must be a two-dimensional matrix"
129
+ end
130
+ payload = data.byteslice(offset + header_size, dimensions.reduce(:*))
131
+ unless payload && payload.bytesize == dimensions.reduce(:*)
132
+ raise ArgumentError, "truncated keyboard .npy payload"
133
+ end
134
+ values = payload.unpack("c*")
135
+ Array.new(dimensions[0]) do |row|
136
+ values.slice(row * dimensions[1], dimensions[1])
137
+ end
138
+ end
139
+
140
+ def keyboard_grid_for_alphabet(grid, alphabet)
141
+ size = alphabet.each_char.count
142
+ if grid.length == 128 && grid.all? { |row| row.length == 128 } && size == 127
143
+ grid = grid.drop(1).map { |row| row.drop(1) }
144
+ end
145
+ unless grid.length == size && grid.all? { |row| row.length == size }
146
+ raise ArgumentError, "keyboard matrix shape does not match the alphabet size"
147
+ end
148
+ grid
149
+ end
150
+
151
+ def keyboard_counts_grid(counts, alphabet)
152
+ symbols = alphabet.each_char.to_a
153
+ size = symbols.length
154
+ if counts.is_a?(Array) && counts.length == size &&
155
+ counts.all? { |row| row.is_a?(Array) && row.length == size }
156
+ return counts.map do |row|
157
+ row.map do |value|
158
+ unless value.is_a?(Numeric) && value.finite? && value >= 0
159
+ raise ArgumentError, "counts must be finite non-negative numbers"
160
+ end
161
+ value.to_f
162
+ end
163
+ end
164
+ end
165
+ unless counts.is_a?(Array) && counts.all? { |row| row.is_a?(Hash) }
166
+ raise ArgumentError,
167
+ "counts must be a numeric matrix or an Array of typed/intended/count Hashes"
168
+ end
169
+ indices = symbols.each_with_index.to_h
170
+ grid = Array.new(size) { Array.new(size, 0.0) }
171
+ counts.each do |row|
172
+ typed = row[:typed] || row["typed"]
173
+ intended = row[:intended] || row["intended"]
174
+ count = row.key?(:count) ? row[:count] : row["count"]
175
+ unless typed.is_a?(String) && intended.is_a?(String) &&
176
+ count.is_a?(Numeric) && count.finite? && count >= 0
177
+ raise ArgumentError, "confusion-count rows are invalid"
178
+ end
179
+ next unless indices.key?(typed) && indices.key?(intended)
180
+ grid[indices[typed]][indices[intended]] = count.to_f
181
+ end
182
+ grid
183
+ end
184
+
185
+ def load_bundled_keyboards
186
+ directory = __data_directory("keyboard_data")
187
+ keyboard_available.each do |name|
188
+ matrix = keyboard_from_npy(
189
+ File.join(directory, "#{name}.npy"), name: "keyboard:#{name}"
190
+ )
191
+ install_keyboard(name, matrix)
192
+ end
193
+ nil
194
+ end
195
+
196
+ def install_keyboard(name, matrix)
197
+ Keyboard.define_singleton_method(name) { matrix }
198
+ end
199
+ end
200
+ end