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