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,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Linear recurrences.
|
|
5
|
+
#
|
|
6
|
+
# rsolve(eq(u(n + 2), u(n + 1) + u(n)), u, n) # general solution
|
|
7
|
+
# rsolve(eq(u(n + 2), u(n + 1) + u(n)), u, n, init: {0 => 0, 1 => 1}) # Fibonacci
|
|
8
|
+
# rsolve(eq(u(n + 1), 2*u(n) + 1), u, n, init: {0 => 0}) # => -1 + 2**n
|
|
9
|
+
# rsolve(eq(u(n + 1), n*u(n)), u, n) # => C1*(n - 1)!
|
|
10
|
+
#
|
|
11
|
+
# In bin/rcas an undefined name applied to arguments, u(n + 1), is the
|
|
12
|
+
# unknown sequence. With constant coefficients the homogeneous solution
|
|
13
|
+
# comes from the characteristic roots (multiplicities give n, n**2, ...
|
|
14
|
+
# factors) and a forcing term that is a sum of polynomials times b**n goes
|
|
15
|
+
# through undetermined coefficients; with coefficients that depend on n,
|
|
16
|
+
# Petkovsek's algorithm gives the hypergeometric solutions, and rsolve
|
|
17
|
+
# writes down the general solution only when there are as many of them as
|
|
18
|
+
# the order of the recurrence (`hyper` lists them either way). Initial
|
|
19
|
+
# values fix the constants through a linear system.
|
|
20
|
+
#
|
|
21
|
+
# Sources (keys: MANUAL.md, Sources): [GKP94, §7.3]; the constant-coefficient
|
|
22
|
+
# method is the discrete twin of ode.rb's; [Pet92] and [Koe14, ch. 9] for
|
|
23
|
+
# the rest (petkovsek.rb).
|
|
24
|
+
module Recurrence
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def rsolve(equation, u, n, init: {})
|
|
28
|
+
name, n, coeffs, forcing = normalize(equation, u, n)
|
|
29
|
+
constants = []
|
|
30
|
+
general =
|
|
31
|
+
if coeffs.any? { |c| c.variables.include?(n.name) }
|
|
32
|
+
hypergeometric_solution(coeffs, forcing, n, constants)
|
|
33
|
+
else
|
|
34
|
+
constant_coefficient_solution(coeffs, forcing, n, constants)
|
|
35
|
+
end
|
|
36
|
+
unless init.empty?
|
|
37
|
+
# the closed form holds from the first index past the singularities
|
|
38
|
+
# of its ratios; before that it holds only if it agrees with the
|
|
39
|
+
# sequence the recurrence itself builds from the initial values.
|
|
40
|
+
# u(0) = 1 for u(n + 1) = (n - 3)*u(n) made the constant 1/(-4)!
|
|
41
|
+
# (S6); n! and n*n! are defined at 0, and u(n + 2) = 2*(n + 2)*u(n + 1)
|
|
42
|
+
# - (n + 1)*(n + 2)*u(n) runs from there (fourth review). Constant
|
|
43
|
+
# coefficients have no singularities, so u(-1) is as good as u(0).
|
|
44
|
+
early = @start ? init.keys.map { |i| Expression.lift(i) }.select { |i| i.is_a?(Num) && i.value < @start } : []
|
|
45
|
+
specific = begin
|
|
46
|
+
initial_values(general, init, n, constants)
|
|
47
|
+
rescue ZeroDivisionError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
if specific.nil? || (!early.empty? && !propagates?(specific, coeffs, init, n))
|
|
51
|
+
raise RCAS::Unsupported, "rsolve: the closed form holds from #{n} = #{@start} on, so it cannot take the initial value at #{early.map(&:to_s).join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
general = specific
|
|
54
|
+
end
|
|
55
|
+
Equation.new(Fn.new(name, [n]), general)
|
|
56
|
+
ensure
|
|
57
|
+
@start = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Petkovsek's `Hyper`: the hypergeometric solutions of a homogeneous
|
|
61
|
+
# recurrence, as terms, without the constants rsolve puts in front.
|
|
62
|
+
def hyper(equation, u, n)
|
|
63
|
+
_, n, coeffs, forcing = normalize(equation, u, n)
|
|
64
|
+
raise RCAS::Unsupported, "hyper: #{equation} is not homogeneous" unless Scalar.zero?(forcing)
|
|
65
|
+
Petkovsek.solutions(coeffs, n)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# [name, index, [p_0, ..., p_r], forcing] of the equation read as
|
|
69
|
+
# sum_j p_j(n)*u(n + j) = forcing(n).
|
|
70
|
+
def normalize(equation, u, n)
|
|
71
|
+
n = Expression.lift(n)
|
|
72
|
+
raise ArgumentError, "rsolve: the index must be a symbol, got #{n}" unless n.is_a?(Var)
|
|
73
|
+
name = sequence_name(u)
|
|
74
|
+
f = Solve.to_zero(equation).simplify
|
|
75
|
+
shifts = shift_table(f, name, n)
|
|
76
|
+
raise ArgumentError, "#{equation} contains no #{name}(#{n} + k)" if shifts.empty?
|
|
77
|
+
low = shifts.values.min
|
|
78
|
+
unless low.zero? # u(n - 1) = ... : renumber so the lowest shift is 0
|
|
79
|
+
f = f.subs(n => n - low).simplify
|
|
80
|
+
shifts = shift_table(f, name, n)
|
|
81
|
+
end
|
|
82
|
+
order = shifts.values.max
|
|
83
|
+
raise ArgumentError, "#{equation} is not a recurrence: only #{name}(#{n}) occurs" if order.zero?
|
|
84
|
+
|
|
85
|
+
ds = (0..order).map { |k| Var.new(:"_s#{k}") }
|
|
86
|
+
g = f.subs(shifts.to_h { |t, k| [t, ds[k]] })
|
|
87
|
+
raise RCAS::Unsupported, "only linear recurrences are supported" unless Solve.linear_in?(g, ds)
|
|
88
|
+
coeffs = ds.map { |d| Solve.polynomial_coefficients(g, d)[1] || Num.new(0) }
|
|
89
|
+
forcing = Simplify.negate(g.subs(ds.to_h { |d| [d, Num.new(0)] })).simplify
|
|
90
|
+
coeffs, forcing = clear_denominators(coeffs, forcing, n)
|
|
91
|
+
[name, n, coeffs, forcing]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# u(n + 1) = (2*n + 3)*u(n)/(n + 2) is the same recurrence as
|
|
95
|
+
# (n + 2)*u(n + 1) - (2*n + 3)*u(n) = 0, and Petkovsek needs the second
|
|
96
|
+
# form: multiply through by the common denominator.
|
|
97
|
+
def clear_denominators(coeffs, forcing, n)
|
|
98
|
+
parts = coeffs + [forcing]
|
|
99
|
+
return [coeffs, forcing] if parts.all? { |c| Solve.polynomial_coefficients(c, n) }
|
|
100
|
+
vars = parts.flat_map { |c| c.variables.to_a }.uniq | [n.name]
|
|
101
|
+
ring = QQ[*vars]
|
|
102
|
+
common = ring.one
|
|
103
|
+
parts.each do |c|
|
|
104
|
+
pair = Fraction.as_fraction(c, vars) or return [coeffs, forcing]
|
|
105
|
+
common = common.lcm(pair.last)
|
|
106
|
+
end
|
|
107
|
+
return [coeffs, forcing] if common.constant?
|
|
108
|
+
factor = common.to_expr
|
|
109
|
+
[coeffs.map { |c| (c * factor).cancel.expand }, (forcing * factor).cancel.expand]
|
|
110
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
111
|
+
[coeffs, forcing]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Constant coefficients: characteristic roots and undetermined
|
|
115
|
+
# coefficients, the discrete twin of dsolve's.
|
|
116
|
+
def constant_coefficient_solution(coeffs, forcing, n, constants)
|
|
117
|
+
roots = Solve.polynomial_roots(coeffs).map(&:simplify)
|
|
118
|
+
homogeneous = homogeneous_solution(roots, n, constants)
|
|
119
|
+
particular = Scalar.zero?(forcing) ? Num.new(0) : (undetermined_coefficients(coeffs, forcing, roots, n) || raise(RCAS::Unsupported, "no method for the forcing term #{forcing}"))
|
|
120
|
+
(homogeneous + particular).simplify
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Coefficients that depend on n: Petkovsek's algorithm. As many
|
|
124
|
+
# independent hypergeometric solutions as the order of the recurrence
|
|
125
|
+
# span its solution space; with fewer, the missing ones are not
|
|
126
|
+
# hypergeometric and the general solution cannot be written down, so we
|
|
127
|
+
# say so rather than pass off a part of it as the whole.
|
|
128
|
+
def hypergeometric_solution(coeffs, forcing, n, constants)
|
|
129
|
+
unless Scalar.zero?(forcing)
|
|
130
|
+
raise RCAS::Unsupported, "rsolve: polynomial coefficients with the forcing term #{forcing} are not supported"
|
|
131
|
+
end
|
|
132
|
+
order = coeffs.size - 1
|
|
133
|
+
ratios = Petkovsek.ratios(coeffs, n)
|
|
134
|
+
found = ratios.map { |ratio| Petkovsek.term(ratio, n) }
|
|
135
|
+
@start = ratios.map { |ratio| Petkovsek.start_index(ratio, n) }.max
|
|
136
|
+
raise RCAS::Unsupported, "rsolve: no hypergeometric solutions (see hyper)" if found.empty?
|
|
137
|
+
if found.size < order
|
|
138
|
+
raise RCAS::Unsupported, "rsolve: only #{found.size} of #{order} solutions are hypergeometric: " \
|
|
139
|
+
"#{found.map(&:to_s).join(', ')} (see hyper)"
|
|
140
|
+
end
|
|
141
|
+
# as many solutions as the order is not yet a basis: they have to be
|
|
142
|
+
# independent, which their Casoratian says (third review, S5)
|
|
143
|
+
unless independent?(found, n, order, @start || 0)
|
|
144
|
+
raise RCAS::Unsupported, "rsolve: the hypergeometric solutions #{found.map(&:to_s).join(', ')} do not span the solutions (see hyper)"
|
|
145
|
+
end
|
|
146
|
+
found.map do |t|
|
|
147
|
+
constants << Var.new(:"C#{constants.size + 1}")
|
|
148
|
+
constants.last * t
|
|
149
|
+
end.reduce(:+).simplify
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The sequence the recurrence builds from `order` consecutive initial
|
|
153
|
+
# values, compared with the closed form over those indices and a few
|
|
154
|
+
# more: true only when every value is decided equal.
|
|
155
|
+
def propagates?(closed, coeffs, init, n)
|
|
156
|
+
order = coeffs.size - 1
|
|
157
|
+
values = init.to_h { |i, v| [Integer(Expression.lift(i).value), Expression.lift(v)] }
|
|
158
|
+
first = values.keys.min
|
|
159
|
+
return false unless (first...first + order).all? { |i| values.key?(i) }
|
|
160
|
+
(first...first + order + 4).each do |j|
|
|
161
|
+
lead = coeffs.last.subs(n => Num.new(j)).simplify
|
|
162
|
+
return false if Scalar.zero?(lead)
|
|
163
|
+
rest = (0...order).map { |i| coeffs[i].subs(n => Num.new(j)) * values.fetch(j + i) }.reduce(:+)
|
|
164
|
+
values[j + order] ||= (Neg.new(rest) / lead).simplify
|
|
165
|
+
end
|
|
166
|
+
values.all? { |j, v| Decide.zero?((closed.subs(n => Num.new(j)) - v).simplify) == true }
|
|
167
|
+
rescue ZeroDivisionError, KeyError, ArgumentError, TypeError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# det[t_j(m + i)] != 0 at some integer m past the start: then the terms
|
|
172
|
+
# are independent. Dependent only when that determinant is decided zero
|
|
173
|
+
# at every point tried - for a parameter, identically; terms that do not
|
|
174
|
+
# evaluate (a product node) or a determinant that is not decided keep
|
|
175
|
+
# the count, which is the claim Petkovsek's algorithm makes (a basis
|
|
176
|
+
# n!, a**n*n! was called dependent: fourth review).
|
|
177
|
+
def independent?(terms, n, order, start)
|
|
178
|
+
verdicts = (start..start + 4).map do |m|
|
|
179
|
+
rows = (0...order).map do |i|
|
|
180
|
+
terms.map { |t| t.subs(n => Num.new(m + i)).simplify }
|
|
181
|
+
end
|
|
182
|
+
next nil if rows.flatten.any? { |v| v.each_node.any? { |e| e.is_a?(Product) || e.is_a?(Sum) || e == UNDEFINED } }
|
|
183
|
+
det = Elimination.det(rows).simplify
|
|
184
|
+
det.variables.empty? ? Decide.zero?(det) : Decide.identically_zero?(det)
|
|
185
|
+
end
|
|
186
|
+
!verdicts.all?(true)
|
|
187
|
+
rescue ZeroDivisionError
|
|
188
|
+
true
|
|
189
|
+
rescue StandardError => rescued
|
|
190
|
+
RCAS.guard!(rescued)
|
|
191
|
+
true # nothing to evaluate: the count stands, as before
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def sequence_name(u)
|
|
195
|
+
case u
|
|
196
|
+
when Symbol then u
|
|
197
|
+
when Var then u.name
|
|
198
|
+
when Fn then u.name
|
|
199
|
+
else raise ArgumentError, "rsolve: name the sequence, e.g. rsolve(equation, u, n)"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# { u(n + k) node => k }
|
|
204
|
+
def shift_table(f, name, n)
|
|
205
|
+
f.each_node.select { |e| e.is_a?(Fn) && e.name == name }.uniq.to_h do |t|
|
|
206
|
+
raise ArgumentError, "#{t}: one argument expected" unless t.args.size == 1
|
|
207
|
+
cs = Solve.polynomial_coefficients(t.args.first, n)
|
|
208
|
+
unless cs && cs.size == 2 && Scalar.one?(cs[1]) && cs[0].is_a?(Num) && cs[0].value.is_a?(Integer)
|
|
209
|
+
raise RCAS::Unsupported, "#{t}: the argument must be #{n} plus an integer"
|
|
210
|
+
end
|
|
211
|
+
[t, cs[0].value]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def homogeneous_solution(roots, n, constants)
|
|
216
|
+
tally = []
|
|
217
|
+
roots.each do |r|
|
|
218
|
+
entry = tally.find { |root, _| root == r }
|
|
219
|
+
entry ? entry[1] += 1 : tally << [r, 1]
|
|
220
|
+
end
|
|
221
|
+
tally.sort_by { |r, _| [r.variables.empty? && !r.evalf.is_a?(Complex) ? r.evalf.to_f : Float::INFINITY, r.to_s] }
|
|
222
|
+
parts = tally.map do |r, m|
|
|
223
|
+
next nil if Scalar.zero?(r) # a zero root contributes nothing for n >= order
|
|
224
|
+
poly = (0...m).map do |j|
|
|
225
|
+
constants << Var.new(:"C#{constants.size + 1}")
|
|
226
|
+
j.zero? ? constants.last : constants.last * n**j
|
|
227
|
+
end.reduce(:+)
|
|
228
|
+
poly * r**n
|
|
229
|
+
end.compact
|
|
230
|
+
parts.empty? ? Num.new(0) : parts.reduce(:+)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Forcing terms polynomial(n) * b**n; the ansatz for base b is
|
|
234
|
+
# n**m * (A_0 + ... + A_d n**d) * b**n, m the multiplicity of b as a root.
|
|
235
|
+
def undetermined_coefficients(coeffs, forcing, roots, n)
|
|
236
|
+
classes = forcing_classes(forcing, n) or return nil
|
|
237
|
+
unknowns = []
|
|
238
|
+
ansatz = classes.map do |b, degree|
|
|
239
|
+
m = roots.count { |r| Scalar.zero?((r - b).simplify) }
|
|
240
|
+
poly = (0..degree).map do |j|
|
|
241
|
+
unknowns << Var.new(:"_A#{unknowns.size}")
|
|
242
|
+
unknowns.last * n**(j + m)
|
|
243
|
+
end.reduce(:+)
|
|
244
|
+
Scalar.one?(b) ? poly : poly * b**n
|
|
245
|
+
end.reduce(:+)
|
|
246
|
+
applied = coeffs.each_with_index.map { |a, k| a * ansatz.subs(n => n + k) }.reduce(:+)
|
|
247
|
+
residual = (applied - forcing).expand
|
|
248
|
+
equations = collect_by_function(residual, unknowns, n)
|
|
249
|
+
solution = Solve.linear_system(equations, unknowns).first or return nil
|
|
250
|
+
unknowns.each { |a| solution[a] ||= Num.new(0) }
|
|
251
|
+
ansatz.subs(solution).simplify
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# { base => degree }: base**(c n) with the offset folded into the coefficient.
|
|
255
|
+
def forcing_classes(forcing, n)
|
|
256
|
+
constant, terms = Simplify.termize(forcing, simplify: true)
|
|
257
|
+
classes = {}
|
|
258
|
+
add = lambda do |b, degree|
|
|
259
|
+
key = classes.keys.find { |b2| Scalar.zero?((b - b2).simplify) } || b
|
|
260
|
+
classes[key] = [classes[key] || 0, degree].max
|
|
261
|
+
end
|
|
262
|
+
add.call(Num.new(1), 0) unless constant.zero?
|
|
263
|
+
terms.each_key do |factors|
|
|
264
|
+
base_total = Num.new(1)
|
|
265
|
+
degree = 0
|
|
266
|
+
factors.each do |base, e|
|
|
267
|
+
exponent = Expression.lift(e)
|
|
268
|
+
if base == n
|
|
269
|
+
return nil unless e.is_a?(Integer) && e >= 0
|
|
270
|
+
degree += e
|
|
271
|
+
elsif base.variables.include?(n.name)
|
|
272
|
+
return nil
|
|
273
|
+
elsif exponent.variables.include?(n.name)
|
|
274
|
+
cs = Solve.polynomial_coefficients(exponent, n)
|
|
275
|
+
return nil unless cs && cs.size == 2
|
|
276
|
+
base_total *= Simplify.power_node(base, cs[1])
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
add.call(base_total.simplify, degree)
|
|
280
|
+
end
|
|
281
|
+
classes
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Group the terms of a residual, linear in the unknowns, by the function
|
|
285
|
+
# of n they multiply; b**(n + 1) is split into b * b**n.
|
|
286
|
+
def collect_by_function(residual, unknowns, n)
|
|
287
|
+
constant, table = Expand.table(residual)
|
|
288
|
+
groups = { Num.new(1) => [Num.new(constant)] }
|
|
289
|
+
table.each do |factors, coeff|
|
|
290
|
+
coefficient = Num.new(coeff)
|
|
291
|
+
function = {}
|
|
292
|
+
factors.each do |base, e|
|
|
293
|
+
exponent = Expression.lift(e)
|
|
294
|
+
if unknowns.include?(base)
|
|
295
|
+
coefficient *= base
|
|
296
|
+
elsif exponent.variables.include?(n.name)
|
|
297
|
+
cs = Solve.polynomial_coefficients(exponent, n)
|
|
298
|
+
if cs && cs.size == 2
|
|
299
|
+
coefficient *= Simplify.power_node(base, cs[0]) unless Scalar.zero?(cs[0])
|
|
300
|
+
function[base] = (cs[1] * n).expand
|
|
301
|
+
else
|
|
302
|
+
function[base] = e
|
|
303
|
+
end
|
|
304
|
+
elsif base.variables.include?(n.name)
|
|
305
|
+
function[base] = e
|
|
306
|
+
else
|
|
307
|
+
coefficient *= Simplify.power_node(base, e)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
key = Simplify.rebuild_product(1, function).simplify
|
|
311
|
+
(groups[key] ||= []) << coefficient
|
|
312
|
+
end
|
|
313
|
+
groups.values.map { |parts| parts.reduce(:+) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def initial_values(general, init, n, constants)
|
|
317
|
+
equations = init.map { |i, value| (general.subs(n => i) - Expression.lift(value)).simplify }
|
|
318
|
+
solution = Solve.linear_system(equations, constants).first
|
|
319
|
+
raise ArgumentError, "rsolve: the initial values #{init} are inconsistent" if solution.nil?
|
|
320
|
+
general.subs(solution).simplify
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|