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
data/lib/rcas/factor.rb
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module RCAS
|
|
5
|
+
# Exact polynomial factorization over ZZ and QQ.
|
|
6
|
+
#
|
|
7
|
+
# Univariate: numeric content, squarefree decomposition (Yun), then for
|
|
8
|
+
# every squarefree part Zassenhaus' algorithm - factor modulo a small
|
|
9
|
+
# prime with Cantor-Zassenhaus, Hensel-lift to a modulus beyond the
|
|
10
|
+
# Mignotte bound, recombine the modular factors: by van Hoeij's lattice
|
|
11
|
+
# (van_hoeij.rb) when there are many, by subsets otherwise, and by
|
|
12
|
+
# subsets whenever the lattice leaves the question open.
|
|
13
|
+
#
|
|
14
|
+
# Multivariate: pull out the content in the main variable (factored
|
|
15
|
+
# recursively), squarefree-decompose, then reduce each part to one
|
|
16
|
+
# variable by Kronecker substitution, factor that, and recombine subsets
|
|
17
|
+
# of the univariate factors that map back to true divisors.
|
|
18
|
+
#
|
|
19
|
+
# Sources (keys: MANUAL.md, Sources): Yun [Yun76], [vzGG13, §14.6];
|
|
20
|
+
# Zassenhaus [Zas69], [GCL92, ch. 8], [vzGG13, ch. 15]; Cantor-Zassenhaus
|
|
21
|
+
# [CZ81]; Hensel lifting [GCL92, ch. 6]; Mignotte bound [Mig74]; Kronecker
|
|
22
|
+
# substitution [Knu98, §4.6.2], [vzGG13, §8.4]; x**n -+ 1 by cyclotomic
|
|
23
|
+
# polynomials [Lan02, VI §3].
|
|
24
|
+
module Factor
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# => Factorization
|
|
28
|
+
def factor(poly)
|
|
29
|
+
ring = poly.ring
|
|
30
|
+
unless ring.base == ZZ || ring.base == QQ
|
|
31
|
+
raise RCAS::Unsupported, "factorization is implemented over ZZ and QQ, not #{ring.base}"
|
|
32
|
+
end
|
|
33
|
+
poly.terms.each_value do |c|
|
|
34
|
+
next if c.is_a?(Num) && (c.value.is_a?(Integer) || c.value.is_a?(Rational))
|
|
35
|
+
raise DomainError, "factorization needs numeric coefficients, got #{poly}"
|
|
36
|
+
end
|
|
37
|
+
return Factorization.new(ring, Num.new(0), []) if poly.zero?
|
|
38
|
+
|
|
39
|
+
zz = ring.base == ZZ ? ring : ZZ[*ring.vars]
|
|
40
|
+
scaled = poly.clear_denominators
|
|
41
|
+
unit = Rational(poly.leading_coefficient.value, scaled.leading_coefficient.value)
|
|
42
|
+
f = scaled.to_ring(zz)
|
|
43
|
+
|
|
44
|
+
content = f.content
|
|
45
|
+
content = -content if Scalar.negative?(f.leading_coefficient)
|
|
46
|
+
f = f.primitive_part
|
|
47
|
+
unit *= content
|
|
48
|
+
|
|
49
|
+
factors = factor_primitive(f).map { |g, m| [g.to_ring(ring), m] }
|
|
50
|
+
Factorization.new(ring, Num.new(Simplify.normalize_number(unit)), factors)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# x**n - 1 is the product of the cyclotomic polynomials Phi_d for the
|
|
54
|
+
# divisors d of n, and x**n + 1 of those for the d dividing 2n but not
|
|
55
|
+
# n - each irreducible over QQ [Lan02, VI §3]. Zassenhaus took nine
|
|
56
|
+
# seconds over x**210 - 1, which is 48 factors read off a list (third
|
|
57
|
+
# review, section 5). nil for anything else.
|
|
58
|
+
def cyclotomic_split(f)
|
|
59
|
+
ring = f.ring
|
|
60
|
+
return nil unless ring.vars.size == 1 && f.terms.size == 2
|
|
61
|
+
exponents = f.terms.keys.map(&:first).sort
|
|
62
|
+
return nil unless exponents.first.zero? && exponents.last >= 2
|
|
63
|
+
n = exponents.last
|
|
64
|
+
lead = f.terms[[n]]
|
|
65
|
+
tail = f.terms[[0]]
|
|
66
|
+
return nil unless lead.is_a?(Num) && lead.value == 1 && tail.is_a?(Num) && [1, -1].include?(tail.value)
|
|
67
|
+
divisors = tail.value == -1 ? (1..n).select { |d| (n % d).zero? } : (1..2 * n).select { |d| ((2 * n) % d).zero? && (n % d).nonzero? }
|
|
68
|
+
divisors.map do |d|
|
|
69
|
+
coefficients = Poly.send(:cyclotomic_coeffs, d) # Integers, lowest degree first
|
|
70
|
+
[Polynomial.new(ring, coefficients.each_with_index.reject { |c, _| c.zero? }.to_h { |c, i| [[i], Num.new(c)] }), 1]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ---- multivariate driver (f primitive over ZZ, positive lc) -----------
|
|
75
|
+
|
|
76
|
+
def factor_primitive(f)
|
|
77
|
+
ring = f.ring
|
|
78
|
+
return [] if f.constant?
|
|
79
|
+
binomial = cyclotomic_split(f)
|
|
80
|
+
return binomial if binomial
|
|
81
|
+
|
|
82
|
+
out = []
|
|
83
|
+
# monomial factors
|
|
84
|
+
ring.vars.each do |v|
|
|
85
|
+
k = f.terms.keys.map { |e| e[ring.index(v)] }.min
|
|
86
|
+
next unless k.positive?
|
|
87
|
+
out << [ring.call(Var.new(v)), k]
|
|
88
|
+
f = f.exact_div(ring.call(Var.new(v)**k))
|
|
89
|
+
end
|
|
90
|
+
return out if f.constant?
|
|
91
|
+
|
|
92
|
+
if ring.univariate?
|
|
93
|
+
out.concat(factor_univariate(f))
|
|
94
|
+
else
|
|
95
|
+
x = ring.vars.find { |v| f.degree(v).positive? }
|
|
96
|
+
content = PolyGCD.content_in(f, x)
|
|
97
|
+
out.concat(factor_primitive(content.primitive_part))
|
|
98
|
+
f = f.exact_div(content)
|
|
99
|
+
squarefree(f, x).each do |part, mult|
|
|
100
|
+
kronecker_factors(part, x).each { |g| out << [g, mult] }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
out.sort_by { |g, m| [g.degree, g.to_s, m] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Yun's squarefree decomposition with respect to x. f primitive in x.
|
|
107
|
+
#
|
|
108
|
+
# Most polynomials are squarefree, and saying so does not need the
|
|
109
|
+
# multivariate gcd, whose primitive remainder sequence swells: a quartic
|
|
110
|
+
# in four variables ran for minutes (third review, section 5). With the
|
|
111
|
+
# other variables set to integers where the leading coefficient does not
|
|
112
|
+
# vanish, a common factor of f and f' would survive into the images; a
|
|
113
|
+
# constant gcd of the images proves there is none.
|
|
114
|
+
def squarefree(f, x)
|
|
115
|
+
return [[f, 1]] if evidently_squarefree?(f, x)
|
|
116
|
+
out = []
|
|
117
|
+
fp = f.derivative(x)
|
|
118
|
+
g = PolyGCD.gcd_zz(f, fp)
|
|
119
|
+
c = f.exact_div(g)
|
|
120
|
+
d = fp.exact_div(g) - c.derivative(x)
|
|
121
|
+
i = 1
|
|
122
|
+
while c.degree(x).positive?
|
|
123
|
+
p = PolyGCD.gcd_zz(c, d)
|
|
124
|
+
c = c.exact_div(p)
|
|
125
|
+
d = d.exact_div(p) - c.derivative(x)
|
|
126
|
+
out << [p.primitive_part, i] if p.degree(x).positive?
|
|
127
|
+
i += 1
|
|
128
|
+
end
|
|
129
|
+
out
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def evidently_squarefree?(f, x)
|
|
133
|
+
others = f.ring.vars - [x]
|
|
134
|
+
degree = f.degree(x)
|
|
135
|
+
lead = f.leading_coefficient_in(x).to_expr
|
|
136
|
+
expr = f.to_expr
|
|
137
|
+
random = Random.new(20260923)
|
|
138
|
+
3.times do
|
|
139
|
+
point = others.to_h { |v| [Var.new(v), Num.new(random.rand(2..97))] }
|
|
140
|
+
next if Scalar.zero?(lead.subs(point).simplify)
|
|
141
|
+
image = Solve.polynomial_coefficients(expr.subs(point).expand, Var.new(x)) or return false
|
|
142
|
+
values = image.map { |c| c.is_a?(Num) && c.value.is_a?(Integer) ? c.value : (return false) }
|
|
143
|
+
dense = Dense.trim(values)
|
|
144
|
+
return false unless Dense.deg(dense) == degree
|
|
145
|
+
return Dense.deg(Dense.gcd(dense, Dense.derivative(dense))).zero?
|
|
146
|
+
end
|
|
147
|
+
false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def factor_univariate(f)
|
|
151
|
+
Dense.squarefree(Dense.from_poly(f)).flat_map do |part, mult|
|
|
152
|
+
Zassenhaus.factor(part).map { |g| [Dense.to_poly(g, f.ring), mult] }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# All irreducible factors of a dense integer polynomial, repeated by
|
|
157
|
+
# multiplicity, ignoring content and sign. Includes x for monomial factors.
|
|
158
|
+
def univariate_irreducibles(dense)
|
|
159
|
+
f = Dense.primitive(dense)
|
|
160
|
+
k = f.index { |c| !c.zero? }
|
|
161
|
+
out = Array.new(k, [0, 1])
|
|
162
|
+
f = f.drop(k)
|
|
163
|
+
Dense.squarefree(f).each { |part, mult| out.concat(Zassenhaus.factor(part) * mult) }
|
|
164
|
+
out
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Factor a squarefree multivariate polynomial through Kronecker substitution.
|
|
168
|
+
def kronecker_factors(h, _x)
|
|
169
|
+
ring = h.ring
|
|
170
|
+
n = ring.vars.size
|
|
171
|
+
d = ring.vars.map { |v| h.degree(v) }.max + 1
|
|
172
|
+
weights = (0...n).map { |i| d**i }
|
|
173
|
+
|
|
174
|
+
image = []
|
|
175
|
+
h.terms.each do |e, c|
|
|
176
|
+
idx = e.zip(weights).sum { |k, w| k * w }
|
|
177
|
+
image[idx] = (image[idx] || 0) + c.value
|
|
178
|
+
end
|
|
179
|
+
image = Dense.trim(image.map { |c| c || 0 })
|
|
180
|
+
|
|
181
|
+
remaining = univariate_irreducibles(image)
|
|
182
|
+
current = h
|
|
183
|
+
out = []
|
|
184
|
+
size = 1
|
|
185
|
+
while size <= remaining.size && current.degree.positive?
|
|
186
|
+
found = false
|
|
187
|
+
remaining.each_index.to_a.combination(size).each do |indices|
|
|
188
|
+
prod = indices.reduce([1]) { |acc, i| Dense.mul(acc, remaining[i]) }
|
|
189
|
+
cand = kronecker_inverse(prod, ring, d)
|
|
190
|
+
next if cand.nil?
|
|
191
|
+
begin
|
|
192
|
+
q = current.exact_div(cand)
|
|
193
|
+
rescue DomainError
|
|
194
|
+
next
|
|
195
|
+
end
|
|
196
|
+
out << cand.primitive_part
|
|
197
|
+
current = q
|
|
198
|
+
remaining = remaining.reject.with_index { |_, i| indices.include?(i) }
|
|
199
|
+
found = true
|
|
200
|
+
break
|
|
201
|
+
end
|
|
202
|
+
size += 1 unless found
|
|
203
|
+
end
|
|
204
|
+
out << current.primitive_part if current.degree.positive?
|
|
205
|
+
out
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def kronecker_inverse(dense, ring, d)
|
|
209
|
+
n = ring.vars.size
|
|
210
|
+
terms = {}
|
|
211
|
+
dense.each_with_index do |c, idx|
|
|
212
|
+
next if c.zero?
|
|
213
|
+
return nil if idx >= d**n
|
|
214
|
+
e = Array.new(n) { |i| (idx / d**i) % d }
|
|
215
|
+
terms[e] = Num.new(c)
|
|
216
|
+
end
|
|
217
|
+
Polynomial.new(ring, terms)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# ---- dense integer polynomials: index = degree ------------------------
|
|
221
|
+
|
|
222
|
+
module Dense
|
|
223
|
+
module_function
|
|
224
|
+
|
|
225
|
+
def trim(a)
|
|
226
|
+
a = a.dup
|
|
227
|
+
a.pop while !a.empty? && a.last.zero?
|
|
228
|
+
a
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def deg(a) = a.size - 1
|
|
232
|
+
|
|
233
|
+
def from_poly(p)
|
|
234
|
+
raise ArgumentError, "univariate polynomial expected" unless p.ring.univariate?
|
|
235
|
+
trim(Array.new(p.degree + 1) { |k| p.coeff(k).value })
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def to_poly(a, ring)
|
|
239
|
+
Polynomial.new(ring, a.each_with_index.to_h { |c, k| [[k], Num.new(c)] })
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def add(a, b) = trim(Array.new([a.size, b.size].max) { |i| (a[i] || 0) + (b[i] || 0) })
|
|
243
|
+
def sub(a, b) = trim(Array.new([a.size, b.size].max) { |i| (a[i] || 0) - (b[i] || 0) })
|
|
244
|
+
def scale(a, c) = c.zero? ? [] : a.map { |x| x * c }
|
|
245
|
+
|
|
246
|
+
def mul(a, b)
|
|
247
|
+
return [] if a.empty? || b.empty?
|
|
248
|
+
out = Array.new(a.size + b.size - 1, 0)
|
|
249
|
+
a.each_with_index { |x, i| next if x.zero?; b.each_with_index { |y, j| out[i + j] += x * y } }
|
|
250
|
+
trim(out)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def derivative(a) = trim(a.each_with_index.map { |c, i| c * i }.drop(1))
|
|
254
|
+
|
|
255
|
+
# Exact division over ZZ; nil when b does not divide a.
|
|
256
|
+
def div_exact(a, b)
|
|
257
|
+
return [] if a.empty?
|
|
258
|
+
return nil if b.empty? || deg(a) < deg(b)
|
|
259
|
+
r = a.dup
|
|
260
|
+
q = Array.new(deg(a) - deg(b) + 1, 0)
|
|
261
|
+
lb = b.last
|
|
262
|
+
(deg(a) - deg(b)).downto(0) do |k|
|
|
263
|
+
c = r[k + deg(b)]
|
|
264
|
+
return nil unless (c % lb).zero?
|
|
265
|
+
c /= lb
|
|
266
|
+
q[k] = c
|
|
267
|
+
b.each_with_index { |y, j| r[k + j] -= c * y }
|
|
268
|
+
end
|
|
269
|
+
trim(r).empty? ? trim(q) : nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def content(a) = a.reduce(0) { |g, c| g.gcd(c) }
|
|
273
|
+
|
|
274
|
+
# Yun's algorithm: [[squarefree part, multiplicity], ...] for a
|
|
275
|
+
# primitive polynomial with positive leading coefficient.
|
|
276
|
+
def squarefree(f)
|
|
277
|
+
out = []
|
|
278
|
+
fp = derivative(f)
|
|
279
|
+
g = gcd(f, fp)
|
|
280
|
+
c = div_exact(f, g)
|
|
281
|
+
d = sub(div_exact(fp, g), derivative(c))
|
|
282
|
+
i = 1
|
|
283
|
+
while deg(c) >= 1
|
|
284
|
+
p = gcd(c, d)
|
|
285
|
+
c = div_exact(c, p)
|
|
286
|
+
d = sub(div_exact(d, p), derivative(c))
|
|
287
|
+
out << [p, i] if deg(p) >= 1
|
|
288
|
+
i += 1
|
|
289
|
+
end
|
|
290
|
+
out
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def primitive(a)
|
|
294
|
+
return [] if a.empty?
|
|
295
|
+
c = content(a)
|
|
296
|
+
c = -c if a.last.negative?
|
|
297
|
+
a.map { |x| x / c }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def prem(a, b)
|
|
301
|
+
r = a
|
|
302
|
+
lb = b.last
|
|
303
|
+
while !r.empty? && deg(r) >= deg(b)
|
|
304
|
+
shift = deg(r) - deg(b)
|
|
305
|
+
r = sub(scale(r, lb), scale(Array.new(shift, 0) + b, r.last))
|
|
306
|
+
end
|
|
307
|
+
r
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def gcd(a, b)
|
|
311
|
+
return primitive(b) if a.empty?
|
|
312
|
+
return primitive(a) if b.empty?
|
|
313
|
+
c = content(a).gcd(content(b))
|
|
314
|
+
a = primitive(a)
|
|
315
|
+
b = primitive(b)
|
|
316
|
+
until b.empty?
|
|
317
|
+
a, b = b, primitive(prem(a, b))
|
|
318
|
+
end
|
|
319
|
+
scale(primitive(a), c)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ---- modulo a prime -------------------------------------------------
|
|
323
|
+
|
|
324
|
+
def mod(a, p) = trim(a.map { |c| c % p })
|
|
325
|
+
|
|
326
|
+
def sym_mod(a, m)
|
|
327
|
+
trim(a.map { |c| c %= m; c > m / 2 ? c - m : c })
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def mul_mod(a, b, p) = mod(mul(a, b), p)
|
|
331
|
+
|
|
332
|
+
# Divide by the leading coefficient (or +lc+) modulo m; m need not be prime.
|
|
333
|
+
def monic_mod(a, m, lc = a.last)
|
|
334
|
+
return a if a.empty?
|
|
335
|
+
mod(scale(a, inv_mod(lc, m)), m)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def inv_mod(a, m)
|
|
339
|
+
g, x = a % m, 1
|
|
340
|
+
r, y = m, 0
|
|
341
|
+
until r.zero?
|
|
342
|
+
q = g / r
|
|
343
|
+
g, r = r, g - q * r
|
|
344
|
+
x, y = y, x - q * y
|
|
345
|
+
end
|
|
346
|
+
raise ZeroDivisionError, "#{a} is not invertible mod #{m}" unless g == 1
|
|
347
|
+
x % m
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def divmod_mod(a, b, p)
|
|
351
|
+
raise ZeroDivisionError if b.empty?
|
|
352
|
+
r = mod(a, p)
|
|
353
|
+
q = Array.new([deg(a) - deg(b) + 1, 0].max, 0)
|
|
354
|
+
inv = b.last.pow(p - 2, p)
|
|
355
|
+
while !r.empty? && deg(r) >= deg(b)
|
|
356
|
+
shift = deg(r) - deg(b)
|
|
357
|
+
c = (r.last * inv) % p
|
|
358
|
+
q[shift] = c
|
|
359
|
+
r = mod(sub(r, scale(Array.new(shift, 0) + b, c)), p)
|
|
360
|
+
end
|
|
361
|
+
[trim(q), r]
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def rem_mod(a, b, p) = divmod_mod(a, b, p).last
|
|
365
|
+
|
|
366
|
+
def gcd_mod(a, b, p)
|
|
367
|
+
a = mod(a, p)
|
|
368
|
+
b = mod(b, p)
|
|
369
|
+
a, b = b, rem_mod(a, b, p) until b.empty?
|
|
370
|
+
monic_mod(a, p)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# a**e mod (f, p)
|
|
374
|
+
def powmod(a, e, f, p)
|
|
375
|
+
result = [1]
|
|
376
|
+
base = rem_mod(a, f, p)
|
|
377
|
+
while e.positive?
|
|
378
|
+
result = rem_mod(mul(result, base), f, p) if e.odd?
|
|
379
|
+
base = rem_mod(mul(base, base), f, p)
|
|
380
|
+
e >>= 1
|
|
381
|
+
end
|
|
382
|
+
result
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# s, t with s*a + t*b = 1 mod p (a, b coprime mod p)
|
|
386
|
+
def bezout_mod(a, b, p)
|
|
387
|
+
r0, r1 = mod(a, p), mod(b, p)
|
|
388
|
+
s0, s1 = [1], []
|
|
389
|
+
t0, t1 = [], [1]
|
|
390
|
+
until r1.empty?
|
|
391
|
+
q, r = divmod_mod(r0, r1, p)
|
|
392
|
+
r0, r1 = r1, r
|
|
393
|
+
s0, s1 = s1, mod(sub(s0, mul(q, s1)), p)
|
|
394
|
+
t0, t1 = t1, mod(sub(t0, mul(q, t1)), p)
|
|
395
|
+
end
|
|
396
|
+
inv = r0.last.pow(p - 2, p)
|
|
397
|
+
[mod(scale(s0, inv), p), mod(scale(t0, inv), p)]
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# ---- Zassenhaus ---------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
module Zassenhaus
|
|
404
|
+
module_function
|
|
405
|
+
|
|
406
|
+
# f: primitive squarefree dense integer polynomial, positive leading
|
|
407
|
+
# coefficient. Returns the irreducible factors (primitive, positive lc).
|
|
408
|
+
def factor(f)
|
|
409
|
+
n = Dense.deg(f)
|
|
410
|
+
return [] if n < 1
|
|
411
|
+
return [f] if n == 1
|
|
412
|
+
|
|
413
|
+
lc = f.last
|
|
414
|
+
chosen = nil
|
|
415
|
+
tried = 0
|
|
416
|
+
NumberTheory.each_prime do |p|
|
|
417
|
+
next if p == 2 || (lc % p).zero?
|
|
418
|
+
fp = Dense.mod(f, p)
|
|
419
|
+
next unless Dense.deg(Dense.gcd_mod(fp, Dense.derivative(fp), p)).zero?
|
|
420
|
+
modular = cantor_zassenhaus(fp, p)
|
|
421
|
+
chosen = [p, modular] if chosen.nil? || modular.size < chosen[1].size
|
|
422
|
+
tried += 1
|
|
423
|
+
break if modular.size == 1 || tried >= 4
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
p, modular = chosen
|
|
427
|
+
return [f] if modular.size == 1
|
|
428
|
+
|
|
429
|
+
# Mignotte-style bound on the coefficients of lc * (any factor of f).
|
|
430
|
+
bound = 2**n * Integer.sqrt(f.sum { |c| c * c }) * lc.abs + 1
|
|
431
|
+
m = p
|
|
432
|
+
k = 1
|
|
433
|
+
while m <= 2 * bound
|
|
434
|
+
m *= p
|
|
435
|
+
k += 1
|
|
436
|
+
end
|
|
437
|
+
if Factor.recombination == :van_hoeij && (found = VanHoeij.recombine(f, modular, p, k))
|
|
438
|
+
return found
|
|
439
|
+
end
|
|
440
|
+
lifted = hensel_lift_leading(f, modular, p, k)
|
|
441
|
+
recombine(f, lifted, m, budget: Factor.subset_budget) do |rest, local|
|
|
442
|
+
# the subsets grew too many: the lattice takes the cofactor and its
|
|
443
|
+
# local factors, lifted already (lc(rest) times their product is
|
|
444
|
+
# rest modulo p**k, since every factor taken out had a leading
|
|
445
|
+
# coefficient prime to p)
|
|
446
|
+
VanHoeij.recombine(rest, local.map { |g| Dense.mod(g, p) }, p, k, lifted: local)
|
|
447
|
+
end.sort_by { |g| [Dense.deg(g), g] }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Distinct-degree then equal-degree factorization of a monic squarefree
|
|
451
|
+
# polynomial modulo an odd prime.
|
|
452
|
+
def cantor_zassenhaus(f, p)
|
|
453
|
+
f = Dense.monic_mod(f, p)
|
|
454
|
+
factors = []
|
|
455
|
+
h = [0, 1] # x
|
|
456
|
+
d = 0
|
|
457
|
+
while Dense.deg(f) >= 2 * (d + 1)
|
|
458
|
+
d += 1
|
|
459
|
+
h = Dense.powmod(h, p, f, p)
|
|
460
|
+
g = Dense.gcd_mod(Dense.sub(h, [0, 1]), f, p)
|
|
461
|
+
next if Dense.deg(g) < 1
|
|
462
|
+
factors.concat(equal_degree(g, d, p))
|
|
463
|
+
f = Dense.divmod_mod(f, g, p).first
|
|
464
|
+
h = Dense.rem_mod(h, f, p)
|
|
465
|
+
end
|
|
466
|
+
factors << f if Dense.deg(f) >= 1
|
|
467
|
+
factors
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def equal_degree(g, d, p, rng = Random.new(42))
|
|
471
|
+
return [g] if Dense.deg(g) == d
|
|
472
|
+
loop do
|
|
473
|
+
a = Dense.trim(Array.new(Dense.deg(g)) { rng.rand(p) })
|
|
474
|
+
next if Dense.deg(a) < 1
|
|
475
|
+
b = Dense.sub(Dense.powmod(a, (p**d - 1) / 2, g, p), [1])
|
|
476
|
+
c = Dense.gcd_mod(b, g, p)
|
|
477
|
+
next unless Dense.deg(c).between?(1, Dense.deg(g) - 1)
|
|
478
|
+
return equal_degree(c, d, p, rng) + equal_degree(Dense.divmod_mod(g, c, p).first, d, p, rng)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Lift f = lc * g1 * ... * gr (mod p), g_i monic, to (mod p**k). The
|
|
483
|
+
# first factor absorbs the leading coefficient during lifting and is
|
|
484
|
+
# made monic again afterwards; the returned factors are all monic mod p**k.
|
|
485
|
+
def hensel_lift_leading(f, factors, p, k)
|
|
486
|
+
m = p**k
|
|
487
|
+
lc = f.last
|
|
488
|
+
return [Dense.monic_mod(f, m, lc)] if factors.size == 1
|
|
489
|
+
g = Dense.mod(Dense.scale(factors.first, lc), p)
|
|
490
|
+
g = g + Array.new(Dense.deg(factors.first) - Dense.deg(g), 0) if Dense.deg(g) < Dense.deg(factors.first)
|
|
491
|
+
g[-1] = lc
|
|
492
|
+
h = factors.drop(1).reduce([1]) { |acc, x| Dense.mul_mod(acc, x, p) }
|
|
493
|
+
g, h = lift_pair(f, g, h, p, k)
|
|
494
|
+
[Dense.mod(Dense.scale(g, Dense.inv_mod(lc, m)), m)] + hensel_lift(h, factors.drop(1), p, k)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Lift f = g1 * g2 * ... * gr (mod p), all monic, to (mod p**k).
|
|
498
|
+
def hensel_lift(f, factors, p, k)
|
|
499
|
+
return [Dense.sym_mod(f, p**k)] if factors.size == 1
|
|
500
|
+
g = factors.first
|
|
501
|
+
h = factors.drop(1).reduce([1]) { |acc, x| Dense.mul_mod(acc, x, p) }
|
|
502
|
+
g, h = lift_pair(f, g, h, p, k)
|
|
503
|
+
[g] + hensel_lift(h, factors.drop(1), p, k)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Linear Hensel lifting of f = g*h (mod p) to (mod p**k). h is monic and
|
|
507
|
+
# lc(g) == lc(f) exactly, which both updates preserve, so f - g*h always
|
|
508
|
+
# has degree below deg(f).
|
|
509
|
+
def lift_pair(f, g, h, p, k)
|
|
510
|
+
s, t = Dense.bezout_mod(g, h, p)
|
|
511
|
+
m = p
|
|
512
|
+
(k - 1).times do
|
|
513
|
+
e = Dense.trim(Dense.sub(f, Dense.mul(g, h)).map { |c| (c / m) % p })
|
|
514
|
+
dg = Dense.rem_mod(Dense.mul(t, e), g, p)
|
|
515
|
+
dh = Dense.rem_mod(Dense.mul(s, e), h, p)
|
|
516
|
+
g = Dense.add(g, Dense.scale(dg, m))
|
|
517
|
+
h = Dense.add(h, Dense.scale(dh, m))
|
|
518
|
+
m *= p
|
|
519
|
+
end
|
|
520
|
+
[Dense.sym_mod(g, m), Dense.sym_mod(h, m)]
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Try products of the monic lifted factors, scaled by the current
|
|
524
|
+
# leading coefficient and made primitive, as divisors of f. With a
|
|
525
|
+
# budget, once the subsets of the next size would number more than
|
|
526
|
+
# that, the cofactor and the local factors left are offered to the
|
|
527
|
+
# block (van Hoeij's lattice); its factors end the search, and nil
|
|
528
|
+
# lets the subsets go on without a limit.
|
|
529
|
+
def recombine(f, modular, m, budget: nil)
|
|
530
|
+
result = []
|
|
531
|
+
remaining = modular
|
|
532
|
+
current = f
|
|
533
|
+
size = 1
|
|
534
|
+
while 2 * size <= remaining.size
|
|
535
|
+
if budget && block_given? && remaining.size.to_i.then { |r| (1..size).reduce(1) { |c, i| c * (r - i + 1) / i } } > budget
|
|
536
|
+
handed = yield(current, remaining)
|
|
537
|
+
return result + handed if handed
|
|
538
|
+
budget = nil
|
|
539
|
+
end
|
|
540
|
+
found = false
|
|
541
|
+
lc = current.last
|
|
542
|
+
remaining.each_index.to_a.combination(size).each do |indices|
|
|
543
|
+
# cheap filter on the trailing coefficient before the full product
|
|
544
|
+
tail = indices.reduce(lc) { |acc, i| acc * remaining[i][0] % m }
|
|
545
|
+
tail = tail > m / 2 ? tail - m : tail
|
|
546
|
+
next if tail.zero? || (!current[0].zero? && ((lc * current[0]) % tail).nonzero?)
|
|
547
|
+
|
|
548
|
+
prod = indices.reduce([lc]) { |acc, i| Dense.mul_mod(acc, remaining[i], m) }
|
|
549
|
+
cand = Dense.primitive(Dense.sym_mod(prod, m))
|
|
550
|
+
q = Dense.div_exact(current, cand)
|
|
551
|
+
next if q.nil?
|
|
552
|
+
result << cand
|
|
553
|
+
current = q
|
|
554
|
+
remaining = remaining.reject.with_index { |_, i| indices.include?(i) }
|
|
555
|
+
found = true
|
|
556
|
+
break
|
|
557
|
+
end
|
|
558
|
+
size += 1 unless found
|
|
559
|
+
end
|
|
560
|
+
result << Dense.primitive(current) if Dense.deg(current) >= 1
|
|
561
|
+
result
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# Result of Polynomial#factor: unit * product of factor**multiplicity.
|
|
567
|
+
class Factorization
|
|
568
|
+
include Enumerable
|
|
569
|
+
|
|
570
|
+
attr_reader :ring, :unit, :factors
|
|
571
|
+
|
|
572
|
+
def initialize(ring, unit, factors)
|
|
573
|
+
@ring = ring
|
|
574
|
+
@unit = unit
|
|
575
|
+
@factors = factors.freeze
|
|
576
|
+
freeze
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def each(&block) = factors.each(&block)
|
|
580
|
+
def size = factors.size
|
|
581
|
+
def [](i) = factors[i]
|
|
582
|
+
def irreducible? = factors.size == 1 && factors.first.last == 1 && Scalar.one?(unit)
|
|
583
|
+
|
|
584
|
+
# Multiply back out.
|
|
585
|
+
def expand
|
|
586
|
+
factors.reduce(ring.call(unit)) { |acc, (g, m)| acc * g**m }
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def to_expr
|
|
590
|
+
product = factors.map { |g, m| Simplify.power_node(g.to_expr, m) }
|
|
591
|
+
return unit if product.empty?
|
|
592
|
+
body = product.reduce { |acc, f| Mul.new(acc, f) }
|
|
593
|
+
case unit.value
|
|
594
|
+
when 1 then body
|
|
595
|
+
when -1 then Neg.new(body)
|
|
596
|
+
else Mul.new(unit, body)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def to_s = to_expr.to_s
|
|
601
|
+
alias inspect to_s
|
|
602
|
+
|
|
603
|
+
def ==(other) = other.is_a?(Factorization) && other.unit == unit && other.factors == factors
|
|
604
|
+
end
|
|
605
|
+
end
|