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.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- 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
|