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,466 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# An element of a PolynomialRing, stored as { exponent_vector => coefficient }.
|
|
5
|
+
# Coefficients are Expressions (usually Num, but parameters are allowed).
|
|
6
|
+
#
|
|
7
|
+
# R = QQ[:x]
|
|
8
|
+
# f = R.(:x**2 - 1)
|
|
9
|
+
# g = R.(:x - 1)
|
|
10
|
+
# f / g # => 1 + x
|
|
11
|
+
# f.gcd(R.(:x**2 + 2 * :x + 1)) # => 1 + x
|
|
12
|
+
class Polynomial
|
|
13
|
+
include Algebraic
|
|
14
|
+
|
|
15
|
+
attr_reader :ring, :terms
|
|
16
|
+
|
|
17
|
+
def initialize(ring, terms)
|
|
18
|
+
@ring = ring
|
|
19
|
+
terms = terms.transform_values { |c| ring.base.normalize_coefficient(c) } if ring.base.respond_to?(:normalize_coefficient)
|
|
20
|
+
@terms = terms.reject { |_, c| Scalar.zero?(c) }.freeze
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build from an Expression. Raises DomainError if the expression is not a
|
|
25
|
+
# polynomial in the ring's variables with coefficients in the base domain.
|
|
26
|
+
def self.from_expr(ring, expr, promote: false)
|
|
27
|
+
return expr.to_ring(ring) if expr.is_a?(Polynomial)
|
|
28
|
+
|
|
29
|
+
expr = Expression.lift(expr)
|
|
30
|
+
size = ring.vars.size
|
|
31
|
+
terms = {}
|
|
32
|
+
|
|
33
|
+
constant, table = Expand.table(expr)
|
|
34
|
+
terms[Array.new(size, 0)] = Num.new(constant) unless constant.zero?
|
|
35
|
+
|
|
36
|
+
table.each do |factors, coeff|
|
|
37
|
+
exps = Array.new(size, 0)
|
|
38
|
+
c = Expression.lift(coeff)
|
|
39
|
+
|
|
40
|
+
factors.each do |base, exp|
|
|
41
|
+
if base.is_a?(Var) && (i = ring.index(base.name))
|
|
42
|
+
unless exp.is_a?(Integer) && exp >= 0
|
|
43
|
+
raise DomainError, "#{expr} is not a polynomial in #{base}: exponent #{exp}"
|
|
44
|
+
end
|
|
45
|
+
exps[i] += exp
|
|
46
|
+
elsif (Simplify.power_node(base, exp).variables & ring.vars).empty?
|
|
47
|
+
# the exponent counts too: exp(x) is e**x, no coefficient of a
|
|
48
|
+
# polynomial in x (third review, A3)
|
|
49
|
+
c *= Simplify.power_node(base, exp)
|
|
50
|
+
else
|
|
51
|
+
raise DomainError, "#{expr} is not a polynomial in #{ring.vars.join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
terms[exps] = Scalar.add(terms[exps] || Num.new(0), c.simplify)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
terms.each_value do |c|
|
|
59
|
+
next if Scalar.zero?(c) || ring.base.include?(c)
|
|
60
|
+
if promote && (d = Infer.domain(c))
|
|
61
|
+
return from_expr(PolynomialRing.new(ring.base.join(d), ring.vars), expr)
|
|
62
|
+
end
|
|
63
|
+
raise DomainError, "coefficient #{c} of #{expr} is not in #{ring.base}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
new(ring, terms)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ---- structure --------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
# The ring is where the polynomial lives; base is its coefficient domain.
|
|
72
|
+
def domain = ring
|
|
73
|
+
def base = ring.base
|
|
74
|
+
|
|
75
|
+
def zero? = terms.empty?
|
|
76
|
+
def constant? = terms.keys.all? { |e| e.all?(&:zero?) }
|
|
77
|
+
def constant_term = terms[Array.new(ring.vars.size, 0)] || Num.new(0)
|
|
78
|
+
|
|
79
|
+
# Total degree, or the degree in one variable. The zero polynomial has degree -1.
|
|
80
|
+
def degree(var = nil)
|
|
81
|
+
return -1 if zero?
|
|
82
|
+
if var
|
|
83
|
+
i = ring.index(var.is_a?(Var) ? var.name : var.to_sym) or raise ArgumentError, "#{var} is not a variable of #{ring}"
|
|
84
|
+
terms.keys.map { |e| e[i] }.max
|
|
85
|
+
else
|
|
86
|
+
terms.keys.map(&:sum).max
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def leading_exponents = terms.keys.max_by { |e| [e.sum, e] }
|
|
91
|
+
def leading_coefficient = zero? ? Num.new(0) : terms[leading_exponents]
|
|
92
|
+
|
|
93
|
+
# Coefficient of x**a * y**b ... given the exponents in ring order.
|
|
94
|
+
def coeff(*exps) = terms[exps] || Num.new(0)
|
|
95
|
+
|
|
96
|
+
# Univariate only: coefficients from the constant term upwards.
|
|
97
|
+
def coefficients
|
|
98
|
+
univariate!
|
|
99
|
+
(0..degree).map { |k| coeff(k) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def monomials
|
|
103
|
+
terms.keys.sort_by { |e| [e.sum, e] }.map { |e| monomial(e) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ---- arithmetic -------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def +(other) = combine(other, :+) { |a, b| Polynomial.new(a.ring, merge(a.terms, b.terms)) }
|
|
109
|
+
def -(other) = combine(other, :-) { |a, b| a + -b }
|
|
110
|
+
def -@ = Polynomial.new(ring, terms.transform_values { |c| Scalar.neg(c) })
|
|
111
|
+
|
|
112
|
+
def *(other)
|
|
113
|
+
combine(other, :*) do |a, b|
|
|
114
|
+
product = {}
|
|
115
|
+
a.terms.each do |ea, ca|
|
|
116
|
+
b.terms.each do |eb, cb|
|
|
117
|
+
e = ea.zip(eb).map(&:sum)
|
|
118
|
+
product[e] = Scalar.add(product[e] || Num.new(0), Scalar.mul(ca, cb))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
Polynomial.new(a.ring, product)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def **(n)
|
|
126
|
+
raise ArgumentError, "polynomial powers must be non-negative integers" unless n.is_a?(Integer) && n >= 0
|
|
127
|
+
result = ring.one
|
|
128
|
+
base = self
|
|
129
|
+
while n.positive?
|
|
130
|
+
result *= base if n.odd?
|
|
131
|
+
base *= base
|
|
132
|
+
n >>= 1
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Exact division; raises DomainError if the remainder is not zero.
|
|
138
|
+
def /(other)
|
|
139
|
+
combine(other, :/) do |a, b|
|
|
140
|
+
next a.exact_div(b) unless a.ring.univariate? || b.constant?
|
|
141
|
+
q, r = a.divmod(b)
|
|
142
|
+
raise DomainError, "#{b} does not divide #{a} (remainder #{r})" unless r.zero?
|
|
143
|
+
q
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Division that must come out exact, in any number of variables.
|
|
148
|
+
def exact_div(other)
|
|
149
|
+
other = Polynomial.from_expr(ring, other) unless other.is_a?(Polynomial) && other.ring == ring
|
|
150
|
+
raise ZeroDivisionError, "division by the zero polynomial" if other.zero?
|
|
151
|
+
remainder = terms.dup
|
|
152
|
+
quotient = {}
|
|
153
|
+
eg = other.leading_exponents
|
|
154
|
+
cg = other.leading_coefficient
|
|
155
|
+
until remainder.empty?
|
|
156
|
+
er = remainder.keys.max_by { |e| [e.sum, e] }
|
|
157
|
+
shift = er.zip(eg).map { |a, b| a - b }
|
|
158
|
+
c = Scalar.div(remainder[er], cg)
|
|
159
|
+
unless shift.all? { |k| k >= 0 } && ring.base.include?(c)
|
|
160
|
+
raise DomainError, "#{other} does not divide #{self} in #{ring}"
|
|
161
|
+
end
|
|
162
|
+
quotient[shift] = c
|
|
163
|
+
other.terms.each do |e, coeff|
|
|
164
|
+
key = e.zip(shift).map(&:sum)
|
|
165
|
+
v = Scalar.sub(remainder[key] || Num.new(0), Scalar.mul(c, coeff))
|
|
166
|
+
Scalar.zero?(v) ? remainder.delete(key) : remainder[key] = v
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
Polynomial.new(ring, quotient)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def divmod(other)
|
|
173
|
+
combine(other, :divmod) do |a, b|
|
|
174
|
+
raise ZeroDivisionError, "division by the zero polynomial" if b.zero?
|
|
175
|
+
return [a * Scalar.div(Num.new(1), b.constant_term), a.ring.zero] if b.constant? && !b.constant_term.is_a?(Num)
|
|
176
|
+
a.univariate! unless b.constant?
|
|
177
|
+
|
|
178
|
+
quotient = {}
|
|
179
|
+
remainder = a
|
|
180
|
+
db = b.degree
|
|
181
|
+
lb = b.leading_coefficient
|
|
182
|
+
while !remainder.zero? && remainder.degree >= db
|
|
183
|
+
c = Scalar.div(remainder.leading_coefficient, lb)
|
|
184
|
+
unless a.ring.base.include?(c)
|
|
185
|
+
raise DomainError, "#{b} does not divide #{a} in #{a.ring}; try #{a.ring.base.fraction_field}[#{a.ring.vars.join(', ')}]"
|
|
186
|
+
end
|
|
187
|
+
shift = remainder.leading_exponents.zip(b.leading_exponents).map { |x, y| x - y }
|
|
188
|
+
quotient[shift] = Scalar.add(quotient[shift] || Num.new(0), c)
|
|
189
|
+
remainder -= Polynomial.new(a.ring, { shift => c }) * b
|
|
190
|
+
end
|
|
191
|
+
[Polynomial.new(a.ring, quotient), remainder]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def div(other) = divmod(other).first
|
|
196
|
+
def %(other) = divmod(other).last
|
|
197
|
+
|
|
198
|
+
def coerce(other) = [Polynomial.from_expr(ring, other), self]
|
|
199
|
+
|
|
200
|
+
def rop(op, left)
|
|
201
|
+
left = Expression.lift(left)
|
|
202
|
+
scalar = scalar_in_base(left)
|
|
203
|
+
return scalar.public_send(op, self) if scalar
|
|
204
|
+
Polynomial.from_expr(ring.join(Polynomial.ring_for(ring.base, left)), left, promote: true).public_send(op, self)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A parameter expression such as 1/(2*a) over Frac(QQ[a])[x] is a
|
|
208
|
+
# constant of the ring, not a polynomial in a new variable a.
|
|
209
|
+
def scalar_in_base(expr)
|
|
210
|
+
return nil if expr.is_a?(Num) || expr.variables.empty? || !(expr.variables & ring.vars).empty?
|
|
211
|
+
return nil unless (ring.base.is_a?(FractionField) || ring.base.is_a?(PolynomialRing)) && ring.base.include?(expr)
|
|
212
|
+
Polynomial.new(ring, { Array.new(ring.vars.size, 0) => expr })
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def ==(other)
|
|
216
|
+
case other
|
|
217
|
+
when Polynomial then to_expr.simplify == other.to_expr.simplify
|
|
218
|
+
when Expression, Numeric, Symbol
|
|
219
|
+
begin
|
|
220
|
+
self == Polynomial.from_expr(ring.join(Polynomial.ring_for(ring.base, Expression.lift(other))), other)
|
|
221
|
+
rescue DomainError
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
else false
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
alias eql? ==
|
|
228
|
+
def hash = [Polynomial, to_expr.simplify].hash
|
|
229
|
+
|
|
230
|
+
# ---- calculus / evaluation --------------------------------------------
|
|
231
|
+
|
|
232
|
+
# Antiderivative in +var+, over the fraction field of the base if needed.
|
|
233
|
+
def integrate(var = nil)
|
|
234
|
+
var ||= ring.vars.first
|
|
235
|
+
i = var_index(var)
|
|
236
|
+
target = ring.base.field? ? ring : ring.base.fraction_field[*ring.vars]
|
|
237
|
+
result = {}
|
|
238
|
+
terms.each do |e, c|
|
|
239
|
+
d = e.dup
|
|
240
|
+
d[i] += 1
|
|
241
|
+
result[d] = Scalar.div(c, Num.new(d[i]))
|
|
242
|
+
end
|
|
243
|
+
Polynomial.new(target, result)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def derivative(var = nil)
|
|
247
|
+
var ||= ring.vars.first
|
|
248
|
+
i = ring.index(var.is_a?(Var) ? var.name : var.to_sym) or raise ArgumentError, "#{var} is not a variable of #{ring}"
|
|
249
|
+
result = {}
|
|
250
|
+
terms.each do |e, c|
|
|
251
|
+
next if e[i].zero?
|
|
252
|
+
d = e.dup
|
|
253
|
+
d[i] -= 1
|
|
254
|
+
result[d] = Scalar.add(result[d] || Num.new(0), Scalar.mul(Num.new(e[i]), c))
|
|
255
|
+
end
|
|
256
|
+
Polynomial.new(ring, result)
|
|
257
|
+
end
|
|
258
|
+
alias diff derivative
|
|
259
|
+
|
|
260
|
+
def call(*args, **bindings) = to_expr.call(*args, **bindings)
|
|
261
|
+
def to_proc = to_expr.to_proc
|
|
262
|
+
def subs(*args) = to_expr.subs(*args)
|
|
263
|
+
|
|
264
|
+
# ---- gcd --------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
# Monic gcd over a field; primitive gcd with positive leading coefficient
|
|
267
|
+
# over ZZ. Works in any number of variables over ZZ and QQ.
|
|
268
|
+
def gcd(other) = combine(other, :gcd) { |a, b| PolyGCD.gcd(a, b) }
|
|
269
|
+
def lcm(other) = combine(other, :lcm) { |a, b| PolyGCD.lcm(a, b) }
|
|
270
|
+
|
|
271
|
+
# [g, s, t] with s*self + t*other == g (univariate over a field).
|
|
272
|
+
def xgcd(other) = combine(other, :xgcd) { |a, b| PolyGCD.xgcd(a, b) }
|
|
273
|
+
|
|
274
|
+
def coprime?(other) = gcd(other).constant?
|
|
275
|
+
|
|
276
|
+
# Resultant with respect to a variable, as the determinant of the
|
|
277
|
+
# Sylvester matrix [GCL92, ch. 7], [CLO15, §3.6] (keys: MANUAL.md, Sources).
|
|
278
|
+
def resultant(other, var = nil)
|
|
279
|
+
combine(other, :resultant) do |a, b|
|
|
280
|
+
x = var || a.ring.vars.first
|
|
281
|
+
m = a.degree(x)
|
|
282
|
+
n = b.degree(x)
|
|
283
|
+
return a.ring.zero if m.negative? || n.negative?
|
|
284
|
+
return a.ring.one if m.zero? && n.zero?
|
|
285
|
+
size = m + n
|
|
286
|
+
ac = (0..m).map { |k| a.coefficient_in(x, k).to_expr }
|
|
287
|
+
bc = (0..n).map { |k| b.coefficient_in(x, k).to_expr }
|
|
288
|
+
rows = []
|
|
289
|
+
n.times { |i| rows << Array.new(size) { |j| (j - i).between?(0, m) ? ac[m - (j - i)] : Num.new(0) } }
|
|
290
|
+
m.times { |i| rows << Array.new(size) { |j| (j - i).between?(0, n) ? bc[n - (j - i)] : Num.new(0) } }
|
|
291
|
+
numeric = rows.flatten.all? { |e| Scalar.numeric?(e) }
|
|
292
|
+
# polynomial entries by evaluation and interpolation (PolyMatrix):
|
|
293
|
+
# the cofactor expansion is factorial in the size, and the dispersion
|
|
294
|
+
# of a degree-five term (a 10 x 10 Sylvester matrix) took two minutes
|
|
295
|
+
# (third review, section 5)
|
|
296
|
+
det = numeric ? Elimination.det(rows) : (PolyMatrix.det(rows) || Elimination.cofactor_det(rows).expand)
|
|
297
|
+
Polynomial.from_expr(a.ring, det)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def discriminant(var = nil)
|
|
302
|
+
x = var || ring.vars.first
|
|
303
|
+
n = degree(x)
|
|
304
|
+
raise ArgumentError, "discriminant needs degree >= 1" if n < 1
|
|
305
|
+
res = resultant(derivative(x), x)
|
|
306
|
+
sign = (n * (n - 1) / 2).even? ? 1 : -1
|
|
307
|
+
(res * sign).exact_div(Polynomial.constant(ring, 1) * leading_coefficient_in(x))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# ---- factorization ----------------------------------------------------
|
|
311
|
+
|
|
312
|
+
# Factorization over ZZ or QQ into irreducibles.
|
|
313
|
+
#
|
|
314
|
+
# QQ[:x].(:x**4 - 1).factor # => (-1 + x)*(1 + x)*(1 + x**2)
|
|
315
|
+
def factor(extension: nil, recombination: nil)
|
|
316
|
+
return Factor.with_recombination(recombination) { factor(extension: extension) } if recombination
|
|
317
|
+
return FiniteFieldFactor.factor(self) if ring.base.is_a?(FiniteField)
|
|
318
|
+
return Factor.factor(self) if extension.nil?
|
|
319
|
+
field = extension.is_a?(AlgebraicField) ? extension : Algebraic.adjoin(extension)
|
|
320
|
+
Algebraic.factor_over(self, field)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def irreducible? = factor.irreducible?
|
|
324
|
+
|
|
325
|
+
# [[squarefree part, multiplicity], ...] such that self = unit * prod part**mult.
|
|
326
|
+
def squarefree_decomposition
|
|
327
|
+
f = factor
|
|
328
|
+
groups = f.factors.group_by(&:last).sort
|
|
329
|
+
groups.map { |m, parts| [parts.map(&:first).reduce(ring.one) { |acc, g| acc * g }, m] }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def squarefree_part = squarefree_decomposition.map(&:first).reduce(ring.one) { |acc, g| acc * g }
|
|
333
|
+
|
|
334
|
+
# Rational roots (with multiplicity) of a univariate polynomial.
|
|
335
|
+
def roots
|
|
336
|
+
univariate!
|
|
337
|
+
factor.factors.flat_map do |g, m|
|
|
338
|
+
next [] unless g.degree == 1
|
|
339
|
+
if ring.base.is_a?(FiniteField)
|
|
340
|
+
[Scalar.div(Scalar.neg(g.coeff(0)), g.coeff(1))] * m
|
|
341
|
+
else
|
|
342
|
+
[Simplify.normalize_number(Rational(-g.coeff(0).value, g.coeff(1).value))] * m
|
|
343
|
+
end
|
|
344
|
+
end.sort_by { |r| r.is_a?(Num) && r.value.respond_to?(:to_i) ? [r.value.to_i, ""] : [0, r.to_s] } # a GF(p^n) element has no integer
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# ---- coefficients in one variable -------------------------------------
|
|
348
|
+
|
|
349
|
+
# The coefficient of x**k as a polynomial in the remaining variables.
|
|
350
|
+
def coefficient_in(var, k)
|
|
351
|
+
i = var_index(var)
|
|
352
|
+
picked = terms.select { |e, _| e[i] == k }.to_h { |e, c| [e.dup.tap { |d| d[i] = 0 }, c] }
|
|
353
|
+
Polynomial.new(ring, picked)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def leading_coefficient_in(var) = coefficient_in(var, degree(var))
|
|
357
|
+
|
|
358
|
+
# Multiply by var**k.
|
|
359
|
+
def shift(var, k)
|
|
360
|
+
i = var_index(var)
|
|
361
|
+
Polynomial.new(ring, terms.to_h { |e, c| [e.dup.tap { |d| d[i] += k }, c] })
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Same polynomial times the lcm of its coefficient denominators.
|
|
365
|
+
def clear_denominators
|
|
366
|
+
lcm = terms.values.reduce(1) { |l, c| c.is_a?(Num) && c.value.is_a?(Rational) ? l.lcm(c.value.denominator) : l }
|
|
367
|
+
lcm == 1 ? self : self * lcm
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.constant(ring, value) = Polynomial.new(ring, { Array.new(ring.vars.size, 0) => Num.new(value) })
|
|
371
|
+
|
|
372
|
+
def monic
|
|
373
|
+
return self if zero?
|
|
374
|
+
self * Scalar.div(Num.new(1), leading_coefficient)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# gcd of the numeric coefficients (ZZ or QQ coefficients only).
|
|
378
|
+
def content
|
|
379
|
+
values = terms.values.map { |c| c.is_a?(Num) ? c.value : raise(DomainError, "content needs numeric coefficients") }
|
|
380
|
+
return 0 if values.empty?
|
|
381
|
+
values.reduce { |g, v| Polynomial.rational_gcd(g, v) }.abs
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def var_index(var)
|
|
385
|
+
ring.index(var.is_a?(Var) ? var.name : var.to_sym) or raise ArgumentError, "#{var} is not a variable of #{ring}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def self.rational_gcd(a, b)
|
|
389
|
+
a, b = Rational(a), Rational(b)
|
|
390
|
+
Simplify.normalize_number(Rational(a.numerator.gcd(b.numerator), a.denominator.lcm(b.denominator)))
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Integer-coefficient polynomial with content 1 and positive leading coefficient.
|
|
394
|
+
def primitive_part
|
|
395
|
+
return self if zero?
|
|
396
|
+
c = content
|
|
397
|
+
c = -c if Scalar.negative?(leading_coefficient)
|
|
398
|
+
Polynomial.new(ring, terms.transform_values { |v| Num.new(Rational(v.value, c)) })
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# ---- conversion -------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
def to_ring(other_ring)
|
|
404
|
+
return self if other_ring == ring
|
|
405
|
+
Polynomial.from_expr(other_ring, to_expr)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def to_expr
|
|
409
|
+
return Num.new(0) if zero?
|
|
410
|
+
terms.map { |e, c| e.all?(&:zero?) ? c : Mul.new(c, monomial(e)) }
|
|
411
|
+
.reduce { |sum, t| Add.new(sum, t) }
|
|
412
|
+
.simplify
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def to_s = to_expr.to_s
|
|
416
|
+
def inspect = to_s
|
|
417
|
+
|
|
418
|
+
# Ring for an expression's undeclared variables (declared ones are parameters).
|
|
419
|
+
def self.ring_for(base, expr)
|
|
420
|
+
free = expr.variables.reject { |v| RCAS.assumption(v) }
|
|
421
|
+
free.empty? ? base : PolynomialRing.new(base, free)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
protected
|
|
425
|
+
|
|
426
|
+
def univariate!
|
|
427
|
+
raise RCAS::Unsupported, "this operation needs a univariate ring, got #{ring}" unless ring.univariate?
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
private
|
|
431
|
+
|
|
432
|
+
def monomial(exps)
|
|
433
|
+
factors = ring.vars.zip(exps).reject { |_, k| k.zero? }.map { |v, k| Simplify.power_node(Var.new(v), k) }
|
|
434
|
+
Simplify.product_node(factors)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def merge(a, b)
|
|
438
|
+
a.merge(b) { |_, x, y| Scalar.add(x, y) }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Bring +other+ into a common ring with self, or fall back to plain
|
|
442
|
+
# Expression arithmetic when it is not a polynomial (e.g. sin(x)).
|
|
443
|
+
def combine(other, op)
|
|
444
|
+
case other
|
|
445
|
+
when Polynomial
|
|
446
|
+
target = ring.join(other.ring)
|
|
447
|
+
yield to_ring(target), other.to_ring(target)
|
|
448
|
+
when Expression, Numeric, Symbol
|
|
449
|
+
expr = Expression.lift(other)
|
|
450
|
+
scalar = scalar_in_base(expr)
|
|
451
|
+
return yield self, scalar if scalar
|
|
452
|
+
target = ring.join(Polynomial.ring_for(ring.base, expr))
|
|
453
|
+
begin
|
|
454
|
+
poly = Polynomial.from_expr(target, expr, promote: true)
|
|
455
|
+
rescue DomainError
|
|
456
|
+
raise if %i[divmod gcd lcm xgcd resultant].include?(op)
|
|
457
|
+
return to_expr.public_send(op, expr)
|
|
458
|
+
end
|
|
459
|
+
target = target.join(poly.ring)
|
|
460
|
+
yield to_ring(target), poly.to_ring(target)
|
|
461
|
+
else
|
|
462
|
+
raise TypeError, "can't combine #{self.class} with #{other.class}"
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|