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