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,318 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Linear algebra for matrices whose entries are polynomials or rational
|
|
5
|
+
# functions in one indeterminate with rational coefficients, by evaluation
|
|
6
|
+
# and interpolation (P. Horn, Faktorisierung in Schief-Polynomringen,
|
|
7
|
+
# Kassel 2008, chapter 6): a degree bound B for the result, B + 1 exact
|
|
8
|
+
# rational evaluations of the matrix, numeric Gaussian elimination at every
|
|
9
|
+
# point and Newton interpolation of the values.
|
|
10
|
+
#
|
|
11
|
+
# PolyDet determinant, O(n**4 d**2) field operations
|
|
12
|
+
# RatDet the same after clearing row denominators
|
|
13
|
+
# PolyLinearSolve M*Y = b through Cramer: interpolate det(M) and the
|
|
14
|
+
# isolated numerators y_i * det(M)
|
|
15
|
+
# Nullspace drop dependent rows and columns, solve the square
|
|
16
|
+
# system for every remaining column (Algorithm 25)
|
|
17
|
+
#
|
|
18
|
+
# Source: [Hor08, ch. 6] (P. Horn, Faktorisierung in Schief-Polynomringen,
|
|
19
|
+
# Kassel 2008), Algorithms 20-25; the same evaluation/interpolation idea
|
|
20
|
+
# for determinants over ZZ is in [vzGG13, §5.5]. Keys: MANUAL.md, Sources.
|
|
21
|
+
#
|
|
22
|
+
# Every public function returns nil when the matrix is not of that shape
|
|
23
|
+
# (several indeterminates, algebraic or floating point coefficients, ...),
|
|
24
|
+
# so Matrix can fall back to elimination over expressions.
|
|
25
|
+
module PolyMatrix
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# ---- entry points -----------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# Determinant as an expression, or nil.
|
|
31
|
+
def det(entries)
|
|
32
|
+
data = prepare(entries) or return nil
|
|
33
|
+
x, rows, scale = data
|
|
34
|
+
d = poly_det(rows, x)
|
|
35
|
+
quotient(d, scale.reduce { |acc, s| acc * s }) # Algorithm 22: divide by the row scales
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Solution of the square system M*Y = b as an Array of expressions, or nil
|
|
39
|
+
# when M is singular or the data is not polynomial.
|
|
40
|
+
def solve(entries, b)
|
|
41
|
+
data = prepare(entries, [b]) or return nil
|
|
42
|
+
x, rows, scale, columns = data
|
|
43
|
+
_ = scale
|
|
44
|
+
system = solve_columns(rows, columns, x) or return nil
|
|
45
|
+
system.first.first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Inverse as rows of expressions, or nil when singular or not polynomial.
|
|
49
|
+
def inverse(entries)
|
|
50
|
+
n = entries.size
|
|
51
|
+
identity = Array.new(n) { |j| Array.new(n) { |i| Num.new(i == j ? 1 : 0) } }
|
|
52
|
+
data = prepare(entries, identity) or return nil
|
|
53
|
+
x, rows, scale, columns = data
|
|
54
|
+
_ = scale # prepare scaled the identity columns along with the rows, so S*M*Y = S*e_j
|
|
55
|
+
system = solve_columns(rows, columns, x) or return nil
|
|
56
|
+
system.first.transpose
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Kernel basis in the reduced-row-echelon convention (1 in the free
|
|
60
|
+
# column), as Arrays of expressions, or nil.
|
|
61
|
+
def kernel(entries)
|
|
62
|
+
data = prepare(entries) or return nil
|
|
63
|
+
x, rows, = data
|
|
64
|
+
k = rows.first.size
|
|
65
|
+
independent_rows, pivots = independent(rows, x)
|
|
66
|
+
free = (0...k).to_a - pivots
|
|
67
|
+
return [] if free.empty?
|
|
68
|
+
square = independent_rows.map { |r| pivots.map { |c| r[c] } }
|
|
69
|
+
columns = free.map { |f| independent_rows.map { |r| negate(r[f]) } }
|
|
70
|
+
system = solve_columns(square, columns, x) or return nil
|
|
71
|
+
solutions = system.first
|
|
72
|
+
vectors = free.each_with_index.map do |f, l|
|
|
73
|
+
v = Array.new(k) { Num.new(0) }
|
|
74
|
+
v[f] = Num.new(1)
|
|
75
|
+
pivots.each_with_index { |p, i| v[p] = solutions[l][i] }
|
|
76
|
+
v
|
|
77
|
+
end
|
|
78
|
+
return nil unless vectors.all? { |v| in_kernel?(rows, v, x) }
|
|
79
|
+
vectors
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ---- preparation --------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
# => [x, rows of coefficient arrays, row scales (Polynomial), extra columns]
|
|
85
|
+
# Every entry must be a rational function with rational coefficients in
|
|
86
|
+
# one indeterminate. Rows are multiplied by the lcm of their denominators.
|
|
87
|
+
def prepare(entries, extra_columns = [])
|
|
88
|
+
all = entries.flatten + extra_columns.flatten
|
|
89
|
+
vars = all.flat_map { |e| e.is_a?(Expression) ? e.variables : [] }.uniq
|
|
90
|
+
return nil unless vars.size == 1
|
|
91
|
+
x = vars.first
|
|
92
|
+
ring = QQ[x]
|
|
93
|
+
fractions = entries.map { |r| r.map { |e| fraction(e, ring) or return nil } }
|
|
94
|
+
extras = extra_columns.map { |col| col.map { |e| fraction(e, ring) or return nil } }
|
|
95
|
+
scale = fractions.each_with_index.map do |row, i|
|
|
96
|
+
dens = row.map(&:last) + extras.map { |col| col[i].last }
|
|
97
|
+
dens.reduce(ring.one) { |acc, d| acc.lcm(d) }
|
|
98
|
+
end
|
|
99
|
+
rows = fractions.each_with_index.map { |row, i| row.map { |n, d| coefficients(n * scale[i].exact_div(d)) } }
|
|
100
|
+
columns = extras.map { |col| col.each_with_index.map { |(n, d), i| coefficients(n * scale[i].exact_div(d)) } }
|
|
101
|
+
[x, rows, scale, columns]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def fraction(entry, ring)
|
|
105
|
+
entry = Expression.lift(entry)
|
|
106
|
+
if entry.is_a?(Num)
|
|
107
|
+
return nil unless entry.value.is_a?(Integer) || entry.value.is_a?(Rational)
|
|
108
|
+
return [ring.call(entry.value), ring.one]
|
|
109
|
+
end
|
|
110
|
+
pair = Fraction.as_fraction(entry, ring.vars) or return nil
|
|
111
|
+
pair.map { |p| p.to_ring(ring) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Coefficient array (constant term first) of a univariate Polynomial.
|
|
115
|
+
def coefficients(poly)
|
|
116
|
+
return [] if poly.zero?
|
|
117
|
+
poly.coefficients.map { |c| Rational(c.value) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def degree(coeffs) = coeffs.size - 1
|
|
121
|
+
|
|
122
|
+
def evaluate(coeffs, a) = coeffs.reverse_each.reduce(0r) { |acc, c| acc * a + c }
|
|
123
|
+
|
|
124
|
+
def negate(coeffs) = coeffs.map(&:-@)
|
|
125
|
+
|
|
126
|
+
# ---- PolyDet ------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
# Lemma 6.1: deg det M <= min(sum of row maxima, sum of column maxima).
|
|
129
|
+
def degree_bound(rows)
|
|
130
|
+
row_max = rows.map { |r| r.map { |c| degree(c) }.max }
|
|
131
|
+
col_max = rows.transpose.map { |c| c.map { |e| degree(e) }.max }
|
|
132
|
+
return -1 if row_max.include?(-1) || col_max.include?(-1)
|
|
133
|
+
[row_max.sum, col_max.sum].min
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Algorithm 21: determinant of a matrix of coefficient arrays, as a
|
|
137
|
+
# coefficient array.
|
|
138
|
+
def poly_det(rows, _x)
|
|
139
|
+
bound = degree_bound(rows)
|
|
140
|
+
return [] if bound.negative?
|
|
141
|
+
points = evaluation_points.first(bound + 1)
|
|
142
|
+
values = points.map { |a| numeric_det(rows.map { |r| r.map { |c| evaluate(c, a) } }) }
|
|
143
|
+
interpolate(points, values)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# 0, 1, -1, 2, -2, ...
|
|
147
|
+
def evaluation_points
|
|
148
|
+
Enumerator.new do |y|
|
|
149
|
+
y << 0r
|
|
150
|
+
(1..).each { |i| y << Rational(i) << Rational(-i) }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# ---- PolyLinearSolve ----------------------------------------------------
|
|
155
|
+
|
|
156
|
+
# Algorithm 23 for several right-hand sides sharing the determinant and
|
|
157
|
+
# the evaluation points. => [[solution columns as expressions], det]
|
|
158
|
+
# or nil when the matrix is singular.
|
|
159
|
+
def solve_columns(rows, columns, x)
|
|
160
|
+
n = rows.size
|
|
161
|
+
det = poly_det(rows, x)
|
|
162
|
+
return nil if det.empty?
|
|
163
|
+
bounds = columns.map do |col|
|
|
164
|
+
(0...n).map { |i| degree_bound(rows.each_with_index.map { |r, k| r.dup.tap { |row| row[i] = col[k] } }) }.max
|
|
165
|
+
end
|
|
166
|
+
bound = [bounds.max || 0, 0].max
|
|
167
|
+
points = []
|
|
168
|
+
evaluation_points.each do |a|
|
|
169
|
+
next if evaluate(det, a).zero?
|
|
170
|
+
points << a
|
|
171
|
+
break if points.size == bound + 1
|
|
172
|
+
end
|
|
173
|
+
det_values = points.map { |a| evaluate(det, a) }
|
|
174
|
+
numerators = columns.map { Array.new(n) { [] } }
|
|
175
|
+
points.each_with_index do |a, idx|
|
|
176
|
+
matrix = rows.map { |r| r.map { |c| evaluate(c, a) } }
|
|
177
|
+
columns.each_with_index do |col, l|
|
|
178
|
+
y = numeric_solve(matrix, col.map { |c| evaluate(c, a) })
|
|
179
|
+
n.times { |i| numerators[l][i][idx] = y[i] * det_values[idx] }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
det_poly = polynomial(det, x)
|
|
183
|
+
solutions = numerators.map do |cols|
|
|
184
|
+
cols.map { |values| quotient(interpolate(points, values), det_poly) }
|
|
185
|
+
end
|
|
186
|
+
[solutions, det_poly]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Independent rows and pivot columns, found on the generic evaluation
|
|
190
|
+
# that gives the largest rank among a few points. #kernel verifies the
|
|
191
|
+
# result exactly afterwards.
|
|
192
|
+
def independent(rows, _x)
|
|
193
|
+
best = nil
|
|
194
|
+
evaluation_points.first(4).each do |a|
|
|
195
|
+
matrix = rows.map { |r| r.map { |c| evaluate(c, a) } }
|
|
196
|
+
pivots, row_indices = numeric_pivots(matrix)
|
|
197
|
+
best = [pivots, row_indices] if best.nil? || pivots.size > best.first.size
|
|
198
|
+
end
|
|
199
|
+
pivots, row_indices = best
|
|
200
|
+
[row_indices.map { |i| rows[i] }, pivots]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def in_kernel?(rows, vector, x)
|
|
204
|
+
rows.all? do |r|
|
|
205
|
+
total = r.each_with_index.reduce(Num.new(0)) { |acc, (c, j)| acc + polynomial(c, x).to_expr * vector[j] }
|
|
206
|
+
Scalar.zero?(Fraction.cancel(total))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ---- exact rational linear algebra --------------------------------------
|
|
211
|
+
|
|
212
|
+
def numeric_det(matrix)
|
|
213
|
+
m = matrix.map(&:dup)
|
|
214
|
+
n = m.size
|
|
215
|
+
result = 1r
|
|
216
|
+
n.times do |c|
|
|
217
|
+
p = (c...n).find { |i| !m[i][c].zero? } or return 0r
|
|
218
|
+
if p != c
|
|
219
|
+
m[c], m[p] = m[p], m[c]
|
|
220
|
+
result = -result
|
|
221
|
+
end
|
|
222
|
+
pivot = m[c][c]
|
|
223
|
+
result *= pivot
|
|
224
|
+
((c + 1)...n).each do |i|
|
|
225
|
+
f = m[i][c] / pivot
|
|
226
|
+
next if f.zero?
|
|
227
|
+
m[i] = m[i].zip(m[c]).map { |a, b| a - f * b }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
result
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Solution of a regular square system over QQ.
|
|
234
|
+
def numeric_solve(matrix, b)
|
|
235
|
+
m = matrix.each_with_index.map { |r, i| r + [b[i]] }
|
|
236
|
+
n = m.size
|
|
237
|
+
n.times do |c|
|
|
238
|
+
p = (c...n).find { |i| !m[i][c].zero? } or raise ZeroDivisionError, "singular evaluation"
|
|
239
|
+
m[c], m[p] = m[p], m[c]
|
|
240
|
+
pivot = m[c][c]
|
|
241
|
+
m[c] = m[c].map { |v| v / pivot }
|
|
242
|
+
n.times do |i|
|
|
243
|
+
next if i == c
|
|
244
|
+
f = m[i][c]
|
|
245
|
+
next if f.zero?
|
|
246
|
+
m[i] = m[i].zip(m[c]).map { |a, b| a - f * b }
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
m.map(&:last)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# => [pivot columns, indices of the rows that carry them]
|
|
253
|
+
def numeric_pivots(matrix)
|
|
254
|
+
m = matrix.each_with_index.map { |r, i| [r.dup, i] }
|
|
255
|
+
rows, cols = m.size, (m.first&.first&.size || 0)
|
|
256
|
+
pivots = []
|
|
257
|
+
row_indices = []
|
|
258
|
+
r = 0
|
|
259
|
+
cols.times do |c|
|
|
260
|
+
break if r == rows
|
|
261
|
+
p = (r...rows).find { |i| !m[i][0][c].zero? } or next
|
|
262
|
+
m[r], m[p] = m[p], m[r]
|
|
263
|
+
pivot = m[r][0][c]
|
|
264
|
+
m[r][0] = m[r][0].map { |v| v / pivot }
|
|
265
|
+
rows.times do |i|
|
|
266
|
+
next if i == r
|
|
267
|
+
f = m[i][0][c]
|
|
268
|
+
next if f.zero?
|
|
269
|
+
m[i][0] = m[i][0].zip(m[r][0]).map { |a, b| a - f * b }
|
|
270
|
+
end
|
|
271
|
+
pivots << c
|
|
272
|
+
row_indices << m[r][1]
|
|
273
|
+
r += 1
|
|
274
|
+
end
|
|
275
|
+
[pivots, row_indices.sort]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# ---- interpolation and output -------------------------------------------
|
|
279
|
+
|
|
280
|
+
# Newton interpolation through (points[i], values[i]) => coefficient array.
|
|
281
|
+
def interpolate(points, values)
|
|
282
|
+
n = points.size
|
|
283
|
+
coeffs = values.dup
|
|
284
|
+
(1...n).each do |level|
|
|
285
|
+
(n - 1).downto(level) { |i| coeffs[i] = (coeffs[i] - coeffs[i - 1]) / (points[i] - points[i - level]) }
|
|
286
|
+
end
|
|
287
|
+
poly = []
|
|
288
|
+
(n - 1).downto(0) do |i|
|
|
289
|
+
# poly = poly * (x - points[i]) + coeffs[i]
|
|
290
|
+
shifted = [0r] + poly
|
|
291
|
+
poly.each_with_index { |c, k| shifted[k] -= c * points[i] }
|
|
292
|
+
shifted[0] += coeffs[i]
|
|
293
|
+
poly = shifted
|
|
294
|
+
end
|
|
295
|
+
poly.pop while !poly.empty? && poly.last.zero?
|
|
296
|
+
poly
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def polynomial(coeffs, x)
|
|
300
|
+
ring = QQ[x || :_]
|
|
301
|
+
Polynomial.new(ring, coeffs.each_with_index.reject { |c, _| c.zero? }.to_h { |c, k| [[k], Num.new(Simplify.normalize_number(c))] })
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# num / den as a cancelled expression; num a coefficient array or
|
|
305
|
+
# Polynomial, den a Polynomial.
|
|
306
|
+
def quotient(num, den)
|
|
307
|
+
num = polynomial(num, den.ring.vars.first) if num.is_a?(Array)
|
|
308
|
+
return Num.new(0) if num.zero?
|
|
309
|
+
g = num.gcd(den)
|
|
310
|
+
num = num.exact_div(g)
|
|
311
|
+
den = den.exact_div(g)
|
|
312
|
+
lc = den.leading_coefficient
|
|
313
|
+
num *= Scalar.div(Num.new(1), lc)
|
|
314
|
+
den = den.monic
|
|
315
|
+
den.constant? ? num.to_expr : (num.to_expr / den.to_expr).simplify
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Polynomial solutions of a linear recurrence with polynomial coefficients,
|
|
5
|
+
#
|
|
6
|
+
# q_0(n)*c(n) + q_1(n)*c(n + 1) + ... + q_r(n)*c(n + r) = 0,
|
|
7
|
+
#
|
|
8
|
+
# the subroutine Petkovsek's algorithm rests on. The whole difficulty is
|
|
9
|
+
# bounding the degree of c, and the bound is Abramov's: write the operator
|
|
10
|
+
# in the difference basis, L = sum_i w_i(n)*Delta**i with Delta = N - 1 and
|
|
11
|
+
# w_i = sum_j binomial(j, i)*q_j. Delta**i lowers the degree by exactly i,
|
|
12
|
+
# so with
|
|
13
|
+
#
|
|
14
|
+
# m = max_i (deg w_i - i)
|
|
15
|
+
# P(d) = sum of lc(w_i)*d*(d - 1)*...*(d - i + 1) over the i that reach m
|
|
16
|
+
#
|
|
17
|
+
# a candidate c of degree d gives deg L(c) = d + m whenever P(d) != 0.
|
|
18
|
+
# A solution of L(c) = 0 therefore needs P(d) = 0, and an inhomogeneous
|
|
19
|
+
# right-hand side needs d + m = deg(rhs); the bound is the largest of those.
|
|
20
|
+
#
|
|
21
|
+
# Sources (keys: MANUAL.md, Sources): [Koe14, ch. 9], [PWZ96, ch. 8].
|
|
22
|
+
module PolyRecurrence
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# A basis of the polynomial solutions of the homogeneous equation, as
|
|
26
|
+
# expressions in +n+; [] when the only one is zero.
|
|
27
|
+
def basis(coeffs, n)
|
|
28
|
+
solutions(coeffs, n).last
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# [particular, basis] for L(c) = rhs (rhs nil or 0: [0, basis]), or
|
|
32
|
+
# [nil, []] when there is no polynomial solution.
|
|
33
|
+
def solutions(coeffs, n, rhs = nil)
|
|
34
|
+
homogeneous = rhs.nil? || Scalar.zero?(rhs)
|
|
35
|
+
d = degree_bound(coeffs, n, rhs)
|
|
36
|
+
return [nil, []] if d.nil? || d.negative?
|
|
37
|
+
xs = (0..d).map { |i| Var.new(:"_p#{i}") }
|
|
38
|
+
c = xs.each_with_index.reduce(Num.new(0)) { |acc, (x, i)| acc + x * n**i }
|
|
39
|
+
residual = apply(coeffs, c, n)
|
|
40
|
+
residual -= rhs unless homogeneous
|
|
41
|
+
conditions = Solve.polynomial_coefficients(residual.expand, n) or return [nil, []]
|
|
42
|
+
solution = Solve.linear_system(conditions, xs).first or return [nil, []]
|
|
43
|
+
general = c.subs(solution)
|
|
44
|
+
free = xs - solution.keys
|
|
45
|
+
particular = homogeneous ? Num.new(0) : pick(general, free, nil) || Num.new(0)
|
|
46
|
+
# With a right-hand side each choice of the free constants carries the
|
|
47
|
+
# particular solution with it; the difference is the homogeneous part.
|
|
48
|
+
[particular, free.filter_map { |f| difference(pick(general, free, f), particular) }]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# L(c) with c substituted: sum_j q_j(n)*c(n + j).
|
|
52
|
+
def apply(coeffs, c, n)
|
|
53
|
+
coeffs.each_with_index.reduce(Num.new(0)) { |acc, (q, j)| acc + q * c.subs(n => n + j) }.simplify
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The largest degree a polynomial solution can have, or nil for none.
|
|
57
|
+
def degree_bound(coeffs, n, rhs = nil)
|
|
58
|
+
tops = leading_terms(coeffs, n) or return nil
|
|
59
|
+
return nil if tops.empty?
|
|
60
|
+
m = tops.map { |(i, degree, _)| degree - i }.max
|
|
61
|
+
d = Var.new(:_d)
|
|
62
|
+
indicial = tops.select { |(i, degree, _)| degree - i == m }
|
|
63
|
+
.reduce(Num.new(0)) { |acc, (i, _, lc)| acc + lc * falling(d, i) }
|
|
64
|
+
bounds = integer_roots(indicial, d)
|
|
65
|
+
unless rhs.nil? || Scalar.zero?(rhs)
|
|
66
|
+
cs = Solve.polynomial_coefficients(rhs.expand, n) or return nil
|
|
67
|
+
bounds += [cs.size - 1 - m]
|
|
68
|
+
end
|
|
69
|
+
bounds.max
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# [index, degree, leading coefficient] of every non-zero w_i.
|
|
73
|
+
def leading_terms(coeffs, n)
|
|
74
|
+
delta_coefficients(coeffs).each_with_index.filter_map do |w, i|
|
|
75
|
+
cs = Solve.polynomial_coefficients(w.expand, n) or return nil
|
|
76
|
+
next nil if cs.all? { |c| Scalar.zero?(c) }
|
|
77
|
+
[i, cs.size - 1, cs.last]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# w_i = sum_j binomial(j, i)*q_j: the operator in the difference basis.
|
|
82
|
+
def delta_coefficients(coeffs)
|
|
83
|
+
r = coeffs.size - 1
|
|
84
|
+
(0..r).map do |i|
|
|
85
|
+
(i..r).reduce(Num.new(0)) { |acc, j| acc + Num.new(choose(j, i)) * coeffs[j] }.simplify
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def choose(n, k) = (0...k).reduce(1) { |acc, i| acc * (n - i) / (i + 1) }
|
|
90
|
+
|
|
91
|
+
# d*(d - 1)*...*(d - i + 1)
|
|
92
|
+
def falling(d, i) = (0...i).reduce(Num.new(1)) { |acc, s| acc * (d - s) }
|
|
93
|
+
|
|
94
|
+
def integer_roots(poly, d)
|
|
95
|
+
coeffs = Solve.polynomial_coefficients(poly.expand, d) or return []
|
|
96
|
+
return [] if coeffs.size < 2 # a non-zero constant has no roots
|
|
97
|
+
Solve.polynomial_roots(coeffs)
|
|
98
|
+
.filter_map { |r| r.simplify }
|
|
99
|
+
.select { |r| r.is_a?(Num) && r.value.is_a?(Integer) && !r.value.negative? }
|
|
100
|
+
.map(&:value).uniq
|
|
101
|
+
rescue NotImplementedError, RCAS::Unsupported, DomainError
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# One member of the solution space: +free+ all zero but +one+, which is 1.
|
|
106
|
+
def pick(general, free, one)
|
|
107
|
+
value = general.subs(free.to_h { |v| [v, Num.new(v == one ? 1 : 0)] }).simplify
|
|
108
|
+
Scalar.zero?(value) ? nil : value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def difference(solution, particular)
|
|
112
|
+
return nil if solution.nil?
|
|
113
|
+
value = (solution - particular).simplify
|
|
114
|
+
Scalar.zero?(value) ? nil : value
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|