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