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,481 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # The i-th root (real roots first, ascending; then complex by real and
5
+ # imaginary part) of an irreducible polynomial with rational coefficients.
6
+ class RootOf < Expression
7
+ attr_reader :poly, :index
8
+
9
+ # poly: a univariate Polynomial over QQ (or ZZ)
10
+ def initialize(poly, index)
11
+ @poly = poly
12
+ @index = index
13
+ @value = RootOf.numeric_roots(poly)[index]
14
+ freeze
15
+ end
16
+
17
+ attr_reader :value
18
+
19
+ def var = poly.ring.vars.first
20
+ def degree = poly.degree
21
+ def ==(other) = other.is_a?(RootOf) && other.poly == poly && other.index == index
22
+ alias eql? ==
23
+ def hash = [RootOf, poly.to_expr.simplify, index].hash
24
+ def to_sexp = [:root_of, poly.to_expr.to_sexp, index]
25
+
26
+ def self.numeric_roots(poly)
27
+ roots = Solve.numeric_roots(poly).map(&:value)
28
+ roots.sort_by { |z| z.is_a?(Complex) ? [1, z.real, z.imaginary] : [0, z, 0] }
29
+ end
30
+
31
+ def real? = !value.is_a?(Complex)
32
+ end
33
+
34
+ # QQ(alpha): numbers a0 + a1*alpha + ... + a_{n-1}*alpha**(n-1) with the
35
+ # minimal polynomial of alpha reducing higher powers.
36
+ #
37
+ # Sources (keys: MANUAL.md, Sources): arithmetic in QQ(alpha) and minimal
38
+ # polynomials through resultants [Loo83], [Coh93, §4.2]; factoring over
39
+ # QQ(alpha) by norms is Trager's method [Tra76], [Coh93, Algorithm 3.6.4].
40
+ class AlgebraicField < Domain
41
+ attr_reader :minpoly, :generator, :radicals
42
+
43
+ A = :_a
44
+
45
+ # generator: an Expression (2**(1/2), RootOf(...), 2**(1/2) + 3**(1/2))
46
+ # radicals: { [base, q] => polynomial in _a for base**(1/q) } used when
47
+ # converting expressions into the field.
48
+ def initialize(minpoly, generator, radicals = {})
49
+ @minpoly = minpoly
50
+ @generator = generator
51
+ @radicals = radicals
52
+ freeze
53
+ end
54
+
55
+ def ring = QQ[A]
56
+ def degree = minpoly.degree
57
+ def name = "QQ(#{generator})"
58
+ def field? = true
59
+
60
+ def element(poly) = AlgebraicNumber.new(self, poly % minpoly)
61
+ def zero = element(ring.zero)
62
+ def one = element(ring.one)
63
+ def alpha = element(ring.call(Var.new(A)))
64
+ def constant(v) = element(ring.call(v))
65
+
66
+ def include?(obj)
67
+ case obj
68
+ when AlgebraicNumber then obj.field == self
69
+ when Numeric then QQ.include?(obj)
70
+ when Expression then !Algebraic.convert(obj, self).nil?
71
+ else false
72
+ end
73
+ end
74
+
75
+ def subset?(other)
76
+ case other
77
+ when AlgebraicField then other == self
78
+ when NumberSet then other.rank >= (generator_real? ? 3 : 4)
79
+ else false
80
+ end
81
+ end
82
+
83
+ # Decided: a RootOf knows from Sturm's count, a radical from its
84
+ # radicand (an |Im| < 1e-12 test before: fourth review, 2.4).
85
+ def generator_real?
86
+ return generator.real? if generator.is_a?(RootOf)
87
+ Inequalities.real?(generator)
88
+ rescue NotImplementedError, RCAS::Unsupported
89
+ false
90
+ end
91
+
92
+ def join(other)
93
+ case other
94
+ when NumberSet then other.rank <= 2 ? self : other
95
+ when AlgebraicField then other == self ? self : (generator_real? && other.generator_real? ? RR : CC)
96
+ when PolynomialRing then PolynomialRing.new(join(other.base), other.vars)
97
+ when FractionField then FractionField.new(join(other.ring))
98
+ else raise DomainError, "no common domain for #{self} and #{other}"
99
+ end
100
+ end
101
+
102
+ def ==(other) = other.is_a?(AlgebraicField) && other.minpoly == minpoly && other.generator == generator
103
+ alias eql? ==
104
+ def hash = [AlgebraicField, minpoly.to_expr.simplify, generator].hash
105
+ end
106
+
107
+ class AlgebraicNumber
108
+ include Algebraic
109
+
110
+ attr_reader :field, :poly
111
+
112
+ # QQ(alpha) is where an algebraic number lives.
113
+ def domain = field
114
+
115
+ def initialize(field, poly)
116
+ @field = field
117
+ @poly = poly
118
+ freeze
119
+ end
120
+
121
+ def +(other) = field.element(poly + lift(other).poly)
122
+ def -(other) = field.element(poly - lift(other).poly)
123
+ def *(other) = field.element(poly * lift(other).poly)
124
+ def -@ = field.element(-poly)
125
+ def /(other) = self * lift(other).inverse
126
+
127
+ def **(n)
128
+ raise ArgumentError, "integer exponents only" unless n.is_a?(Integer)
129
+ return inverse**(-n) if n.negative?
130
+ result = field.one
131
+ base = self
132
+ while n.positive?
133
+ result *= base if n.odd?
134
+ base *= base
135
+ n >>= 1
136
+ end
137
+ result
138
+ end
139
+
140
+ def inverse
141
+ raise ZeroDivisionError, "division by zero in #{field}" if zero?
142
+ g, s, = poly.xgcd(field.minpoly)
143
+ raise ArgumentError, "#{field.minpoly} is not irreducible" unless g.constant?
144
+ field.element(s * Scalar.div(Num.new(1), g.constant_term))
145
+ end
146
+
147
+ def zero? = poly.zero?
148
+ def rational? = poly.constant?
149
+ def coerce(other) = [lift(other), self]
150
+ def ==(other) = other.is_a?(AlgebraicNumber) ? (self - other).zero? : (rational? && poly.constant_term == Expression.lift(other))
151
+ alias eql? ==
152
+ def hash = [AlgebraicNumber, poly.to_expr.simplify].hash
153
+
154
+ # Back to radicals: a0 + a1*alpha + ...
155
+ def to_expr
156
+ poly.to_expr.subs(Var.new(AlgebraicField::A) => field.generator).expand
157
+ end
158
+
159
+ def evalf = to_expr.evalf
160
+ def to_s = to_expr.to_s
161
+ alias inspect to_s
162
+ def to_latex(wrap: nil) = LaTeX.of(to_expr)
163
+
164
+ # Minimal polynomial of this element over QQ, in +var+.
165
+ def minpoly(var = :x)
166
+ Algebraic.minpoly_of(self, var)
167
+ end
168
+
169
+ private
170
+
171
+ def lift(other)
172
+ return other if other.is_a?(AlgebraicNumber) && other.field == field
173
+ raise ArgumentError, "different fields: #{field} and #{other.field}" if other.is_a?(AlgebraicNumber)
174
+ Algebraic.convert(Expression.lift(other), field) or raise DomainError, "#{other} is not in #{field}"
175
+ end
176
+ end
177
+
178
+ # Exact arithmetic with algebraic numbers: recognising the field an
179
+ # expression lives in, converting into it, minimal polynomials, and
180
+ # factorization over QQ(alpha) by Trager's method.
181
+ module Algebraic
182
+ module_function
183
+
184
+ # The exact value of a constant expression in radicals / RootOf, or nil
185
+ # when it involves anything else (pi, several incompatible radicals, ...).
186
+ def exact(expr)
187
+ expr = Expression.lift(expr)
188
+ return nil unless expr.variables.empty?
189
+ field = field_for(expr) or return nil
190
+ convert(expr, field)
191
+ rescue ZeroDivisionError, ArgumentError, DomainError
192
+ nil
193
+ end
194
+
195
+ def radical?(n) = n.is_a?(Pow) && n.base.is_a?(Num) && (n.base.value.is_a?(Integer) || n.base.value.is_a?(Rational)) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational)
196
+
197
+ # QQ(alpha) for the radicals / RootOf atoms of expr, or nil.
198
+ def field_for(expr)
199
+ radicals = {}
200
+ roots = []
201
+ expr.each_node do |n|
202
+ if radical?(n)
203
+ radicals[[n.base.value, n.exponent.value.denominator]] = true
204
+ elsif n.is_a?(Num) && n.value.is_a?(Complex)
205
+ radicals[[-1, 2]] = true
206
+ elsif n.is_a?(RootOf)
207
+ roots << n unless roots.include?(n)
208
+ end
209
+ end
210
+ generators = radicals.keys.size + roots.size
211
+ return nil if generators.zero? || generators > 2
212
+ return root_field(roots.first) if generators == 1 && roots.size == 1
213
+ return radical_field(*radicals.keys.first) if generators == 1
214
+ return nil unless roots.empty? && radicals.keys.all? { |_, q| q == 2 }
215
+ square_root_compositum(radicals.keys.map(&:first))
216
+ end
217
+
218
+ def adjoin(alpha)
219
+ alpha = Expression.lift(alpha).simplify
220
+ field_for(alpha) or raise ArgumentError, "can't build QQ(#{alpha})"
221
+ end
222
+
223
+ def root_field(root)
224
+ ring = QQ[AlgebraicField::A]
225
+ m = ring.call(root.poly.to_expr.subs(Var.new(root.var) => Var.new(AlgebraicField::A)))
226
+ AlgebraicField.new(m.monic, root)
227
+ end
228
+
229
+ # QQ(base**(1/q))
230
+ def radical_field(base, q)
231
+ ring = QQ[AlgebraicField::A]
232
+ a = Var.new(AlgebraicField::A)
233
+ candidate = ring.call(a**q - base)
234
+ generator = base == -1 && q == 2 ? I : Pow.new(Num.new(base), Num.new(Rational(1, q)))
235
+ value = generator.evalf
236
+ m = candidate.factor.factors.map(&:first).min_by { |g| g.to_expr.evalf(AlgebraicField::A => value).abs }
237
+ AlgebraicField.new(m.monic, generator, { [base, q] => ring.call(a) })
238
+ end
239
+
240
+ # QQ(sqrt(a), sqrt(b)) = QQ(gamma), gamma = sqrt(a) + sqrt(b):
241
+ # sqrt(a) = (gamma**2 + a - b) / (2 gamma)
242
+ def square_root_compositum(bases)
243
+ a, b = bases
244
+ ring = QQ[AlgebraicField::A]
245
+ g = Var.new(AlgebraicField::A)
246
+ m = ring.call(((g**2 - (a + b))**2 - 4 * a * b).expand)
247
+ field = AlgebraicField.new(m, (Pow.new(Num.new(a), Num.new(Rational(1, 2))) + Pow.new(Num.new(b), Num.new(Rational(1, 2)))).simplify)
248
+ gamma = field.alpha
249
+ half = gamma.inverse * Rational(1, 2)
250
+ sqrt_a = ((gamma * gamma) + Rational(a - b)) * half
251
+ sqrt_b = ((gamma * gamma) + Rational(b - a)) * half
252
+ AlgebraicField.new(m, field.generator, { [a, 2] => sqrt_a.poly, [b, 2] => sqrt_b.poly })
253
+ end
254
+
255
+ # Expression => AlgebraicNumber in +field+, or nil.
256
+ def convert(expr, field)
257
+ case expr
258
+ when Num
259
+ v = expr.value
260
+ return field.constant(v) if v.is_a?(Integer) || v.is_a?(Rational)
261
+ return nil unless v.is_a?(Complex) && [v.real, v.imaginary].all? { |c| c.is_a?(Integer) || c.is_a?(Rational) }
262
+ unit = field.radicals[[-1, 2]] or return nil
263
+ field.constant(v.real) + field.element(unit) * v.imaginary
264
+ when RootOf then field.generator == expr ? field.alpha : nil
265
+ when Pow
266
+ if radical?(expr)
267
+ base, e = expr.base.value, expr.exponent.value
268
+ root = field.radicals[[base, e.denominator]]
269
+ root ? field.element(root)**e.numerator : nil
270
+ elsif expr.exponent.is_a?(Num) && expr.exponent.integer?
271
+ inner = convert(expr.base, field) or return nil
272
+ inner**expr.exponent.value
273
+ end
274
+ when Add then binary(expr, field, :+)
275
+ when Sub then binary(expr, field, :-)
276
+ when Mul then binary(expr, field, :*)
277
+ when Div then binary(expr, field, :/)
278
+ when Neg then (inner = convert(expr.arg, field)) && -inner
279
+ else
280
+ folded = expr.simplify
281
+ folded == expr ? nil : convert(folded, field)
282
+ end
283
+ end
284
+
285
+ def binary(expr, field, op)
286
+ l = convert(expr.left, field) or return nil
287
+ r = convert(expr.right, field) or return nil
288
+ l.public_send(op, r)
289
+ end
290
+
291
+ # Minimal polynomial of an AlgebraicNumber (or exact constant expression).
292
+ def minpoly_of(number, var = :x)
293
+ if number.is_a?(Expression) || number.is_a?(Numeric)
294
+ expr = Expression.lift(number)
295
+ # a rational is algebraic of degree 1 (A10); a nested radical is
296
+ # algebraic too, and is taken apart by resultants rather than called
297
+ # "not an algebraic number"
298
+ folded = expr.simplify
299
+ if folded.is_a?(Num) && (folded.value.is_a?(Integer) || folded.value.is_a?(Rational))
300
+ return QQ[var].call(Var.new(var) - folded)
301
+ end
302
+ number = exact(expr)
303
+ return resultant_minpoly(expr, var) if number.nil?
304
+ end
305
+ raise ArgumentError, "not an algebraic number" if number.nil?
306
+ x = Var.new(var)
307
+ ring2 = QQ[AlgebraicField::A, var]
308
+ m = ring2.call(number.field.minpoly.to_expr)
309
+ g = ring2.call(x - number.poly.to_expr)
310
+ res = QQ[var].call(m.resultant(g, AlgebraicField::A).to_expr)
311
+ value = number.evalf
312
+ res.factor.factors.map(&:first).min_by { |f| f.to_expr.evalf(var => value).abs }.monic
313
+ end
314
+
315
+ # The minimal polynomial of a constant built from rationals, i, the
316
+ # four operations and rational powers, by resultants [CLO15, §3.6]: a
317
+ # polynomial for each part, combined - P(x) for u + v is
318
+ # res_y(P_u(y), P_v(x - y)) - and the factor that vanishes at the value
319
+ # kept. pi and e are transcendental and say so; anything else it cannot
320
+ # take apart is a refusal (RCAS::Unsupported), never "not algebraic".
321
+ def resultant_minpoly(expr, var)
322
+ x = Var.new(var)
323
+ poly = annihilator(expr, x)
324
+ ring = QQ[var]
325
+ value = expr.evalf
326
+ raise RCAS::Unsupported, "minpoly: #{expr} has no numeric value to choose a factor by" unless value.is_a?(Numeric)
327
+ ring.call(poly).factor.factors.map(&:first).min_by { |f| (f.to_expr.evalf(var => value)).abs }.monic
328
+ end
329
+
330
+ def annihilator(e, x)
331
+ y = Var.new(:"_y#{x.name}")
332
+ case e
333
+ when Num
334
+ v = e.value
335
+ return (x - Num.new(v)).expand if v.is_a?(Integer) || v.is_a?(Rational)
336
+ if v.is_a?(Complex) && [v.real, v.imaginary].all? { |c| c.is_a?(Integer) || c.is_a?(Rational) }
337
+ return ((x - Num.new(v.real))**2 + Num.new(v.imaginary)**2).expand
338
+ end
339
+ raise RCAS::Unsupported, "minpoly: #{e} is not exact"
340
+ when Const
341
+ raise ArgumentError, "not an algebraic number: #{e} is transcendental" if %i[pi e].include?(e.name) || e == E
342
+ raise RCAS::Unsupported, "minpoly: #{e}"
343
+ when Neg then annihilator(e.arg, x).subs(x => Neg.new(x)).expand
344
+ when Add, Sub, Mul, Div then combine(e, x, y)
345
+ when Pow then power_annihilator(e, x, y)
346
+ else
347
+ raise ArgumentError, "not an algebraic number: #{e} is transcendental" if e == E || (e.is_a?(Fn) && e.name == :exp && e.args.first == Num.new(1))
348
+ raise RCAS::Unsupported, "minpoly: #{e} is beyond the radicals rcas takes apart"
349
+ end
350
+ end
351
+
352
+ def combine(e, x, y)
353
+ pl = annihilator(e.left, x).subs(x => y)
354
+ pr = annihilator(e.right, x)
355
+ other =
356
+ case e
357
+ when Add then pr.subs(x => x - y)
358
+ when Sub then pr.subs(x => y - x)
359
+ when Mul then scaled(pr, x, x / y, y)
360
+ when Div then annihilator(e.left, x).subs(x => x * y)
361
+ end
362
+ pl = annihilator(e.right, x).subs(x => y) if e.is_a?(Div)
363
+ eliminate(pl, other.expand, y, x)
364
+ end
365
+
366
+ # y**deg * P(x/y), a polynomial again.
367
+ def scaled(p, x, replacement, y)
368
+ degree = Solve.polynomial_coefficients(p, x).size - 1
369
+ (p.subs(x => replacement) * y**degree).expand
370
+ end
371
+
372
+ def power_annihilator(e, x, y)
373
+ exponent = e.exponent
374
+ raise RCAS::Unsupported, "minpoly: #{e} has a non-rational exponent" unless exponent.is_a?(Num) && (exponent.value.is_a?(Integer) || exponent.value.is_a?(Rational))
375
+ r = Rational(exponent.value)
376
+ base = annihilator(e.base, x)
377
+ base = scaled(base, x, 1 / x, x) if r.negative? # the reciprocal
378
+ r = r.abs
379
+ rooted = base.subs(x => x**r.denominator).expand # b**(1/q): P(x**q)
380
+ return rooted if r.numerator == 1
381
+ eliminate(rooted.subs(x => y), (x - y**r.numerator).expand, y, x)
382
+ end
383
+
384
+ def eliminate(p, q, y, x)
385
+ ring = QQ[y.name, x.name]
386
+ ring.call(p).resultant(ring.call(q), y.name).to_expr.expand
387
+ end
388
+
389
+ # ---- factorization over QQ(alpha): Trager -------------------------------
390
+
391
+ def factor_over(poly, field)
392
+ raise ArgumentError, "univariate polynomials only" unless poly.ring.univariate?
393
+ xname = poly.ring.vars.first
394
+ x = Var.new(xname)
395
+ a = Var.new(AlgebraicField::A)
396
+ kring = PolynomialRing.new(field, [xname])
397
+ unit = Num.new(poly.leading_coefficient.value)
398
+ factors = []
399
+ poly.clear_denominators.to_ring(QQ[xname]).squarefree_decomposition.each do |part, mult|
400
+ part = part.monic
401
+ next if part.degree.zero?
402
+ shift = 0
403
+ norm = nil
404
+ loop do
405
+ shifted = part.to_expr.subs(x => x - shift * a)
406
+ ring2 = QQ[AlgebraicField::A, xname]
407
+ norm = QQ[xname].call(ring2.call(shifted).resultant(ring2.call(field.minpoly.to_expr), AlgebraicField::A).to_expr)
408
+ break if norm.gcd(norm.derivative).constant?
409
+ shift += 1
410
+ end
411
+ norm.factor.factors.map(&:first).each do |ni|
412
+ candidate = KPolynomial.from_rational(ni.to_expr.subs(x => x + shift * a), field, xname)
413
+ g = KPolynomial.from_rational(part.to_expr, field, xname).gcd(candidate)
414
+ next if g.degree.zero?
415
+ factors << [g.to_polynomial(kring), mult]
416
+ end
417
+ end
418
+ Factorization.new(kring, unit, factors.sort_by { |g, m| [g.degree, g.to_s, m] })
419
+ end
420
+
421
+ # Polynomials in x with coefficients in an algebraic field (dense array).
422
+ class KPolynomial
423
+ attr_reader :coeffs, :field
424
+
425
+ def initialize(coeffs, field)
426
+ @coeffs = coeffs.dup
427
+ @coeffs.pop while !@coeffs.empty? && @coeffs.last.zero?
428
+ @field = field
429
+ freeze
430
+ end
431
+
432
+ # expr: polynomial in x whose coefficients are polynomials in _a
433
+ def self.from_rational(expr, field, xname)
434
+ ring2 = QQ[AlgebraicField::A, xname]
435
+ p = ring2.call(expr)
436
+ coeffs = (0..p.degree(xname)).map { |k| field.element(QQ[AlgebraicField::A].call(p.coefficient_in(xname, k).to_expr)) }
437
+ new(coeffs, field)
438
+ end
439
+
440
+ def degree = coeffs.size - 1
441
+ def zero? = coeffs.empty?
442
+ def lc = coeffs.last
443
+
444
+ def monic
445
+ return self if zero?
446
+ inv = lc.inverse
447
+ KPolynomial.new(coeffs.map { |c| c * inv }, field)
448
+ end
449
+
450
+ def rem(other)
451
+ r = coeffs.dup
452
+ inv = other.lc.inverse
453
+ while r.size >= other.coeffs.size && !r.empty?
454
+ c = r.last * inv
455
+ shift = r.size - other.coeffs.size
456
+ other.coeffs.each_with_index { |oc, k| r[shift + k] = r[shift + k] - c * oc }
457
+ r.pop while !r.empty? && r.last.zero?
458
+ end
459
+ KPolynomial.new(r, field)
460
+ end
461
+
462
+ def gcd(other)
463
+ a, b = self, other
464
+ a, b = b, a.rem(b) until b.zero?
465
+ a.monic
466
+ end
467
+
468
+ def to_polynomial(kring)
469
+ Polynomial.new(kring, coeffs.each_with_index.to_h { |c, k| [[k], c.to_expr] })
470
+ end
471
+ end
472
+ end
473
+
474
+ class NumberSet
475
+ # QQ.adjoin(sqrt(2)) is the field QQ(sqrt(2)).
476
+ def adjoin(alpha)
477
+ raise DomainError, "adjoin extends QQ" unless self == QQ
478
+ Algebraic.adjoin(alpha)
479
+ end
480
+ end
481
+ end