rcas 0.2.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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # The product of two matrices, three ways.
5
+ #
6
+ # * A matrix of Integers and Rationals is multiplied on the bare Ruby
7
+ # numbers: each row of the left factor and each column of the right one
8
+ # is scaled to Integers by the lcm of its denominators, the product is
9
+ # taken in Integers, and each entry is divided once at the end. The
10
+ # entries are wrapped in Num only then - through Scalar, every one of the
11
+ # n**3 products and sums built a node, and that was twenty times the
12
+ # arithmetic.
13
+ # * The schoolbook product: n**3 multiplications.
14
+ # * Strassen's: two 2x2 block matrices multiplied with seven products
15
+ # instead of eight [Str69], in Winograd's form with fifteen additions
16
+ # instead of eighteen [Win71], applied to the blocks recursively. That
17
+ # is n**log2(7) = n**2.807 multiplications. An odd dimension gets a row
18
+ # or a column of zeros for one level, and a block with a dimension at
19
+ # most `cutoff` is multiplied the schoolbook way.
20
+ #
21
+ # The exponent has come down much further since - below 2.3714 [ADVXXZ25],
22
+ # by the laser method of Coppersmith and Winograd [CW90] - but those
23
+ # algorithms win only for matrices far beyond any that exist ("galactic"),
24
+ # and nothing below Strassen's exponent is used in practice.
25
+ #
26
+ # `MatrixMultiply.algorithm` (thread-local, like Factor.recombination) is
27
+ # :auto, :strassen or :schoolbook. :auto takes the schoolbook product for
28
+ # symbolic entries - Strassen trades a product for additions, and with
29
+ # symbolic entries its products are of sums that must be expanded again,
30
+ # which is dearer - and Strassen for Integer/Rational matrices whose
31
+ # smallest dimension reaches AUTO_MIN (AUTO_MIN_LARGE for entries past a
32
+ # machine word). :strassen runs it on any entries, which is there to be compared.
33
+ module MatrixMultiply
34
+ ALGORITHMS = %i[auto strassen schoolbook].freeze
35
+
36
+ # Measured on random square Integer matrices (24 Sept 2026, Ruby 3.3).
37
+ # With one-digit entries Strassen is level with the schoolbook product
38
+ # at n = 128 and 12% faster at 256, best with leaves of 48; with entries
39
+ # of thirty digits the products outweigh the additions, and it is 15%
40
+ # faster from n = 64 and 34% at 256, best with leaves of 16. Past
41
+ # LARGE_BITS a product is no longer one machine word.
42
+ AUTO_MIN = 192
43
+ CUTOFF = 48
44
+ AUTO_MIN_LARGE = 64
45
+ CUTOFF_LARGE = 16
46
+ LARGE_BITS = 62
47
+
48
+ module_function
49
+
50
+ def algorithm = Thread.current[:rcas_matrix_multiplication] || :auto
51
+
52
+ # MatrixMultiply.with_algorithm(:strassen) { a * b }; nil keeps the algorithm
53
+ # in force, so a caller that passes its own default does not reset it.
54
+ def with_algorithm(name)
55
+ return yield if name.nil?
56
+ raise ArgumentError, "multiplication algorithm must be one of #{ALGORITHMS.join(', ')}" unless ALGORITHMS.include?(name)
57
+ saved = Thread.current[:rcas_matrix_multiplication]
58
+ Thread.current[:rcas_matrix_multiplication] = name
59
+ yield
60
+ ensure
61
+ Thread.current[:rcas_matrix_multiplication] = saved unless name.nil?
62
+ end
63
+
64
+ # Rows of entries (Expressions or Ruby numbers) of a*b, for a m x k and
65
+ # b k x n (columns = n says it when k is 0); the entries come back as Integer, Rational or Expression.
66
+ def product(a, b, algorithm = self.algorithm, columns: b.first&.size || 0)
67
+ raise ArgumentError, "multiplication algorithm must be one of #{ALGORITHMS.join(', ')}" unless ALGORITHMS.include?(algorithm)
68
+ return Array.new(a.size) { Array.new(columns, 0) } if a.empty? || b.empty? || columns.zero?
69
+ x = rational_values(a)
70
+ y = x && rational_values(b)
71
+ return rational_product(x, y, algorithm) if y
72
+ rows = a.map { |r| r.map { |e| Scalar.lift(e) } }
73
+ cols = b.map { |r| r.map { |e| Scalar.lift(e) } }
74
+ algorithm == :strassen ? strassen(rows, cols, SCALAR, 1) : SCALAR.leaf(rows, cols)
75
+ end
76
+
77
+ # The bare numbers of a matrix of Integers and Rationals, or nil.
78
+ def rational_values(rows)
79
+ rows.map do |r|
80
+ r.map do |e|
81
+ v = e.is_a?(Num) ? e.value : e
82
+ return nil unless v.is_a?(Integer) || v.is_a?(Rational)
83
+ v
84
+ end
85
+ end
86
+ end
87
+
88
+ # Rows scaled by their denominators on the left, columns on the right:
89
+ # the Integer product divided by the two scales is the Rational one.
90
+ def rational_product(a, b, algorithm)
91
+ left = a.map { |r| r.map(&:denominator).reduce(1, :lcm) }
92
+ right = b.transpose.map { |c| c.map(&:denominator).reduce(1, :lcm) }
93
+ x = left.all?(1) ? a : a.each_with_index.map { |r, i| r.map { |v| (v * left[i]).to_i } }
94
+ y = right.all?(1) ? b : b.map { |r| r.each_with_index.map { |v, j| (v * right[j]).to_i } }
95
+ c = strassen?(x, y, algorithm) ? strassen(x, y, INTEGER, cutoff_for(x, y)) : INTEGER.leaf(x, y)
96
+ return c if left.all?(1) && right.all?(1)
97
+ c.each_with_index.map { |r, i| r.each_with_index.map { |v, j| Rational(v, left[i] * right[j]) } }
98
+ end
99
+
100
+ def strassen?(a, b, algorithm)
101
+ return true if algorithm == :strassen
102
+ algorithm == :auto && [a.size, b.size, b.first.size].min >= (large?(a, b) ? AUTO_MIN_LARGE : AUTO_MIN)
103
+ end
104
+
105
+ def cutoff_for(a, b) = large?(a, b) ? CUTOFF_LARGE : CUTOFF
106
+
107
+ def large?(a, b) = [a, b].any? { |m| m.any? { |r| r.any? { |v| v.abs.bit_length > LARGE_BITS } } }
108
+
109
+ # Winograd's form of Strassen's algorithm [Win71] on blocks of rows,
110
+ # with `ops` doing the arithmetic of whole blocks.
111
+ def strassen(a, b, ops, cutoff)
112
+ m = a.size
113
+ k = b.size
114
+ n = b.first.size
115
+ return ops.leaf(a, b) if [m, k, n].min <= cutoff || [m, k, n].min < 2
116
+ if m.odd? || k.odd? || n.odd?
117
+ c = strassen(pad(a, m.odd?, k.odd?, ops.zero), pad(b, k.odd?, n.odd?, ops.zero), ops, cutoff)
118
+ return c.first(m).map { |r| r.first(n) }
119
+ end
120
+ a11, a12, a21, a22 = quarters(a)
121
+ b11, b12, b21, b22 = quarters(b)
122
+ s1 = ops.add(a21, a22)
123
+ s2 = ops.sub(s1, a11)
124
+ s3 = ops.sub(a11, a21)
125
+ s4 = ops.sub(a12, s2)
126
+ t1 = ops.sub(b12, b11)
127
+ t2 = ops.sub(b22, t1)
128
+ t3 = ops.sub(b22, b12)
129
+ t4 = ops.sub(t2, b21)
130
+ m1 = strassen(a11, b11, ops, cutoff)
131
+ m2 = strassen(a12, b21, ops, cutoff)
132
+ m3 = strassen(s4, b22, ops, cutoff)
133
+ m4 = strassen(a22, t4, ops, cutoff)
134
+ m5 = strassen(s1, t1, ops, cutoff)
135
+ m6 = strassen(s2, t2, ops, cutoff)
136
+ m7 = strassen(s3, t3, ops, cutoff)
137
+ u2 = ops.add(m1, m6)
138
+ u3 = ops.add(u2, m7)
139
+ c11 = ops.add(m1, m2)
140
+ c12 = ops.add(ops.add(u2, m5), m3)
141
+ c21 = ops.sub(u3, m4)
142
+ c22 = ops.add(u3, m5)
143
+ c11.zip(c12).map { |l, r| l + r } + c21.zip(c22).map { |l, r| l + r }
144
+ end
145
+
146
+ def quarters(a)
147
+ h = a.size / 2
148
+ w = a.first.size / 2
149
+ top = a.first(h)
150
+ bottom = a.drop(h)
151
+ [top.map { |r| r.first(w) }, top.map { |r| r.drop(w) }, bottom.map { |r| r.first(w) }, bottom.map { |r| r.drop(w) }]
152
+ end
153
+
154
+ def pad(a, row, col, zero)
155
+ a = a.map { |r| r + [zero] } if col
156
+ a += [Array.new(a.first.size, zero)] if row
157
+ a
158
+ end
159
+
160
+ # Block arithmetic on bare Integers.
161
+ module IntegerBlocks
162
+ module_function
163
+
164
+ def zero = 0
165
+ def add(a, b) = a.each_with_index.map { |r, i| s = b[i]; r.each_with_index.map { |v, j| v + s[j] } }
166
+ def sub(a, b) = a.each_with_index.map { |r, i| s = b[i]; r.each_with_index.map { |v, j| v - s[j] } }
167
+
168
+ def leaf(a, b)
169
+ columns = b.transpose
170
+ a.map do |r|
171
+ l = r.size
172
+ columns.map do |c|
173
+ s = 0
174
+ k = 0
175
+ while k < l
176
+ s += r[k] * c[k]
177
+ k += 1
178
+ end
179
+ s
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ # Block arithmetic on Expressions, through Scalar.
186
+ module ScalarBlocks
187
+ module_function
188
+
189
+ def zero = Num.new(0)
190
+ def add(a, b) = a.zip(b).map { |r, s| r.zip(s).map { |x, y| Scalar.add(x, y) } }
191
+ def sub(a, b) = a.zip(b).map { |r, s| r.zip(s).map { |x, y| Scalar.sub(x, y) } }
192
+
193
+ def leaf(a, b)
194
+ columns = b.transpose
195
+ a.map { |r| columns.map { |c| r.zip(c).map { |x, y| Scalar.mul(x, y) }.reduce { |s, v| Scalar.add(s, v) } || Num.new(0) } }
196
+ end
197
+ end
198
+
199
+ INTEGER = IntegerBlocks
200
+ SCALAR = ScalarBlocks
201
+ end
202
+ end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Determinants, inverses and solutions of Integer and Rational matrices
5
+ # by computing modulo many primes and putting the answers together with
6
+ # the Chinese remainder theorem [vzGG13, §5.5].
7
+ #
8
+ # Elimination over QQ is exact too, but its entries grow as it goes: each
9
+ # step's numbers are quotients of minors of the matrix, and every one of
10
+ # the n**3 operations pays for their size and for a gcd. Modulo a prime p
11
+ # below 2**31 every number is one machine word. How many primes are
12
+ # needed is known beforehand: Hadamard's inequality [Had93] bounds
13
+ # |det A| by the product of the lengths of the rows (or of the columns),
14
+ # and the answer is the unique residue modulo the product of the primes
15
+ # that lies in (-M/2, M/2) once M exceeds twice the bound. That makes the
16
+ # answer a proof, not a guess that agreed for a few more primes.
17
+ #
18
+ # A system A*X = B (B the identity for the inverse) is solved the same
19
+ # way through Cramer's rule: det(A)*X is an integer matrix, the adjugate
20
+ # times B, whose entries are determinants of A with one column replaced
21
+ # by a column of B, and Hadamard's inequality bounds those as well. A
22
+ # prime that divides det(A) cannot give det(A)*X by inverting A modulo p;
23
+ # it still counts for the determinant, and there are only finitely many.
24
+ #
25
+ # Rational entries are scaled to Integers row by row first: the
26
+ # determinant is then divided by the scales, and the solutions of the
27
+ # scaled system (right-hand sides scaled with the rows) are those of the
28
+ # original one.
29
+ module Multimodular
30
+ # Residues stay below 2**31, so a product of two is below 2**62 and
31
+ # Ruby keeps it a machine integer.
32
+ PRIME_LIMIT = 2**31
33
+
34
+ # :auto takes the primes for every Integer and Rational matrix: they
35
+ # were faster at every size measured on 24 Sept 2026, 2x2 included
36
+ # (20 against 23 microseconds for det and inverse), five times at
37
+ # n = 100 (MANUAL.md, "Determinants modulo many primes").
38
+ ALGORITHMS = %i[auto multimodular elimination].freeze
39
+
40
+ @primes = []
41
+ @lock = Mutex.new
42
+
43
+ module_function
44
+
45
+ # The first k primes below PRIME_LIMIT, largest first.
46
+ def primes(k)
47
+ @lock.synchronize do
48
+ candidate = @primes.last || PRIME_LIMIT
49
+ while @primes.size < k
50
+ candidate -= 1
51
+ candidate -= 1 until NumberTheory.prime?(candidate)
52
+ @primes << candidate
53
+ end
54
+ @primes.first(k)
55
+ end
56
+ end
57
+
58
+ def prime(i) = primes(i + 1)[i]
59
+
60
+ # The bare numbers of a square matrix of Integers and Rationals, or nil.
61
+ def values(rows) = MatrixMultiply.rational_values(rows)
62
+
63
+ def use?(rows, algorithm, allowed = ALGORITHMS)
64
+ raise ArgumentError, "algorithm must be one of #{allowed.join(', ')}" unless allowed.include?(algorithm)
65
+ algorithm != :elimination && !rows.empty? && !values(rows).nil?
66
+ end
67
+
68
+ # A system has one algorithm more: Dixon's, for one right-hand side.
69
+ SOLVE_ALGORITHMS = %i[auto multimodular dixon elimination].freeze
70
+
71
+ # Measured on random systems (24 Sept 2026): Dixon and the primes are
72
+ # level at n = 24-30 and Dixon is ahead from 32 on, whatever the size of
73
+ # the entries - 1.2x at 32 with one digit, 2x with five, 3-6x at
74
+ # n = 100-150. Below it the one elimination and the reconstruction
75
+ # attempts cost what a few more primes would.
76
+ DIXON_MIN = 32
77
+
78
+ # Multimodular, Dixon or nil (elimination) for A*x = b.
79
+ def solver(rows, b, algorithm)
80
+ return nil unless use?(rows, algorithm, SOLVE_ALGORITHMS) && MatrixMultiply.rational_values([b])
81
+ return Dixon if algorithm == :dixon
82
+ return self if algorithm == :multimodular
83
+ rows.size >= DIXON_MIN ? Dixon : self
84
+ end
85
+
86
+ # det of a square matrix of Integers and Rationals.
87
+ def det(rows)
88
+ a, scales = integral(values(rows))
89
+ d = crt_solve(a, []).first
90
+ scales.all?(1) ? d : Rational(d, scales.reduce(1, :*))
91
+ end
92
+
93
+ # A**-1 as rows of Integers and Rationals, or nil when A is singular.
94
+ def inverse(rows)
95
+ a, scales = integral(values(rows))
96
+ n = a.size
97
+ identity = Array.new(n) { |i| Array.new(n) { |j| i == j ? scales[i] : 0 } }
98
+ d, x = crt_solve(a, identity)
99
+ return nil if d.zero?
100
+ x.map { |r| r.map { |v| Rational(v, d) } }.map { |r| r.map { |v| v.denominator == 1 ? v.numerator : v } }
101
+ end
102
+
103
+ # The solution of A*x = b for a square non-singular A, or nil.
104
+ def solve(rows, b)
105
+ a, scales = integral(values(rows))
106
+ column = b.map { |e| e.is_a?(Num) ? e.value : e }.each_with_index.map { |v, i| v * scales[i] }
107
+ common = column.map(&:denominator).reduce(1, :lcm)
108
+ d, x = crt_solve(a, column.map { |v| [(v * common).to_i] })
109
+ return nil if d.zero?
110
+ x.map { |r| Rational(r.first, d * common) }.map { |v| v.denominator == 1 ? v.numerator : v }
111
+ end
112
+
113
+ # Rows scaled to Integers by the lcm of their denominators, and the scales.
114
+ def integral(a)
115
+ scales = a.map { |r| r.map(&:denominator).reduce(1, :lcm) }
116
+ [a.each_with_index.map { |r, i| r.map { |v| (v * scales[i]).to_i } }, scales]
117
+ end
118
+
119
+ # [d, N] with A**-1*B = N/d, for an Integer matrix A and Integer columns
120
+ # B (rows of B, possibly none), by the Chinese remainder theorem; [0,
121
+ # nil] when A is singular. With no columns, d is det A.
122
+ #
123
+ # Two ways to stop. The bound: once the primes multiply past twice
124
+ # Hadamard's bound for det A (all primes) and for the Cramer numerators
125
+ # det(A)*A**-1*B (the primes not dividing det A), d = det A and N are
126
+ # proved. And early (`early:`): after 1, 2, 4, ... usable primes the
127
+ # residues of A**-1*B are read back as fractions with one common
128
+ # denominator (rational reconstruction [vzGG13, §5.10]), and if A*N = d*B
129
+ # holds exactly, that is the solution - A is invertible, since it was
130
+ # modulo a prime, so there is no other. The bounds are for the worst
131
+ # matrix; a solution much smaller than its bound, as for a matrix of
132
+ # determinant 1, is found after a fraction of the primes.
133
+ def crt_solve(a, b, early: true)
134
+ n = a.size
135
+ return [1, b] if n.zero?
136
+ det_bound = hadamard(a)
137
+ return [0, nil] if det_bound.zero?
138
+ width = b.first&.size || 0
139
+ numerator_bound = cramer_bound(a, b)
140
+ det = 0
141
+ det_modulus = 1
142
+ x = Array.new(n) { Array.new(width, 0) }
143
+ x_modulus = 1
144
+ usable = 0
145
+ attempt = 1
146
+ (0..).each do |i|
147
+ if det_modulus > 2 * det_bound
148
+ d = symmetric(det, det_modulus)
149
+ return [0, nil] if d.zero?
150
+ return [d, x.map { |r| r.map { |v| symmetric(v * d % x_modulus, x_modulus) } }] if width.zero? || x_modulus > 2 * numerator_bound
151
+ end
152
+ p = prime(i)
153
+ d_p, x_p = eliminate(a, b, p)
154
+ if det_modulus <= 2 * det_bound
155
+ det = garner(det, det_modulus, d_p, p)
156
+ det_modulus *= p
157
+ end
158
+ next if x_p.nil? || width.zero?
159
+ x = x.each_with_index.map { |r, k| r.each_with_index.map { |v, j| garner(v, x_modulus, x_p[k][j], p) } }
160
+ x_modulus *= p
161
+ usable += 1
162
+ next unless early && usable == attempt
163
+ attempt *= 2
164
+ found = verified(a, b, x, x_modulus) and return found
165
+ end
166
+ end
167
+
168
+ # [d, N] with A*N = d*B exactly, read off the residues X of A**-1*B
169
+ # modulo m, or nil.
170
+ def verified(a, b, x, m)
171
+ found = reconstruct(x, m) or return nil
172
+ d, numerators = found
173
+ product = MatrixMultiply::INTEGER.leaf(a, numerators)
174
+ product.each_with_index.all? { |row, i| row.each_with_index.all? { |v, j| v == d * b[i][j] } } ? [d, numerators] : nil
175
+ end
176
+
177
+ # Rational reconstruction of a matrix of residues modulo m with one
178
+ # common denominator: [d, N] with N/d congruent to the residues and
179
+ # |N|, d <= sqrt(m/2), or nil. The denominator is grown entry by entry
180
+ # - an entry that is already a small numerator over it costs one
181
+ # multiplication, and the extended Euclidean algorithm runs only on the
182
+ # others [vzGG13, §5.10].
183
+ def reconstruct(x, m)
184
+ bound = Integer.sqrt(m / 2)
185
+ d = 1
186
+ x.each do |row|
187
+ row.each do |v|
188
+ next if symmetric(v * d % m, m).abs <= bound
189
+ fraction = rational(v * d % m, m, bound) or return nil
190
+ d *= fraction.denominator
191
+ return nil if d > bound
192
+ end
193
+ end
194
+ [d, x.map { |row| row.map { |v| symmetric(v * d % m, m) } }]
195
+ end
196
+
197
+ # The fraction r/t = u modulo m with |r|, |t| <= bound, by the extended
198
+ # Euclidean algorithm stopped halfway, or nil.
199
+ def rational(u, m, bound)
200
+ r0, r1 = m, u
201
+ t0, t1 = 0, 1
202
+ while r1 > bound
203
+ q = r0 / r1
204
+ r0, r1 = r1, r0 - q * r1
205
+ t0, t1 = t1, t0 - q * t1
206
+ end
207
+ return nil if t1.zero? || t1.abs > bound || r1.gcd(t1) != 1
208
+ Rational(r1, t1)
209
+ end
210
+
211
+ # The residue modulo m*p that is v modulo m and r modulo p (Garner).
212
+ def garner(v, m, r, p)
213
+ t = (r - v) * m.pow(p - 2, p) % p
214
+ v + m * t
215
+ end
216
+
217
+ def symmetric(v, m) = v > m / 2 ? v - m : v
218
+
219
+ # |det A| <= the product of the row lengths, and of the column lengths;
220
+ # the smaller of the two, as an Integer that is not below the root.
221
+ def hadamard(a)
222
+ rows = a.map { |r| r.sum { |v| v * v } }
223
+ cols = a.transpose.map { |c| c.sum { |v| v * v } }
224
+ square = [rows.reduce(1, :*), cols.reduce(1, :*)].min
225
+ square.zero? ? 0 : Integer.sqrt(square) + 1
226
+ end
227
+
228
+ # det(A)*x_i for A*x = b is det(A) with column i replaced by b, and its
229
+ # Hadamard bound through the columns is |b| times the lengths of the
230
+ # other columns of A.
231
+ def cramer_bound(a, b)
232
+ return 0 if b.empty? || b.first.empty?
233
+ cols = a.transpose.map { |c| c.sum { |v| v * v } }
234
+ rhs = b.transpose.map { |c| c.sum { |v| v * v } }.max
235
+ Integer.sqrt(rhs * cols.reduce(1, :*) / cols.min) + 1
236
+ end
237
+
238
+ # det(A) mod p, and A**-1*B mod p unless p divides det(A):
239
+ # elimination to a triangle, then substitution backwards.
240
+ def eliminate(a, b, p)
241
+ n = a.size
242
+ width = b.first&.size || 0
243
+ rows = Array.new(n) { |i| (a[i] + (b[i] || [])).map { |v| v % p } }
244
+ total = n + width
245
+ d = 1
246
+ n.times do |c|
247
+ pivot = (c...n).find { |i| rows[i][c] != 0 }
248
+ return [0, nil] unless pivot
249
+ if pivot != c
250
+ rows[c], rows[pivot] = rows[pivot], rows[c]
251
+ d = p - d
252
+ end
253
+ top = rows[c]
254
+ d = d * top[c] % p
255
+ inverse = top[c].pow(p - 2, p)
256
+ ((c + 1)...n).each do |i|
257
+ row = rows[i]
258
+ f = row[c] * inverse % p
259
+ next if f.zero?
260
+ f = p - f
261
+ j = c
262
+ while j < total
263
+ row[j] = (row[j] + f * top[j]) % p
264
+ j += 1
265
+ end
266
+ end
267
+ end
268
+ return [d, nil] if width.zero?
269
+ x = Array.new(n)
270
+ (n - 1).downto(0) do |i|
271
+ row = rows[i]
272
+ inverse = row[i].pow(p - 2, p)
273
+ x[i] = Array.new(width) do |j|
274
+ s = row[n + j]
275
+ k = i + 1
276
+ while k < n
277
+ s = (s - row[k] * x[k][j]) % p
278
+ k += 1
279
+ end
280
+ s * inverse % p
281
+ end
282
+ end
283
+ [d, x]
284
+ end
285
+ end
286
+ end