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
data/lib/rcas/fps.rb ADDED
@@ -0,0 +1,678 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # The formal power series of a function: the general coefficient in closed
5
+ # form, not the first few terms.
6
+ #
7
+ # fps(exp(x), x) # => sum(x**k/k!, k, 0, oo)
8
+ # fps(sin(x), x) # => sum((-1)**k*x**(1 + 2*k)/(1 + 2*k)!, k, 0, oo)
9
+ # fps((1 + x)**a, x) # => sum(x**k*binomial(a, k), k, 0, oo)
10
+ #
11
+ # Koepf's FPS algorithm [Koe92], [Koe14, ch. 10], in four steps:
12
+ #
13
+ # 1. a homogeneous linear differential equation with polynomial
14
+ # coefficients, sum_j p_j(x)*f^(j)(x) = 0, found with undetermined
15
+ # coefficients (+differential_equation+),
16
+ # 2. the recurrence its Taylor coefficients obey, read off from
17
+ # x**i*f^(j) term by term (+recurrence+),
18
+ # 3. that recurrence solved. rcas goes exactly as far as Koepf's
19
+ # algorithm: the two-term case q(k)*a(k) + r(k)*a(k + m) = 0, which is
20
+ # what an m-fold symmetric hypergeometric coefficient satisfies, so the
21
+ # ratio a(k + m)/a(k) can be read off and multiplied up,
22
+ # 4. the series assembled: one sum per residue class modulo m, its
23
+ # coefficient the product of the ratios (Products.product turns that
24
+ # into factorials), and the terms before the first index of a class
25
+ # taken from the Taylor polynomial.
26
+ #
27
+ # Step 1 is where the search happens. The derivatives are split into
28
+ # {monomial in the transcendental atoms => rational function of x} and the
29
+ # ansatz has to cancel every monomial separately, which is sufficient for
30
+ # the equation to hold, never spurious: a relation between the atoms
31
+ # themselves (sin**2 + cos**2 = 1) is not used, so at worst a longer
32
+ # equation than necessary is found.
33
+ #
34
+ # Sources (keys: MANUAL.md, Sources): [Koe92] for the algorithm, [Koe14,
35
+ # ch. 10] for the presentation with hypergeometric terms; the coefficient
36
+ # recurrence of a holonomic equation is textbook, [Sta99, ch. 6].
37
+ module FPS
38
+ MAX_ORDER = 4 # order of the differential equation looked for
39
+ MAX_DEGREE = 4 # degree of its polynomial coefficients
40
+ LOOKAHEAD = 2 # coefficients checked past the first one of each class
41
+ MAX_START = 24 # highest index taken from the Taylor polynomial
42
+
43
+ module_function
44
+
45
+ # The formal power series of f around x = a, or nil when there is none of
46
+ # hypergeometric type within the bounds.
47
+ def expansion(f, x, a = 0, max_order: MAX_ORDER, max_degree: MAX_DEGREE)
48
+ f = Expression.lift(f)
49
+ x = Expression.lift(x)
50
+ a = Expression.lift(a)
51
+ raise ArgumentError, "fps: the variable must be a symbol, got #{x}" unless x.is_a?(Var)
52
+ raise ArgumentError, "fps: expansion point #{a} depends on #{x}" if depends?(a, x)
53
+ local = Scalar.zero?(a) ? f : f.subs(x => x + a)
54
+ result = at_zero(local, x, max_order, max_degree)
55
+ return nil if result.nil?
56
+ Scalar.zero?(a) ? result : result.subs(x => x - a)
57
+ end
58
+
59
+ # f = sum_k a(k)*x**k around 0.
60
+ def at_zero(f, x, max_order, max_degree)
61
+ f = f.simplify
62
+ return f if polynomial?(f, x) # a polynomial is its own power series
63
+ k = Var.new(index_for(f, x))
64
+ search(f, x, max_order, max_degree) do |ps|
65
+ shifts = recurrence(ps, x, k)
66
+ shifts && assemble(shifts, f, x, k)
67
+ end
68
+ end
69
+
70
+ # ---- 1. the differential equation --------------------------------------
71
+
72
+ # The coefficients [p_0, ..., p_J] of a differential equation
73
+ # sum_j p_j(x)*f^(j)(x) = 0 of smallest order, or nil. The order and the
74
+ # degree of the p_j both grow until one is found.
75
+ def differential_equation(f, x, max_order: MAX_ORDER, max_degree: MAX_DEGREE)
76
+ f = Expression.lift(f)
77
+ x = Expression.lift(x)
78
+ search(f.simplify, x, max_order, max_degree) { |ps| ps }
79
+ end
80
+
81
+ # Every candidate equation, smallest order and degree first, until the
82
+ # block returns something.
83
+ def search(f, x, max_order, max_degree)
84
+ derivatives = [f]
85
+ rows = [parts(f, x)]
86
+ return nil if rows.first.empty? # f = 0
87
+ (1..max_order).each do |order|
88
+ begin
89
+ derivatives << derivatives.last.diff(x).simplify
90
+ rescue ArgumentError, NotImplementedError, RCAS::Unsupported
91
+ return nil
92
+ end
93
+ rows << parts(derivatives.last, x)
94
+ (0..max_degree).each do |degree|
95
+ candidates = equations(rows, x, order, degree)
96
+ candidates.each do |ps|
97
+ result = yield(ps)
98
+ return result if result
99
+ end
100
+ # x**i times an equation of this order is an equation again, and its
101
+ # recurrence is the same one shifted: a higher degree adds nothing
102
+ # once this order has an equation.
103
+ break unless candidates.empty?
104
+ end
105
+ end
106
+ nil
107
+ end
108
+
109
+ # The equations of this order and degree, as a basis of the solutions of
110
+ # the linear system for the coefficients of the p_j.
111
+ def equations(rows, x, order, degree)
112
+ unknowns = (0..order).map { |j| (0..degree).map { |i| Var.new(:"_p#{j}_#{i}") } }
113
+ equations = conditions(rows.first(order + 1), unknowns, x, degree) or return []
114
+ return [] if equations.empty?
115
+ width = degree + 1
116
+ kernel(equations, unknowns.flatten).filter_map do |vector|
117
+ ps = unknowns.each_index.map do |j|
118
+ vector[j * width, width].each_with_index.reduce(Num.new(0)) do |acc, (c, i)|
119
+ acc + c * Simplify.power_node(x, i)
120
+ end.simplify
121
+ end
122
+ ps unless Scalar.zero?(ps.last)
123
+ end
124
+ end
125
+
126
+ # The null space of a homogeneous linear system, as vectors of
127
+ # coefficients. With a parameter among them the entries are polynomials in
128
+ # it, and PolyMatrix does that by evaluation and interpolation, where an
129
+ # elimination over the rational functions would swell (the same reason
130
+ # Zeilberger uses it); a cancelling elimination is the fallback.
131
+ def kernel(equations, unknowns)
132
+ entries = equations.map do |equation|
133
+ unknowns.map { |u| Solve.polynomial_coefficients(equation, u)&.at(1) || Num.new(0) }
134
+ end
135
+ vectors = PolyMatrix.kernel(entries)
136
+ return vectors if vectors
137
+ solution = QSummation.linear_solve(equations, unknowns) or return []
138
+ free = unknowns - solution.keys
139
+ free.map do |u|
140
+ point = free.to_h { |v| [v, Num.new(v == u ? 1 : 0)] }
141
+ unknowns.map { |v| (solution[v] || v).subs(point).simplify }
142
+ end
143
+ end
144
+
145
+ # sum_j p_j(x)*f^(j)(x) = 0 has to hold monomial by monomial; each
146
+ # monomial gives a rational function of x whose numerator must vanish,
147
+ # and that numerator is linear in the coefficients of the p_j.
148
+ def conditions(rows, unknowns, x, degree)
149
+ keys = rows.flat_map(&:keys).uniq
150
+ keys.flat_map do |key|
151
+ pairs = rows.each_with_index.filter_map { |row, j| row[key] && [j, row[key]] }
152
+ numerators = cleared(pairs, x) or return nil
153
+ expression = numerators.reduce(Num.new(0)) do |acc, (j, numerator)|
154
+ acc + polynomial(unknowns[j], x, degree) * numerator
155
+ end
156
+ Solve.polynomial_coefficients(expression.expand, x) or return nil
157
+ end
158
+ end
159
+
160
+ def polynomial(coefficients, x, degree)
161
+ (0..degree).reduce(Num.new(0)) { |acc, i| acc + coefficients[i] * Simplify.power_node(x, i) }
162
+ end
163
+
164
+ # The coefficients of one monomial over their common denominator. Every
165
+ # term of a derivative is a product of powers, so its denominator can be
166
+ # read off; only the few that occur go into the polynomial ring, for one
167
+ # lcm and one division each. (Fraction.as_fraction on the assembled
168
+ # coefficient builds the common denominator by multiplying instead, and
169
+ # the third derivative of a rational function already has fifty terms.)
170
+ def cleared(pairs, x)
171
+ pieces = pairs.map { |j, terms| [j, terms.map { |factors, coeff| fraction_of(factors, coeff) }] }
172
+ denominators = pieces.flat_map { |_, terms| terms.map(&:last) }.uniq
173
+ ring = QQ[*denominators.reduce([x.name]) { |acc, d| acc | d.variables }]
174
+ polynomials = denominators.to_h { |d| [d, ring.call(d)] }
175
+ total = polynomials.values.reduce { |acc, d| acc.exact_div(acc.gcd(d)) * d }
176
+ quotients = polynomials.transform_values { |d| total.exact_div(d).to_expr }
177
+ pieces.map do |j, terms|
178
+ [j, terms.reduce(Num.new(0)) { |acc, (numerator, denominator)| acc + numerator * quotients[denominator] }]
179
+ end
180
+ rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
181
+ structural(pairs)
182
+ end
183
+
184
+ # [numerator, denominator] of one term of a term table.
185
+ def fraction_of(factors, coeff)
186
+ numerator = Num.new(coeff)
187
+ denominator = Num.new(1)
188
+ factors.each do |base, e|
189
+ if e.is_a?(Integer) && e.negative?
190
+ denominator *= Simplify.power_node(base, -e)
191
+ else
192
+ numerator *= Simplify.power_node(base, e)
193
+ end
194
+ end
195
+ [numerator, denominator.simplify]
196
+ end
197
+
198
+ # Without a polynomial ring (a constant such as pi in a denominator):
199
+ # multiply by the powers that occur, which clears them too, if less tidily.
200
+ def structural(pairs)
201
+ powers = {}
202
+ pairs.each do |_, terms|
203
+ terms.each do |factors, _|
204
+ factors.each { |base, e| powers[base] = [powers[base] || 0, -e].max if e.is_a?(Integer) && e.negative? }
205
+ end
206
+ end
207
+ denominator = powers.reduce(Num.new(1)) { |acc, (base, e)| acc * Simplify.power_node(base, e) }
208
+ pairs.map { |j, terms| [j, (coefficient_of(terms) * denominator).simplify] }
209
+ end
210
+
211
+ def coefficient_of(terms)
212
+ terms.reduce(Num.new(0)) do |acc, (factors, coeff)|
213
+ acc + factors.reduce(Num.new(coeff)) { |value, (base, e)| value * Simplify.power_node(base, e) }
214
+ end
215
+ end
216
+
217
+ # An expression split by its transcendental part:
218
+ # { {atom => exponent} => [[rational factors, numeric coefficient], ...] }.
219
+ # exp(x)/(1 - x)**2 - x*exp(2*x) has the two keys {e => x} and {e => 2*x}.
220
+ def parts(expr, x)
221
+ constant, table = Expand.table(cancelled(expr))
222
+ out = Hash.new { |h, key| h[key] = [] }
223
+ out[{}] << [{}, constant] unless constant.zero?
224
+ table.each do |factors, coeff|
225
+ rational = {}
226
+ atoms = {}
227
+ factors.each { |base, exponent| split(base, exponent, x, rational, atoms) }
228
+ out[atoms] << [rational, coeff]
229
+ end
230
+ out
231
+ end
232
+
233
+ # base**exponent into the rational part, the atom part, or both: a power
234
+ # of a polynomial splits, (1 - x**2)**(-3/2) into (1 - x**2)**(-2) and
235
+ # (1 - x**2)**(1/2), so that a derivative and its successor share an atom.
236
+ def split(base, exponent, x, rational, atoms)
237
+ if !depends?(base, x) && !depends?(exponent, x)
238
+ rational[base] = exponent
239
+ elsif rational?(base)
240
+ whole, rest = integer_part(exponent)
241
+ rational[base] = whole unless whole.zero?
242
+ atoms[base] = rest if rest
243
+ else
244
+ atoms[base] = exponent
245
+ end
246
+ end
247
+
248
+ # [integer part, remainder or nil] of an exponent.
249
+ def integer_part(exponent)
250
+ exponent = exponent.value if exponent.is_a?(Num)
251
+ case exponent
252
+ when Integer then [exponent, nil]
253
+ when Rational then [exponent.floor, exponent - exponent.floor]
254
+ when Expression
255
+ constant, = Expand.table(exponent)
256
+ whole = constant.is_a?(Integer) || constant.is_a?(Rational) ? constant.floor : 0
257
+ [whole, whole.zero? ? exponent : (exponent - Num.new(whole)).simplify]
258
+ else [0, exponent]
259
+ end
260
+ end
261
+
262
+ # A rational function of its variables: no function call, no root.
263
+ def rational?(expr)
264
+ expr.each_node.all? do |node|
265
+ case node
266
+ when Var, Num, Add, Sub, Mul, Div, Neg then true
267
+ when Pow then node.exponent.is_a?(Num) && node.exponent.value.is_a?(Integer)
268
+ else false
269
+ end
270
+ end
271
+ end
272
+
273
+ def depends?(expr, x) = expr.is_a?(Expression) && expr.variables.include?(x.name)
274
+
275
+ def polynomial?(f, x) = !Solve.polynomial_coefficients(f.expand, x).nil?
276
+
277
+ def cancelled(expr)
278
+ value = expr.cancel
279
+ value.is_a?(Expression) ? value : expr
280
+ rescue StandardError => rescued
281
+ RCAS.guard!(rescued)
282
+ expr
283
+ end
284
+
285
+ # ---- 2. the recurrence for the coefficients -----------------------------
286
+
287
+ # sum_j p_j(x)*f^(j)(x) = 0 with f = sum_k a(k)*x**k gives, for the
288
+ # coefficient of x**k, sum_s q_s(k)*a(k + s) = 0 (valid for k >= 0, with
289
+ # a(k) = 0 for negative k): x**i*f^(j) contributes a(k + j - i) with the
290
+ # falling factorial (k - i + j)...(k - i + 1).
291
+ # => { shift => polynomial in k }, or nil.
292
+ def recurrence(ps, x, k)
293
+ shifts = {}
294
+ ps.each_with_index do |p, j|
295
+ coefficients = Solve.polynomial_coefficients(p.expand, x) or return nil
296
+ coefficients.each_with_index do |c, i|
297
+ next if Scalar.zero?(c)
298
+ falling = (1..j).reduce(Num.new(1)) { |acc, t| acc * (k - Num.new(i) + Num.new(t)) }
299
+ shifts[j - i] = (shifts[j - i] || Num.new(0)) + c * falling
300
+ end
301
+ end
302
+ shifts.transform_values { |q| q.expand.simplify }.reject { |_, q| Scalar.zero?(q) }
303
+ end
304
+
305
+ # ---- 3. and 4. the series ----------------------------------------------
306
+
307
+ # Two terms q(k)*a(k) + r(k)*a(k + m) = 0 make the coefficients m-fold
308
+ # symmetric hypergeometric: one geometric-like ratio per residue class
309
+ # modulo m. Anything else is left to the caller (nil).
310
+ def assemble(shifts, f, x, k)
311
+ return nil unless shifts.size == 2
312
+ low, high = shifts.keys.minmax
313
+ m = high - low
314
+ q = shifts[low].subs(k => k - Num.new(low)).expand
315
+ r = shifts[high].subs(k => k - Num.new(low)).expand
316
+ ratio = (Simplify.negate(q) / r).cancel
317
+ valid = [low, 0].max # the shifted recurrence holds from here on
318
+
319
+ ratios = (0...m).to_h { |residue| [residue, ratio.subs(k => Num.new(m) * k + Num.new(residue)).cancel] }
320
+ # the roots of r before cancelling: (k - 1)*(a(k) - a(k - 1)) = 0
321
+ # says nothing about a(1), and the cancelled ratio 1 forgot that, so
322
+ # x/(1 - x) came out as 0 (third review, D5)
323
+ leads = (0...m).to_h { |residue| [residue, r.subs(k => Num.new(m) * k + Num.new(residue)).expand] }
324
+ starts = ratios.to_h { |residue, step| [residue, start_index(step, k, valid, m, residue, leads[residue])] }
325
+ known = starts.map { |residue, start| m * (start + LOOKAHEAD) + residue }.max
326
+ return nil if known > MAX_START
327
+ taylor = begin
328
+ Limits.taylor(f, x, 0, known + 1)
329
+ rescue SeriesError, ZeroDivisionError, NotImplementedError, RCAS::Unsupported
330
+ return nil
331
+ end
332
+
333
+ classes = ratios.map { |residue, step| piece(step, taylor, starts[residue], residue, m, x, k) }
334
+ return nil if classes.any? { |piece| piece == false }
335
+ total = classes.compact.map(&:first).reduce(leading_terms(taylor, starts, m, x)) { |acc, s| acc + s }
336
+ total.simplify
337
+ end
338
+
339
+ # [the sum for one residue class, its general term], nil when the class
340
+ # vanishes (a(k0) = 0 makes every later coefficient of the class zero too,
341
+ # the ratio having no pole past the first index), and false when no form
342
+ # of the coefficient checks out.
343
+ #
344
+ # Tidying can put the coefficient out of reach of its own first index:
345
+ # (2*k - 2)! is the (1/2 + k) gamma of sqrt(1 + x) written with
346
+ # factorials, and it has a pole at k = 0 where the gamma had none. The
347
+ # plain form of the product is then the one to keep.
348
+ def piece(step, taylor, start, residue, m, x, k)
349
+ first = Coefficients.coeff(taylor, x, m * start + residue)
350
+ return nil if Scalar.zero?(first)
351
+ i = Var.new(:_j)
352
+ product = Products.product(step.subs(k => i), i, Num.new(start), (k - Num.new(1)).simplify)
353
+ plain = (first * product).simplify
354
+ term = candidates(plain, k, m).find { |candidate| agrees?(candidate, taylor, start, residue, m, x, k) }
355
+ return false unless term
356
+ power = Simplify.power_node(x, (Num.new(m) * k + Num.new(residue)).simplify)
357
+ [Sum.new((term * power).simplify, k, Num.new(start), OO), term]
358
+ end
359
+
360
+ # The forms of the coefficient to offer, best first: factorials if the
361
+ # gammas all came out in the wash, then binomial coefficients (which is
362
+ # where (1 + x)**a and sqrt(1 + x) belong), then the product as it came.
363
+ def candidates(plain, k, m)
364
+ forms = []
365
+ factorials = tidy(plain, k, m)
366
+ forms << factorials unless gamma?(factorials)
367
+ forms << tidy(plain, k, m, binomials: true)
368
+ forms << plain
369
+ forms.uniq { |form| form.to_s }
370
+ end
371
+
372
+ def gamma?(expr) = expr.each_node.any? { |node| node.is_a?(Fn) && node.name == :gamma }
373
+
374
+ # The coefficients below the first index of their class, from the Taylor
375
+ # polynomial: log(1 + x) starts its sum at k = 1, exp(x) - 1 - x at k = 2.
376
+ def leading_terms(taylor, starts, m, x)
377
+ (0...starts.map { |residue, start| m * start + residue }.max).reduce(Num.new(0)) do |acc, degree|
378
+ residue = degree % m
379
+ next acc unless degree < m * starts[residue] + residue
380
+ acc + Coefficients.coeff(taylor, x, degree) * Simplify.power_node(x, degree)
381
+ end
382
+ end
383
+
384
+ # The linear algebra behind the closed form is exact, so this is a check
385
+ # on the derivation, not on the arithmetic: the first coefficients of the
386
+ # class against the Taylor polynomial.
387
+ def agrees?(term, taylor, start, residue, m, x, k)
388
+ (start..start + LOOKAHEAD).all? do |t|
389
+ value = term.subs(k => Num.new(t)).simplify
390
+ difference = (opened(value) - Coefficients.coeff(taylor, x, m * t + residue)).expand
391
+ Scalar.zero?(difference) || Scalar.zero?(difference.cancel) || Decide.identically_zero?(difference) == true
392
+ end
393
+ rescue ZeroDivisionError, NotImplementedError, RCAS::Unsupported, DomainError
394
+ false
395
+ end
396
+
397
+ # binomial(a, 2) is a*(a - 1)/2 for the comparison. The shape is the point
398
+ # of the answer, so nothing else opens it up.
399
+ def opened(expr)
400
+ expr = expr.map_children { |child| opened(child) }
401
+ return expr unless expr.is_a?(Fn) && expr.name == :binomial
402
+ Combinatorics.expand_binomial(*expr.args) || expr
403
+ end
404
+
405
+ # The first index of the class: past the point where the recurrence takes
406
+ # hold, and past every non-negative integer root of the denominator of the
407
+ # ratio, where it says nothing about the next coefficient. A denominator
408
+ # with a parameter in it has no roots to speak of, and the generic answer
409
+ # is the one given (the sampled check still has to pass).
410
+ def start_index(step, k, valid, m, residue, lead = nil)
411
+ start = [((valid - residue) / m.to_r).ceil, 0].max
412
+ pair = Fraction.as_fraction(step, [k.name])
413
+ roots = pair ? (integer_roots(pair.last, k) || []) : []
414
+ if lead && (poly = lead_polynomial(lead, k))
415
+ roots += integer_roots(poly, k) || []
416
+ end
417
+ roots.empty? ? start : [start, roots.max + 1].max
418
+ end
419
+
420
+ def lead_polynomial(expr, k)
421
+ return nil unless expr.variables.include?(k.name)
422
+ Polynomial.from_expr(QQ[k.name, *(expr.variables - [k.name])], expr)
423
+ rescue DomainError, NotImplementedError, RCAS::Unsupported, ArgumentError
424
+ nil
425
+ end
426
+
427
+ # The non-negative integer roots of a polynomial, or nil when its
428
+ # coefficients are not numbers (a parameter could make anything a root).
429
+ def integer_roots(poly, k)
430
+ coefficients = (0..poly.degree(k.name)).map { |i| poly.coefficient_in(k.name, i).to_expr }
431
+ return nil unless coefficients.all? { |c| c.variables.empty? }
432
+ Solve.polynomial_roots(coefficients).map(&:simplify)
433
+ .select { |root| root.is_a?(Num) && root.value.is_a?(Integer) && !root.value.negative? }
434
+ .map(&:value)
435
+ rescue NotImplementedError, RCAS::Unsupported, DomainError, ZeroDivisionError
436
+ nil
437
+ end
438
+
439
+ # ---- the shape of the coefficient --------------------------------------
440
+
441
+ # Products.product gives one gamma per linear factor of the ratio, so the
442
+ # coefficient of an m-fold symmetric series comes back as gammas at m
443
+ # consecutive m-ths: gamma(1 + k)*gamma(3/2 + k) where (2*k + 1)! is
444
+ # meant. Gauss's multiplication formula
445
+ #
446
+ # gamma(z)*gamma(z + 1/m)*...*gamma(z + (m - 1)/m)
447
+ # = (2*pi)**((m - 1)/2)*m**(1/2 - m*z)*gamma(m*z) [AS64, 6.1.20]
448
+ #
449
+ # puts them back together, and the numeric powers of k that are left over
450
+ # (and those the ratio brought with it) are collected into one base.
451
+ def tidy(term, k, m, binomials: false)
452
+ coeff, factors = Simplify.factorize(term)
453
+ factors = factors.reject { |_, e| e.is_a?(Numeric) && e.zero? }
454
+ extra = merge_gammas(factors, k, m, binomials)
455
+ extra *= clear_contents(factors)
456
+ shorter(collect_powers((Simplify.rebuild_product(coeff, factors) * extra).simplify, k))
457
+ rescue StandardError => rescued
458
+ RCAS.guard!(rescued)
459
+ term
460
+ end
461
+
462
+ # Cancelling turns 1/(2*(1/2 + k)) into 1/(1 + 2*k) and is worth it when
463
+ # it collapses the coefficient, not when it only spreads it out.
464
+ def shorter(term)
465
+ cancelled = term.cancel
466
+ cancelled.to_s.length < term.to_s.length ? cancelled : term
467
+ rescue StandardError => rescued
468
+ RCAS.guard!(rescued)
469
+ term
470
+ end
471
+
472
+ # factorial(u) is gamma(u + 1): the formula needs one currency.
473
+ def as_gammas(factors)
474
+ out = {}
475
+ factors.each do |base, e|
476
+ if base.is_a?(Fn) && base.name == :factorial
477
+ Simplify.add_factor(out, Fn.new(:gamma, [(base.args.first + Num.new(1)).simplify]), e)
478
+ else
479
+ Simplify.add_factor(out, base, e)
480
+ end
481
+ end
482
+ out
483
+ end
484
+
485
+ # Mutates +factors+, returns the constant the formula brings with it.
486
+ def merge_gammas(factors, k, m, binomials)
487
+ factors.replace(as_gammas(factors))
488
+ extra = Num.new(1)
489
+ m.times do
490
+ break unless m > 1
491
+ group = gamma_group(factors, m) or break
492
+ z, exponent = group
493
+ (0...m).each { |j| Simplify.add_factor(factors, gamma_at(z, j, m), -exponent) }
494
+ Simplify.add_factor(factors, Fn.new(:gamma, [(Num.new(m) * z).simplify]), exponent)
495
+ extra *= Simplify.power_node(multiplication_constant(z, m), exponent)
496
+ end
497
+ merge_shifts(factors)
498
+ extra *= to_binomials(factors, k) if binomials
499
+ extra *= expand_halves(factors, k)
500
+ merge_shifts(factors)
501
+ factors.reject! { |_, e| e.is_a?(Numeric) && e.zero? }
502
+ to_factorials(factors, k)
503
+ extra
504
+ end
505
+
506
+ # The constant in Gauss's formula for the group starting at z.
507
+ def multiplication_constant(z, m)
508
+ (Simplify.power_node(Num.new(2) * PI, Rational(m - 1, 2)) *
509
+ Simplify.power_node(Num.new(m), (Num.new(Rational(1, 2)) - Num.new(m) * z).simplify)).simplify
510
+ end
511
+
512
+ # A gamma left at a fractional shift, whose partners in the formula are at
513
+ # integer ones: solved for it, the formula turns gamma(1/2 + k) into
514
+ # (2*k)!*pi**(1/2)/(4**k*k!), which is the form a table has. The number of
515
+ # partners is the denominator of the shift, not the modulus of the series.
516
+ def expand_halves(factors, k)
517
+ extra = Num.new(1)
518
+ factors.keys.each do |base|
519
+ next unless base.is_a?(Fn) && base.name == :gamma
520
+ exponent = factors[base]
521
+ next unless exponent.is_a?(Integer) && !exponent.zero?
522
+ z = base.args.first
523
+ d = shift_denominator(z, k)
524
+ next unless d
525
+ partners = (1...d).map { |j| (z + Num.new(Rational(j, d))).simplify }
526
+ next unless partners.all? { |argument| integral?((argument - Num.new(1)).simplify, k) }
527
+ Simplify.add_factor(factors, base, -exponent)
528
+ Simplify.add_factor(factors, Fn.new(:gamma, [(Num.new(d) * z).simplify]), exponent)
529
+ partners.each { |argument| Simplify.add_factor(factors, Fn.new(:gamma, [argument]), -exponent) }
530
+ extra *= Simplify.power_node(multiplication_constant(z, d), exponent)
531
+ end
532
+ extra
533
+ end
534
+
535
+ # The denominator of the constant part of a gamma argument that is
536
+ # otherwise integral in k: 2 for 1/2 + k, nil for k - a or 1 + k.
537
+ def shift_denominator(z, k)
538
+ constant, table = Expand.table(z)
539
+ return nil unless constant.is_a?(Rational) && constant.denominator > 1
540
+ return nil unless integral?((z - Num.new(constant)).simplify, k) && !table.empty?
541
+ constant.denominator
542
+ end
543
+
544
+ # gamma(b + k)/(gamma(b)*k!) is binomial(b + k - 1, k), which upper
545
+ # negation [GKP94, (5.14)] writes as (-1)**k*binomial(-b, k): with b = -a
546
+ # that is the binomial series of (1 + x)**a, where this shape comes from.
547
+ def to_binomials(factors, k)
548
+ extra = Num.new(1)
549
+ one = Fn.new(:gamma, [(k + Num.new(1)).simplify]) # k! in the currency of as_gammas
550
+ return extra unless factors[one] == -1
551
+ factors.keys.each do |base|
552
+ next unless base.is_a?(Fn) && base.name == :gamma && factors[base] == 1
553
+ b = (base.args.first - k).simplify
554
+ next if depends?(b, k)
555
+ Simplify.add_factor(factors, base, -1)
556
+ Simplify.add_factor(factors, one, 1)
557
+ Simplify.add_factor(factors, Fn.new(:binomial, [Simplify.negate(b).simplify, k]), 1)
558
+ extra *= RCAS.gamma(b) * Simplify.power_node(Num.new(-1), k)
559
+ break
560
+ end
561
+ extra
562
+ end
563
+
564
+ # gamma(z + n)/gamma(z) = z*(z + 1)*...*(z + n - 1) for an integer n > 0:
565
+ # what Combinatorics.merge_factorials does for factorials, and what makes
566
+ # gamma(1/2 + k)/gamma(3/2 + k) the 1/(1 + 2*k) it is.
567
+ def merge_shifts(factors, limit = 8)
568
+ loop do
569
+ pair = shifted_pair(factors, limit) or return factors
570
+ big, small, distance = pair
571
+ exponent = factors.delete(big)
572
+ Simplify.add_factor(factors, small, exponent)
573
+ (0...distance).each { |i| Simplify.add_factor(factors, (small.args.first + Num.new(i)).simplify, exponent) }
574
+ end
575
+ end
576
+
577
+ def shifted_pair(factors, limit)
578
+ gammas = factors.select { |base, e| base.is_a?(Fn) && base.name == :gamma && e.is_a?(Integer) && !e.zero? }.keys
579
+ gammas.combination(2).each do |a, b|
580
+ distance = (a.args.first - b.args.first).simplify
581
+ next unless distance.is_a?(Num) && distance.value.is_a?(Integer) && !distance.value.zero? && distance.value.abs <= limit
582
+ return distance.value.positive? ? [a, b, distance.value] : [b, a, -distance.value]
583
+ end
584
+ nil
585
+ end
586
+
587
+ # (1/2 + k) is (1 + 2*k)/2: a linear factor with a rational constant hides
588
+ # the 1 + 2*k that belongs in the answer.
589
+ def clear_contents(factors)
590
+ extra = Num.new(1)
591
+ factors.keys.each do |base|
592
+ exponent = factors[base]
593
+ next unless (base.is_a?(Add) || base.is_a?(Sub)) && exponent.is_a?(Integer) && !exponent.zero?
594
+ constant, table = Expand.table(base)
595
+ values = [constant] + table.values
596
+ next unless values.all? { |v| v.is_a?(Integer) || v.is_a?(Rational) }
597
+ scale = values.map { |v| Rational(v).denominator }.reduce(1, :lcm)
598
+ next if scale == 1
599
+ factors.delete(base)
600
+ Simplify.add_factor(factors, (base * Num.new(scale)).expand.simplify, exponent)
601
+ extra *= Simplify.power_node(Num.new(Rational(1, scale)), exponent)
602
+ end
603
+ extra
604
+ end
605
+
606
+ def gamma_at(z, j, m) = Fn.new(:gamma, [(z + Num.new(Rational(j, m))).simplify])
607
+
608
+ # The first m gammas at consecutive m-ths, as [z, exponent], or nil.
609
+ def gamma_group(factors, m)
610
+ factors.each do |base, exponent|
611
+ next unless base.is_a?(Fn) && base.name == :gamma && exponent.is_a?(Integer) && !exponent.zero?
612
+ z = base.args.first
613
+ next unless (1...m).all? { |j| factors[gamma_at(z, j, m)] == exponent }
614
+ return [z, exponent]
615
+ end
616
+ nil
617
+ end
618
+
619
+ # gamma(2 + 2*k) is (1 + 2*k)!, which is what a reader expects to see.
620
+ def to_factorials(factors, k)
621
+ factors.keys.each do |base|
622
+ next unless base.is_a?(Fn) && base.name == :gamma
623
+ argument = (base.args.first - Num.new(1)).simplify
624
+ next unless integral?(argument, k)
625
+ exponent = factors.delete(base)
626
+ Simplify.add_factor(factors, Fn.new(:factorial, [argument]), exponent)
627
+ end
628
+ factors
629
+ end
630
+
631
+ # An expression that takes integer values at integer points.
632
+ def integral?(expr, k)
633
+ constant, table = Expand.table(expr)
634
+ return false unless constant.is_a?(Integer)
635
+ table.all? do |factors, coeff|
636
+ coeff.is_a?(Integer) && factors.all? { |base, e| base == k && e.is_a?(Integer) && e.positive? }
637
+ end
638
+ end
639
+
640
+ # (-1/4)**k*2**k*2**(3/2 + k) is one power of k.
641
+ def collect_powers(expr, k)
642
+ coeff, factors = Simplify.factorize(expr)
643
+ base = 1r
644
+ rest = {}
645
+ factors.each do |b, e|
646
+ slope = numeric?(b) ? slope_in(e, k) : nil
647
+ if slope && !slope.zero? && !b.value.zero?
648
+ base *= Rational(b.value)**slope
649
+ Simplify.add_factor(rest, b, Simplify.exponent_value((Expression.lift(e) - Num.new(slope) * k).simplify))
650
+ else
651
+ Simplify.add_factor(rest, b, e)
652
+ end
653
+ end
654
+ Simplify.add_factor(rest, Num.new(Simplify.normalize_number(base)), k) unless base == 1
655
+ Simplify.rebuild_product(coeff, rest.reject { |_, e| e.is_a?(Numeric) && e.zero? }).simplify
656
+ end
657
+
658
+ def numeric?(b) = b.is_a?(Num) && (b.value.is_a?(Integer) || b.value.is_a?(Rational))
659
+
660
+ # The coefficient of k in an exponent, when it is an integer.
661
+ def slope_in(exponent, k)
662
+ return nil unless exponent.is_a?(Expression)
663
+ coefficients = Solve.polynomial_coefficients(exponent.expand, k)
664
+ return nil unless coefficients && coefficients.size == 2
665
+ slope = coefficients.last
666
+ slope.is_a?(Num) && slope.value.is_a?(Integer) ? slope.value : nil
667
+ end
668
+
669
+ # ---- helpers -----------------------------------------------------------
670
+
671
+ # A summation index that is not in use.
672
+ def index_for(f, x)
673
+ taken = f.variables + [x.name]
674
+ return :k unless taken.include?(:k)
675
+ (1..).each { |i| return :"k#{i}" unless taken.include?(:"k#{i}") }
676
+ end
677
+ end
678
+ end