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