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,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