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,532 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Unevaluated sum.
|
|
5
|
+
class Sum < Expression
|
|
6
|
+
attr_reader :term, :var, :from, :to
|
|
7
|
+
|
|
8
|
+
def initialize(term, var, from, to)
|
|
9
|
+
@term = term
|
|
10
|
+
@var = var
|
|
11
|
+
@from = from
|
|
12
|
+
@to = to
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def bound_variable = var
|
|
17
|
+
def children = [term, var, from, to]
|
|
18
|
+
def rebuild(term, var, from, to) = Sum.new(term, var, from, to)
|
|
19
|
+
def to_sexp = [:sum, term.to_sexp, var.to_sexp, from.to_sexp, to.to_sexp]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Symbolic summation.
|
|
23
|
+
#
|
|
24
|
+
# sum(k, k, 1, n) # => n/2 + n**2/2
|
|
25
|
+
# sum(k**2, k, 1, n) # => n/6 + n**2/2 + n**3/3
|
|
26
|
+
# sum(2**k, k, 0, n) # => -1 + 2*2**n
|
|
27
|
+
# sum(1/(k*(k + 1)), k, 1, oo) # => 1
|
|
28
|
+
#
|
|
29
|
+
# Polynomials in k get their closed form directly; other terms go through
|
|
30
|
+
# Gosper's algorithm, which finds an antidifference whenever the term is
|
|
31
|
+
# hypergeometric and one exists. A definite sum in one other variable is
|
|
32
|
+
# then tried with creative telescoping (zeilberger.rb): the recurrence it
|
|
33
|
+
# yields is solved and the answer checked against the sum itself. Anything
|
|
34
|
+
# else stays an unevaluated Sum.
|
|
35
|
+
#
|
|
36
|
+
# Sources (keys: MANUAL.md, Sources): power sums by Newton interpolation
|
|
37
|
+
# and Bernoulli numbers, zeta(2m) [GKP94, §6.5]; Euler-Maclaurin tail for
|
|
38
|
+
# zeta(s) numerically [GKP94, §9.5]; Gosper [Gos78] with the degree bound
|
|
39
|
+
# for the polynomial ansatz from [PWZ96, ch. 5]; reversed bounds by Karr's
|
|
40
|
+
# convention [Kar81].
|
|
41
|
+
module Summation
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def sum(f, k, from, to)
|
|
45
|
+
f = Expression.lift(f).simplify
|
|
46
|
+
k = Expression.lift(k)
|
|
47
|
+
from = Expression.lift(from)
|
|
48
|
+
to = Expression.lift(to)
|
|
49
|
+
whole_bounds!(from, to)
|
|
50
|
+
if (reversed = karr(from, to))
|
|
51
|
+
# Karr's convention [Kar81]: sum_{a}^{b} = -sum_{b+1}^{a-1} for
|
|
52
|
+
# b < a - 1, and 0 for the empty b = a - 1 - the one under which a
|
|
53
|
+
# closed form holds at every integer and additivity has no
|
|
54
|
+
# conditions, and what the closed forms computed already (the fifth
|
|
55
|
+
# review's decision; the direct sum said 0)
|
|
56
|
+
return Num.new(0) if reversed == :empty
|
|
57
|
+
return Neg.new(sum(f, k, reversed.first, reversed.last)).simplify
|
|
58
|
+
end
|
|
59
|
+
return UNDEFINED if pole_in_range?(f, k, from, to)
|
|
60
|
+
# a fixed pole past the lower bound is inside the range for every upper
|
|
61
|
+
# bound beyond it: sum(1/((k - 3)*(k - 2)), k, 0, n) has no value from
|
|
62
|
+
# n = 2 on, and the telescoped closed form was finite there (fourth
|
|
63
|
+
# review, S4); the sum stays formal and says so at any number
|
|
64
|
+
return Sum.new(f, k, from, to) if pole_past_start?(f, k, from, to)
|
|
65
|
+
hypergeometric_form(summed(f, k, from, to), to)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pole_past_start?(f, k, from, to)
|
|
69
|
+
return false unless from.is_a?(Num) && from.value.is_a?(Integer)
|
|
70
|
+
return false if to.variables.empty? || to.variables.include?(k.name)
|
|
71
|
+
integer_poles(f, k).any? { |p| p >= from.value }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def integer_poles(f, k)
|
|
75
|
+
Analysis.denominators(f, k).flat_map do |d|
|
|
76
|
+
roots = begin
|
|
77
|
+
Solve.solve(d, k, domain: RR)
|
|
78
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
79
|
+
RCAS.guard!(rescued, refused: true) if rescued.is_a?(StandardError)
|
|
80
|
+
next []
|
|
81
|
+
end
|
|
82
|
+
next [] unless roots.is_a?(Array)
|
|
83
|
+
roots.filter_map { |r| r.value if r.is_a?(Num) && r.value.is_a?(Integer) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [b + 1, a - 1] for integer bounds a > b + 1, :empty for b = a - 1, nil
|
|
88
|
+
# for an ordinary range or bounds that are not whole numbers.
|
|
89
|
+
def karr(from, to)
|
|
90
|
+
difference = (to - from).simplify
|
|
91
|
+
return nil unless difference.is_a?(Num) && difference.value.is_a?(Integer) && difference.value.negative?
|
|
92
|
+
return :empty if difference.value == -1
|
|
93
|
+
return nil unless from.is_a?(Num) && to.is_a?(Num)
|
|
94
|
+
[(to + 1).simplify, (from - 1).simplify]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A sum runs over integers: sum(k, k, 1.5, 3) was 5.625, which is
|
|
98
|
+
# Faulhaber at a bound that is not one (third review, S11).
|
|
99
|
+
def whole_bounds!(from, to)
|
|
100
|
+
[from, to].each do |b|
|
|
101
|
+
next unless b.is_a?(Num) && b.value.is_a?(Numeric) && b.value.real?
|
|
102
|
+
next if b.value == b.value.round
|
|
103
|
+
raise ArgumentError, "sum: the bounds must be integers, got #{b}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A term with no value inside the range leaves the sum without one:
|
|
108
|
+
# sum(1/(k*(k + 1)), k, -1, 3) telescoped straight across k = -1 and
|
|
109
|
+
# k = 0 to -5/4 (third review, S4, the discrete twin of the poles an
|
|
110
|
+
# integral is split at). Only integer poles count, and only against
|
|
111
|
+
# numeric bounds; a symbolic upper bound is left to the rest.
|
|
112
|
+
def pole_in_range?(f, k, from, to)
|
|
113
|
+
return false unless from.is_a?(Num) && from.value.is_a?(Integer)
|
|
114
|
+
upper = Limits.infinite?(to) ? Float::INFINITY : (to.is_a?(Num) && to.value.is_a?(Integer) ? to.value : nil)
|
|
115
|
+
return false if upper.nil?
|
|
116
|
+
integer_poles(f, k).any? { |p| p >= from.value && p <= upper }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def summed(f, k, from, to)
|
|
120
|
+
# 0 + 0 + ... is 0, however many terms: f*(oo - from + 1) said undefined
|
|
121
|
+
return Num.new(0) if f.is_a?(Num) && f.value.zero?
|
|
122
|
+
return (f * (to - from + 1)).simplify unless f.variables.include?(k.name)
|
|
123
|
+
|
|
124
|
+
coeffs = Solve.polynomial_coefficients(f, k)
|
|
125
|
+
return polynomial_sum(coeffs, k, from, to) if coeffs && to != OO
|
|
126
|
+
|
|
127
|
+
if to == OO && (z = zeta_sum(f, k, from))
|
|
128
|
+
return z
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# sum_{k=0}^{n} binomial(n, k) ... is the full series when the terms
|
|
132
|
+
# vanish beyond n - which binomial(n, k)/(n - k + 1) does not: the
|
|
133
|
+
# pole at k = n + 1 cancels the zero of the binomial there.
|
|
134
|
+
original = upper = to
|
|
135
|
+
to = OO if vanishes_past_upper?(f, k, to)
|
|
136
|
+
|
|
137
|
+
g = gosper(f, k)
|
|
138
|
+
if g
|
|
139
|
+
upper = to == OO ? tail_limit(g, k) : at_bound(g, k, to + 1)
|
|
140
|
+
lower = at_bound(g, k, from)
|
|
141
|
+
return (upper - lower).cancel if upper && lower
|
|
142
|
+
end
|
|
143
|
+
if to == OO && (known = classical_series(f, k, from))
|
|
144
|
+
return known
|
|
145
|
+
end
|
|
146
|
+
if (telescoped = creative_telescoping(f, k, from, upper))
|
|
147
|
+
return telescoped
|
|
148
|
+
end
|
|
149
|
+
direct = direct_sum(f, k, from, original)
|
|
150
|
+
return direct if direct
|
|
151
|
+
return Fn.new(:harmonic, [original]) if from == Num.new(1) && !Limits.infinite?(original) && Scalar.one?((f * k).simplify) # sum 1/k = H_n
|
|
152
|
+
Sum.new(f, k, from, original)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# r(k)*f(k), with a pole of r that a binomial(k + a, c) of f cancels
|
|
156
|
+
# cancelled: 2**k*binomial(k, 2)*(k**2 - 5*k + 8)/(k**2 - k) is
|
|
157
|
+
# 2**k*(k**2 - 5*k + 8)/2, which has a value at k = 0 and 1 - and so has
|
|
158
|
+
# the closed form at every n. binomial(k + a, c) with a whole c is the
|
|
159
|
+
# polynomial (k + a)(k + a - 1).../c!.
|
|
160
|
+
def without_removable_poles(r, f, k)
|
|
161
|
+
g = (r * f).simplify
|
|
162
|
+
return g unless denominator_in?(r, k)
|
|
163
|
+
_, factors = Simplify.factorize(f)
|
|
164
|
+
polynomial = Num.new(1)
|
|
165
|
+
rest = {}
|
|
166
|
+
factors.each do |base, exp|
|
|
167
|
+
top, bottom = base.is_a?(Fn) && base.name == :binomial ? base.args : []
|
|
168
|
+
whole = bottom.is_a?(Num) && bottom.value.is_a?(Integer) && bottom.value.between?(1, 12) && exp.is_a?(Integer) && exp.positive?
|
|
169
|
+
if whole && Solve.polynomial_coefficients(top, k)&.size == 2
|
|
170
|
+
falling = (0...bottom.value).map { |i| top - i }.reduce(:*) / Combinatorics.factorial_value(bottom.value)
|
|
171
|
+
polynomial *= falling**exp
|
|
172
|
+
else
|
|
173
|
+
Simplify.add_factor(rest, base, exp)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
return g if polynomial == Num.new(1)
|
|
177
|
+
cancelled = (r * polynomial).cancel
|
|
178
|
+
return g if denominator_in?(cancelled, k)
|
|
179
|
+
(cancelled.factor * Simplify.rebuild_product(Simplify.factorize(f).first, rest)).simplify
|
|
180
|
+
rescue StandardError => rescued
|
|
181
|
+
RCAS.guard!(rescued)
|
|
182
|
+
(r * f).simplify
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# binomial(k + a, c) with a whole c >= 0: a polynomial in k.
|
|
186
|
+
def polynomial_binomial?(e, k)
|
|
187
|
+
return false unless e.is_a?(Fn) && e.name == :binomial && e.args.size == 2
|
|
188
|
+
top, bottom = e.args
|
|
189
|
+
bottom.is_a?(Num) && bottom.value.is_a?(Integer) && !bottom.value.negative? &&
|
|
190
|
+
!bottom.variables.include?(k.name) && Solve.polynomial_coefficients(top, k)&.size.to_i <= 2
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def denominator_in?(r, k)
|
|
194
|
+
fraction = Fraction.as_fraction(r)
|
|
195
|
+
!fraction.nil? && fraction.last.to_expr.variables.include?(k.name)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The antidifference at a bound. 2**k*binomial(k, 2)*(8 - 5*k + k**2)/(k**2 - k)
|
|
199
|
+
# has removable poles at 0 and 1, where binomial(k, 2) vanishes; the
|
|
200
|
+
# identity g(k + 1) - g(k) = f(k) holds there by continuity, so the value
|
|
201
|
+
# is the limit (a bare ZeroDivisionError before: fourth review). nil
|
|
202
|
+
# when the limit is not a finite value.
|
|
203
|
+
def at_bound(g, k, bound)
|
|
204
|
+
g.subs(k => bound).simplify
|
|
205
|
+
rescue ZeroDivisionError
|
|
206
|
+
value = Limits.limit(g, k, bound)
|
|
207
|
+
value.is_a?(Limit) || Limits.infinite?(value) || value == UNDEFINED ? nil : value
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# f = binomial(n, k)*r(k) with a symbolic n, and r has no pole at
|
|
211
|
+
# k = n + 1, n + 2, ...: then every term past n is 0 and the sum may run
|
|
212
|
+
# to infinity. r has to be a product of powers in k (a rational function
|
|
213
|
+
# times c**k); anything else is not examined and keeps the bound.
|
|
214
|
+
def vanishes_past_upper?(f, k, to)
|
|
215
|
+
return false unless to.is_a?(Expression) && !to.variables.empty? && !to.variables.include?(k.name)
|
|
216
|
+
top = f.each_node.find { |e| e.is_a?(Fn) && e.name == :binomial && e.args.first == to && e.args.last == k }
|
|
217
|
+
return false unless top
|
|
218
|
+
rest = (f / top).simplify
|
|
219
|
+
# binomial(k, 2) is the polynomial k*(k - 1)/2 and has no pole
|
|
220
|
+
return false if rest.each_node.any? { |e| e.is_a?(Fn) && e.variables.include?(k.name) && e.name != :exp && !polynomial_binomial?(e, k) }
|
|
221
|
+
_, factors = Simplify.factorize(rest)
|
|
222
|
+
factors.all? do |base, exp|
|
|
223
|
+
next true unless base.variables.include?(k.name)
|
|
224
|
+
next true unless exp.is_a?(Numeric) && exp.negative?
|
|
225
|
+
coeffs = Solve.polynomial_coefficients(base, k) or next false
|
|
226
|
+
next false unless coeffs.size == 2 # a linear factor: its root can be named
|
|
227
|
+
root = (-coeffs[0] / coeffs[1]).simplify
|
|
228
|
+
shift = (root - to).simplify
|
|
229
|
+
!(shift.is_a?(Num) && shift.value.is_a?(Integer) && shift.value.positive?) &&
|
|
230
|
+
!(shift.is_a?(Num) && shift.value.is_a?(Rational) && shift.value.denominator == 1 && shift.value.positive?) &&
|
|
231
|
+
shift.variables.empty?
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A definite sum in one other variable, over bounds that make the terms
|
|
236
|
+
# outside them vanish: Zeilberger's algorithm gives a recurrence for the
|
|
237
|
+
# sum, and the recurrence is solved.
|
|
238
|
+
def creative_telescoping(f, k, from, to)
|
|
239
|
+
return nil unless from.is_a?(Num) && from.value.is_a?(Integer)
|
|
240
|
+
outer = (f.variables - [k.name]).to_a
|
|
241
|
+
return nil unless outer.size == 1
|
|
242
|
+
n = Var.new(outer.first)
|
|
243
|
+
return nil unless to == n || Limits.infinite?(to)
|
|
244
|
+
return nil unless term_ratio(f, k) && term_ratio(f, n)
|
|
245
|
+
# Order 1 only: a sum whose closed form is hypergeometric satisfies a
|
|
246
|
+
# first-order recurrence, and the higher orders are expensive on terms
|
|
247
|
+
# like binomial(n, k)**4 that have no closed form anyway. sumrecursion
|
|
248
|
+
# goes further when asked.
|
|
249
|
+
Zeilberger.closed_form(f, n, k, from, to == n ? to : n, max_order: 1)
|
|
250
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Ratio of consecutive terms as a rational function of k (binomials and
|
|
255
|
+
# factorials cancelled), or nil.
|
|
256
|
+
def term_ratio(f, k)
|
|
257
|
+
g = Combinatorics.to_factorials(f)
|
|
258
|
+
ratio = (g.subs(k => k + 1) / g).cancel
|
|
259
|
+
pair = Fraction.as_fraction(ratio, [k.name]) or return nil
|
|
260
|
+
[ratio, pair]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def classical_series(f, k, from, depth = 0)
|
|
264
|
+
return nil unless from.is_a?(Num) && from.value.is_a?(Integer)
|
|
265
|
+
pair = term_ratio(f, k)
|
|
266
|
+
return nil if pair.nil?
|
|
267
|
+
known = Combinatorics.known_series(pair.first, k)
|
|
268
|
+
if known
|
|
269
|
+
m0, total = known
|
|
270
|
+
return nil if from.value < m0
|
|
271
|
+
first = f.subs(k => m0).simplify
|
|
272
|
+
return nil if first.each_node.any? { |n| n.is_a?(Fn) && %i[factorial gamma].include?(n.name) && n.args.first.is_a?(Num) }
|
|
273
|
+
result = (first * total).simplify
|
|
274
|
+
(m0...from.value).each { |i| result -= f.subs(k => i).simplify }
|
|
275
|
+
return result.simplify
|
|
276
|
+
end
|
|
277
|
+
# leading terms that vanish (k*binomial(n, k) at k = 0): shift the index
|
|
278
|
+
return nil if depth > 2 || !Scalar.zero?(f.subs(k => from).simplify)
|
|
279
|
+
shifted = f.subs(k => k + from.value + 1).simplify
|
|
280
|
+
classical_series(shifted, k, Num.new(0), depth + 1)
|
|
281
|
+
rescue ZeroDivisionError
|
|
282
|
+
nil # the ratio matched a template but the term itself has a pole
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
MAX_DIRECT_TERMS = 100_000
|
|
286
|
+
|
|
287
|
+
# No closed form but integer bounds: add the terms up exactly.
|
|
288
|
+
def direct_sum(f, k, from, to)
|
|
289
|
+
return nil unless [from, to].all? { |b| b.is_a?(Num) && b.value.is_a?(Integer) }
|
|
290
|
+
return nil if to.value - from.value > MAX_DIRECT_TERMS
|
|
291
|
+
# numbers are added as numbers, everything else collected into one
|
|
292
|
+
# term table and rebuilt once: adding to the growing sum and
|
|
293
|
+
# expanding it again each time was quadratic, and sum(sin(k), k, 1,
|
|
294
|
+
# 3000) ran for minutes (third review, section 5)
|
|
295
|
+
number = 0
|
|
296
|
+
constant = 0
|
|
297
|
+
terms = {}
|
|
298
|
+
(from.value..to.value).each do |i|
|
|
299
|
+
term = Expression.lift(f.call(k.name => i))
|
|
300
|
+
if term.is_a?(Num)
|
|
301
|
+
number += term.value
|
|
302
|
+
else
|
|
303
|
+
c, table = Simplify.termize(term.simplify)
|
|
304
|
+
constant += c
|
|
305
|
+
table.each { |factors, coeff| terms[factors] = (terms[factors] || 0) + coeff }
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
return Num.new(number) if terms.empty? && constant.zero?
|
|
309
|
+
Simplify.rebuild_sum(Simplify.normalize_number(number + constant), terms.reject { |_, c| c.zero? })
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ---- c / k**s from k = m to infinity: c * (zeta(s) - partial sum) -----------
|
|
313
|
+
|
|
314
|
+
def zeta_sum(f, k, from)
|
|
315
|
+
coeff, factors = Simplify.factorize(f)
|
|
316
|
+
return nil unless factors.size == 1 && factors.key?(k)
|
|
317
|
+
s = factors[k]
|
|
318
|
+
return nil unless s.is_a?(Numeric) && s.real? && s < -1
|
|
319
|
+
return nil unless from.is_a?(Num) && from.value.is_a?(Integer) && from.value >= 1
|
|
320
|
+
s = -s
|
|
321
|
+
return nil unless s.is_a?(Integer)
|
|
322
|
+
partial = (1...from.value).sum { |n| Rational(1, n**s) }
|
|
323
|
+
(Num.new(coeff) * (Fn.new(:zeta, [Num.new(s)]) - Num.new(partial))).simplify
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# zeta(2m) = (-1)^(m+1) B_2m (2 pi)^(2m) / (2 (2m)!)
|
|
327
|
+
def zeta_even(n)
|
|
328
|
+
m = n / 2
|
|
329
|
+
b = bernoulli(n)
|
|
330
|
+
factorial = (1..n).reduce(1, :*)
|
|
331
|
+
c = Rational((-1)**(m + 1) * 2**n, 2 * factorial) * b
|
|
332
|
+
(Num.new(c) * PI**n).simplify
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def bernoulli(n)
|
|
336
|
+
@bernoulli ||= [Rational(1)]
|
|
337
|
+
while @bernoulli.size <= n
|
|
338
|
+
m = @bernoulli.size
|
|
339
|
+
s = (0...m).sum { |j| binomial(m + 1, j) * @bernoulli[j] }
|
|
340
|
+
@bernoulli << -s / (m + 1)
|
|
341
|
+
end
|
|
342
|
+
@bernoulli[n]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def binomial(n, k) = (1..k).reduce(1) { |acc, i| acc * (n - k + i) / i }
|
|
346
|
+
|
|
347
|
+
# Euler-Maclaurin tail after 200 terms; accurate to ~1e-12 for s >= 1.5.
|
|
348
|
+
def zeta_numeric(s)
|
|
349
|
+
raise ArgumentError, "zeta(s) needs s > 1, got #{s}" unless s > 1
|
|
350
|
+
n = 200
|
|
351
|
+
head = (1...n).sum { |i| i.to_f**-s }
|
|
352
|
+
head + n**(1 - s) / (s - 1) + 0.5 * n**-s + s * n**(-s - 1) / 12 - s * (s + 1) * (s + 2) * n**(-s - 3) / 720
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# ---- polynomials: Faulhaber by interpolation -----------------------------
|
|
356
|
+
|
|
357
|
+
def polynomial_sum(coeffs, k, from, to)
|
|
358
|
+
total = Num.new(0)
|
|
359
|
+
coeffs.each_with_index do |c, j|
|
|
360
|
+
next if Scalar.zero?(c)
|
|
361
|
+
s = power_sum(j)
|
|
362
|
+
total += c * (s.call(n: to) - s.call(n: from - 1))
|
|
363
|
+
end
|
|
364
|
+
tidy(total.expand)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# A hypergeometric closed form is often a binomial coefficient in
|
|
368
|
+
# disguise: the sum of binomial(n, k)**2 comes back from the gammas as
|
|
369
|
+
# 2**(2*n)*gamma(1/2 + n)/(pi**(1/2)*n!), which is binomial(2*n, n).
|
|
370
|
+
def hypergeometric_form(value, to)
|
|
371
|
+
return value unless to.is_a?(Var)
|
|
372
|
+
Combinatorics.as_binomial(value, to) || value
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# A closed form reads better factored, which is the form a course
|
|
376
|
+
# writes: n**2*(1 + n)**2/4 rather than n**2/4 + n**3/2 + n**4/4. A
|
|
377
|
+
# number stays the number it is, and a polynomial that does not factor
|
|
378
|
+
# comes back as it went in.
|
|
379
|
+
def tidy(value)
|
|
380
|
+
return value if value.is_a?(Num) || value.variables.empty?
|
|
381
|
+
value.factor.simplify
|
|
382
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
383
|
+
RCAS.guard!(rescued, refused: true)
|
|
384
|
+
value
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# S_j(n) = 1**j + 2**j + ... + n**j as a polynomial in n.
|
|
388
|
+
def power_sum(j)
|
|
389
|
+
@power_sums ||= {}
|
|
390
|
+
@power_sums[j] ||= begin
|
|
391
|
+
n = Var.new(:n)
|
|
392
|
+
points = (0..j + 1).map { |m| [m, (1..m).sum { |i| i**j }] }
|
|
393
|
+
# Newton interpolation with exact rationals
|
|
394
|
+
coefficients = points.map { |_, v| Rational(v) }
|
|
395
|
+
(1..j + 1).each do |level|
|
|
396
|
+
(j + 1).downto(level) { |i| coefficients[i] = (coefficients[i] - coefficients[i - 1]) / (points[i][0] - points[i - level][0]) }
|
|
397
|
+
end
|
|
398
|
+
poly = Num.new(0)
|
|
399
|
+
(j + 1).downto(0) { |i| poly = (poly * (n - points[i][0]) + coefficients[i]) }
|
|
400
|
+
poly.expand
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# ---- Gosper's algorithm ------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
# Antidifference g with g(k+1) - g(k) = f(k), or nil.
|
|
407
|
+
def gosper(f, k)
|
|
408
|
+
ratio_pair = term_ratio(f, k)
|
|
409
|
+
return nil if ratio_pair.nil?
|
|
410
|
+
p, q = ratio_pair.last
|
|
411
|
+
ring = p.ring
|
|
412
|
+
return nil unless ring.vars.include?(k.name)
|
|
413
|
+
kn = k.name
|
|
414
|
+
|
|
415
|
+
a, b, c = normal_form(p, q, k)
|
|
416
|
+
|
|
417
|
+
bm = shift(b, k, -1)
|
|
418
|
+
d = degree_bound(a, bm, c, kn) or return nil
|
|
419
|
+
xs = (0..d).map { |i| Var.new(:"_x#{i}") }
|
|
420
|
+
xk = xs.each_with_index.reduce(Num.new(0)) { |acc, (v, i)| acc + v * k**i }
|
|
421
|
+
equation = (a.to_expr * xk.subs(k => k + 1) - bm.to_expr * xk - c.to_expr).expand
|
|
422
|
+
conditions = Solve.polynomial_coefficients(equation, k) or return nil
|
|
423
|
+
solution = Solve.linear_system(conditions, xs).first or return nil
|
|
424
|
+
x_expr = xk.subs(solution).subs(xs.to_h { |v| [v, Num.new(0)] }) # free unknowns: any value works
|
|
425
|
+
# the rational factor cancels on its own: with the term inside, a
|
|
426
|
+
# binomial(k, 3) stood between (k - 3)/4 and its common factor
|
|
427
|
+
g = without_removable_poles((bm.to_expr * x_expr / c.to_expr).cancel, f, k)
|
|
428
|
+
check = (g.subs(k => k + 1) - g - f).cancel
|
|
429
|
+
Scalar.zero?(check) || Decide.identically_zero?(check) ? g : nil
|
|
430
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Gosper-Petkovsek normal form of a term ratio: polynomials a, b, c with
|
|
435
|
+
# p(k)/q(k) = a(k)/b(k) * c(k + 1)/c(k) and gcd(a(k), b(k + h)) = 1 for
|
|
436
|
+
# every integer h >= 0. Zeilberger's algorithm uses it too.
|
|
437
|
+
def normal_form(p, q, k)
|
|
438
|
+
a, b, c = p, q, p.ring.one
|
|
439
|
+
dispersion(a, b, k.name).each do |h|
|
|
440
|
+
g = a.gcd(shift(b, k, h))
|
|
441
|
+
next if g.constant?
|
|
442
|
+
a = a.exact_div(g)
|
|
443
|
+
b = b.exact_div(shift(g, k, -h))
|
|
444
|
+
(1..h).each { |i| c *= shift(g, k, -i) }
|
|
445
|
+
end
|
|
446
|
+
[a, b, c]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def shift(poly, k, h) = poly.ring.call(poly.to_expr.subs(k => k + h))
|
|
450
|
+
|
|
451
|
+
# Non-negative integers h with gcd(a(k), b(k+h)) non-trivial.
|
|
452
|
+
def dispersion(a, b, kn)
|
|
453
|
+
h = Var.new(:_h)
|
|
454
|
+
ringh = QQ[*a.ring.vars, :_h]
|
|
455
|
+
bh = ringh.call(b.to_expr.subs(Var.new(kn) => Var.new(kn) + h))
|
|
456
|
+
res = ringh.call(a.to_expr).resultant(bh, kn)
|
|
457
|
+
return [] if res.zero?
|
|
458
|
+
coeffs = (0..res.degree(:_h)).map { |i| res.coefficient_in(:_h, i).to_expr }
|
|
459
|
+
roots = begin
|
|
460
|
+
Solve.polynomial_roots(coeffs)
|
|
461
|
+
rescue NotImplementedError, RCAS::Unsupported
|
|
462
|
+
[]
|
|
463
|
+
end
|
|
464
|
+
roots.select { |r| r.is_a?(Num) && r.value.is_a?(Integer) && r.value >= 0 }.map(&:value).uniq.sort
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def degree_bound(a, bm, c, kn)
|
|
468
|
+
da, db, dc = a.degree(kn), bm.degree(kn), c.degree(kn)
|
|
469
|
+
lca = a.leading_coefficient_in(kn)
|
|
470
|
+
lcb = bm.leading_coefficient_in(kn)
|
|
471
|
+
candidates = []
|
|
472
|
+
if da != db || lca != lcb
|
|
473
|
+
candidates << dc - [da, db].max
|
|
474
|
+
else
|
|
475
|
+
n = da
|
|
476
|
+
candidates << dc - n + 1
|
|
477
|
+
diff = bm.coefficient_in(kn, n - 1) - a.coefficient_in(kn, n - 1)
|
|
478
|
+
if n >= 1 && diff.constant? && lca.constant?
|
|
479
|
+
ratio = Scalar.div(diff.constant_term, lca.constant_term)
|
|
480
|
+
candidates << ratio.value if ratio.is_a?(Num) && ratio.value.is_a?(Integer)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
d = candidates.max
|
|
484
|
+
d.negative? ? nil : d
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Random-point check for identities the canonical form does not prove.
|
|
488
|
+
# lim_{k -> oo} g(k) for the antidifference: geometric factors with
|
|
489
|
+
# |ratio| < 1 vanish, anything else goes to Limits.limit.
|
|
490
|
+
# The geometric factors of a term are taken together: 2**k/3**k is
|
|
491
|
+
# (2/3)**k and converges, while each factor alone said "2**k diverges"
|
|
492
|
+
# (third review, S3).
|
|
493
|
+
def tail_limit(g, k)
|
|
494
|
+
constant, table = Expand.table(g)
|
|
495
|
+
kept = {}
|
|
496
|
+
table.each do |factors, coeff|
|
|
497
|
+
drop = false
|
|
498
|
+
ratio = 1r
|
|
499
|
+
numeric_ratio = true
|
|
500
|
+
factors.each do |base, exp|
|
|
501
|
+
if base.is_a?(Fn) && %i[factorial gamma].include?(base.name) && base.variables.include?(k.name) && exp.is_a?(Integer)
|
|
502
|
+
raise ArgumentError, "the sum diverges" if exp.positive?
|
|
503
|
+
drop = true
|
|
504
|
+
next
|
|
505
|
+
end
|
|
506
|
+
next unless exp.is_a?(Expression) && exp.variables.include?(k.name) && !base.variables.include?(k.name)
|
|
507
|
+
slope = exp.diff(k)
|
|
508
|
+
next unless slope.is_a?(Num) && (slope.value.is_a?(Integer) || slope.value.is_a?(Rational))
|
|
509
|
+
magnitude =
|
|
510
|
+
if base.is_a?(Num) && base.value.is_a?(Numeric) then base.value.abs
|
|
511
|
+
elsif base == Simplify.exp_base then Math::E
|
|
512
|
+
end
|
|
513
|
+
if magnitude.nil?
|
|
514
|
+
numeric_ratio = false # symbolic ratio: convergence (|ratio| < 1) is assumed
|
|
515
|
+
next
|
|
516
|
+
end
|
|
517
|
+
ratio *= magnitude.is_a?(Float) ? magnitude**slope.value : Rational(magnitude)**slope.value
|
|
518
|
+
end
|
|
519
|
+
if !drop && numeric_ratio && ratio != 1
|
|
520
|
+
raise ArgumentError, "the sum diverges" if ratio > 1
|
|
521
|
+
drop = true
|
|
522
|
+
elsif !numeric_ratio
|
|
523
|
+
drop = true
|
|
524
|
+
end
|
|
525
|
+
kept[factors] = coeff unless drop
|
|
526
|
+
end
|
|
527
|
+
rest = Simplify.rebuild_sum(constant, kept)
|
|
528
|
+
value = Limits.limit(rest, k, OO)
|
|
529
|
+
value.is_a?(Limit) ? nil : value
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|