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/ode.rb
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# D(y, x, n): the n-th derivative of an unknown function y of x, used to
|
|
5
|
+
# write differential equations.
|
|
6
|
+
class Derivative < Expression
|
|
7
|
+
attr_reader :expr, :var, :order
|
|
8
|
+
|
|
9
|
+
def initialize(expr, var, order = 1)
|
|
10
|
+
@expr = Expression.lift(expr)
|
|
11
|
+
@var = Expression.lift(var)
|
|
12
|
+
@order = order
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def children = [expr, var]
|
|
17
|
+
def rebuild(expr, var) = Derivative.new(expr, var, order)
|
|
18
|
+
|
|
19
|
+
# d/dx names x twice: the variable differentiated and the point where
|
|
20
|
+
# the derivative is taken. Substituting x, or bringing x in through a
|
|
21
|
+
# parameter, is a statement about the derivative *at* a point - so the
|
|
22
|
+
# derivative is taken first: D(x**3, x) at x = 2 is 12, not D(8, 2), and
|
|
23
|
+
# D(x*y, x) at y = x is x, not 2*x (fourth review, C2). The derivative
|
|
24
|
+
# of an unknown function y has no value to take at a point; y itself
|
|
25
|
+
# stands for y(x), and dsolve substitutes a solution for it.
|
|
26
|
+
def replace_with(table)
|
|
27
|
+
return table[self] if table.key?(self)
|
|
28
|
+
pointwise = table.key?(var) ||
|
|
29
|
+
(!expr.is_a?(Var) && table.any? { |k, v| k != var && v.variables.include?(var.name) && expr.variables.include?(k.name) })
|
|
30
|
+
return super unless pointwise
|
|
31
|
+
if expr.is_a?(Var)
|
|
32
|
+
return super unless table.key?(var)
|
|
33
|
+
raise RCAS::Unsupported, "#{self} at #{var} = #{table[var]}: the derivative of an unknown function has no value to substitute into"
|
|
34
|
+
end
|
|
35
|
+
taken = evaluate
|
|
36
|
+
raise RCAS::Unsupported, "#{self} could not be taken, so it cannot be evaluated at a point" if taken.each_node.any? { |n| n.is_a?(Derivative) }
|
|
37
|
+
taken.replace_with(table)
|
|
38
|
+
end
|
|
39
|
+
def ==(other) = other.is_a?(Derivative) && other.order == order && other.children == children
|
|
40
|
+
alias eql? ==
|
|
41
|
+
def hash = [Derivative, order, expr, var].hash
|
|
42
|
+
def to_sexp = [:D, expr.to_sexp, var.to_sexp, order]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Ordinary differential equations:
|
|
46
|
+
#
|
|
47
|
+
# dsolve(eq(D(y, x), 2*x*y), y, x) # separable
|
|
48
|
+
# dsolve(eq(D(y, x) + 2*y, exp(x)), y, x) # first-order linear
|
|
49
|
+
# dsolve(D(y, x, 2) - 3*D(y, x) + 2*y, y, x) # constant coefficients
|
|
50
|
+
# dsolve(eq(D(y, x, 2) + y, x*exp(x)), y, x) # non-homogeneous, any order
|
|
51
|
+
#
|
|
52
|
+
# Returns Equation(s) y = ... with constants C1, C2 (or an implicit
|
|
53
|
+
# equation when the separable case cannot be solved for y).
|
|
54
|
+
#
|
|
55
|
+
# Source: the textbook methods of [BD12]: separation of variables and
|
|
56
|
+
# the integrating factor (ch. 2), characteristic roots, undetermined
|
|
57
|
+
# coefficients and variation of parameters (ch. 3-4) (keys: MANUAL.md,
|
|
58
|
+
# Sources).
|
|
59
|
+
module ODE
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
def dsolve(equation, y, x)
|
|
63
|
+
return system(equation, y, x) if equation.is_a?(Array)
|
|
64
|
+
yv = Expression.lift(y)
|
|
65
|
+
xv = Expression.lift(x)
|
|
66
|
+
f = Solve.to_zero(equation).simplify
|
|
67
|
+
order = f.each_node.select { |n| n.is_a?(Derivative) && n.expr == yv }.map(&:order).max
|
|
68
|
+
raise ArgumentError, "#{equation} contains no derivative of #{yv}" if order.nil?
|
|
69
|
+
case order
|
|
70
|
+
when 1 then first_order(f, yv, xv)
|
|
71
|
+
else constant_coefficients(f, yv, xv, order)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ---- systems, linear with constant coefficients ----------------------------
|
|
76
|
+
#
|
|
77
|
+
# dsolve([eq(D(x, t), y), eq(D(y, t), -x)], [x, y], t)
|
|
78
|
+
#
|
|
79
|
+
# Written as u' = A u + c, the solution is a combination of exp(lambda*t)
|
|
80
|
+
# times the eigenvectors of A, with a conjugate pair giving the real pair
|
|
81
|
+
# exp(a*t)*(p*cos(b*t) - q*sin(b*t)) and its partner. A constant c adds the
|
|
82
|
+
# steady state -A^-1 c. A defective matrix (too few eigenvectors) is
|
|
83
|
+
# reported rather than guessed.
|
|
84
|
+
def system(equations, unknowns, t)
|
|
85
|
+
t = Expression.lift(t)
|
|
86
|
+
us = Array(unknowns).map { |u| Expression.lift(u) }
|
|
87
|
+
raise ArgumentError, "dsolve: #{equations.size} equations for #{us.size} unknowns" unless equations.size == us.size
|
|
88
|
+
derivatives = us.map { |u| Derivative.new(u, t) }
|
|
89
|
+
slots = us.each_index.map { |i| Var.new(:"_d#{i}") } # linear_system wants variables
|
|
90
|
+
pattern = derivatives.zip(slots).to_h
|
|
91
|
+
flattened = equations.map { |e| Solve.to_zero(e).simplify.subs(pattern) }
|
|
92
|
+
solved = Solve.linear_system(flattened, slots).first
|
|
93
|
+
raise RCAS::Unsupported, "dsolve: the system is not linear in the derivatives" if solved.nil? || solved.size != us.size
|
|
94
|
+
|
|
95
|
+
rows = []
|
|
96
|
+
forcing = []
|
|
97
|
+
slots.each do |d|
|
|
98
|
+
rhs = Expression.lift(solved[d]).expand
|
|
99
|
+
coefficients = us.map do |u|
|
|
100
|
+
c = Solve.polynomial_coefficients(rhs, u) or raise RCAS::Unsupported, "dsolve: #{rhs} is not linear in #{u}"
|
|
101
|
+
# x' = x**2 has the coefficients [0, 0, 1]: reading c[1] alone
|
|
102
|
+
# solved it as x' = 0 (third review, L5)
|
|
103
|
+
raise RCAS::Unsupported, "dsolve: #{rhs} is not linear in #{u}; only linear systems are solved" if c.size > 2
|
|
104
|
+
value = c[1] || Num.new(0)
|
|
105
|
+
raise RCAS::Unsupported, "dsolve: the coefficient #{value} is not constant" if Solve.depends?(value, t) || us.any? { |v| Solve.depends?(value, v) }
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
rows << coefficients
|
|
109
|
+
forcing << rhs.subs(us.to_h { |u| [u, Num.new(0)] }).simplify
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
matrix = MatrixSpace.new(RR, us.size, us.size).unchecked(rows)
|
|
113
|
+
solutions = homogeneous_system(matrix, t)
|
|
114
|
+
raise RCAS::Unsupported, "dsolve: no solution basis found for this system" if solutions.size < us.size
|
|
115
|
+
|
|
116
|
+
general = us.each_index.map do |i|
|
|
117
|
+
solutions.each_with_index.map { |vector, k| Var.new(:"C#{k + 1}") * vector[i] }.reduce(:+)
|
|
118
|
+
end
|
|
119
|
+
unless forcing.all? { |c| Scalar.zero?(c) }
|
|
120
|
+
steady = steady_state(matrix, forcing, t)
|
|
121
|
+
general = general.each_with_index.map { |g, i| g + steady[i] }
|
|
122
|
+
end
|
|
123
|
+
us.each_with_index.map { |u, i| Equation.new(u, general[i].simplify) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# One real solution vector per degree of freedom, as arrays of expressions.
|
|
127
|
+
def homogeneous_system(matrix, t)
|
|
128
|
+
out = []
|
|
129
|
+
seen = []
|
|
130
|
+
matrix.eigenvectors.each do |value, _multiplicity, vectors|
|
|
131
|
+
next if seen.any? { |v| Scalar.zero?((v - value).simplify) }
|
|
132
|
+
real, imaginary = ComplexParts.parts(Expression.lift(value))
|
|
133
|
+
vectors.each do |vector|
|
|
134
|
+
entries = vector.entries.map { |e| Expression.lift(e) }
|
|
135
|
+
if Scalar.zero?(imaginary)
|
|
136
|
+
out << entries.map { |e| (e * Fn.new(:exp, [value * t])).simplify }
|
|
137
|
+
out.concat(jordan_chain(matrix, value, entries, _multiplicity - vectors.size, t))
|
|
138
|
+
else
|
|
139
|
+
seen << Simplify.simplify(real - I * imaginary) # skip the conjugate
|
|
140
|
+
parts = entries.map { |e| ComplexParts.parts(e) }
|
|
141
|
+
wave = Fn.new(:exp, [real * t])
|
|
142
|
+
cosine = Fn.new(:cos, [imaginary * t])
|
|
143
|
+
sine = Fn.new(:sin, [imaginary * t])
|
|
144
|
+
out << parts.map { |p, q| (wave * (p * cosine - q * sine)).simplify }
|
|
145
|
+
out << parts.map { |p, q| (wave * (p * sine + q * cosine)).simplify }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
out
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A repeated eigenvalue with too few eigenvectors: (A - lambda)w_k = w_(k-1)
|
|
153
|
+
# with w_0 = v gives the chain, and the k-th solution is
|
|
154
|
+
# exp(lambda*t)*sum_j t**(k - j)/(k - j)! * w_j - every vector below it,
|
|
155
|
+
# not only the one before (the third one lost v; third review, L4).
|
|
156
|
+
def jordan_chain(matrix, value, vector, missing, t)
|
|
157
|
+
out = []
|
|
158
|
+
chain = [vector]
|
|
159
|
+
while out.size < missing
|
|
160
|
+
w = solve_singular(matrix, value, chain.last) or break
|
|
161
|
+
chain << w
|
|
162
|
+
k = chain.size - 1
|
|
163
|
+
combination = w.each_index.map do |i|
|
|
164
|
+
chain.each_with_index.map { |c, j| c[i] * t**(k - j) / RCAS.factorial(k - j) }.reduce(:+).simplify
|
|
165
|
+
end
|
|
166
|
+
out << combination.map { |e| (e * Fn.new(:exp, [value * t])).simplify }
|
|
167
|
+
end
|
|
168
|
+
out
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A particular solution of (A - lambda I) w = v, free variables set to zero.
|
|
172
|
+
def solve_singular(matrix, value, vector)
|
|
173
|
+
n = matrix.rows
|
|
174
|
+
ws = (0...n).map { |i| Var.new(:"_w#{i}") }
|
|
175
|
+
equations = (0...n).map do |i|
|
|
176
|
+
row = (0...n).map { |j| (matrix[i, j] - (i == j ? value : Num.new(0))) * ws[j] }.reduce(:+)
|
|
177
|
+
(row - vector[i]).simplify
|
|
178
|
+
end
|
|
179
|
+
solution = Solve.linear_system(equations, ws).first
|
|
180
|
+
return nil if solution.nil?
|
|
181
|
+
zeros = ws.to_h { |w| [w, Num.new(0)] }
|
|
182
|
+
ws.map { |w| Expression.lift(solution[w] || Num.new(0)).subs(zeros).simplify }
|
|
183
|
+
rescue StandardError => rescued
|
|
184
|
+
RCAS.guard!(rescued)
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The constant solution of u' = A u + c.
|
|
189
|
+
def steady_state(matrix, forcing, t)
|
|
190
|
+
raise RCAS::Unsupported, "dsolve: a forcing term depending on #{t} is not supported for systems" if forcing.any? { |c| Solve.depends?(c, t) }
|
|
191
|
+
raise RCAS::Unsupported, "dsolve: the matrix is singular, so there is no constant solution" if Scalar.zero?(matrix.det)
|
|
192
|
+
matrix.solve(forcing.map { |c| Simplify.negate(c).simplify }).entries
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ---- first order ---------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
def first_order(f, y, x)
|
|
198
|
+
dy = Var.new(:_dy)
|
|
199
|
+
coeffs = Solve.polynomial_coefficients(f.subs(Derivative.new(y, x) => dy), dy)
|
|
200
|
+
raise RCAS::Unsupported, "the equation must be linear in D(#{y}, #{x})" unless coeffs && coeffs.size == 2
|
|
201
|
+
rhs = (-coeffs[0] / coeffs[1]).simplify # y' = rhs(x, y)
|
|
202
|
+
# linear first: y' = y separated is log|y| = x + C, and exp(C1 + x)
|
|
203
|
+
# misses y = 0 and every negative solution, where the linear rule
|
|
204
|
+
# gives C1*exp(x) (third review, 3.6)
|
|
205
|
+
linear(rhs, y, x) || separable(rhs, y, x) ||
|
|
206
|
+
raise(RCAS::Unsupported, "#{y}' = #{rhs} is neither separable nor linear")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# y' = g(x) * h(y)
|
|
210
|
+
def separable(rhs, y, x)
|
|
211
|
+
coeff, factors = Simplify.factorize(rhs)
|
|
212
|
+
gx = {}
|
|
213
|
+
hy = {}
|
|
214
|
+
factors.each do |base, exp|
|
|
215
|
+
in_y = Solve.depends?(base, y) || (exp.is_a?(Expression) && Solve.depends?(exp, y))
|
|
216
|
+
in_x = Solve.depends?(base, x) || (exp.is_a?(Expression) && Solve.depends?(exp, x))
|
|
217
|
+
return nil if in_x && in_y
|
|
218
|
+
(in_y ? hy : gx)[base] = exp
|
|
219
|
+
end
|
|
220
|
+
g = Simplify.rebuild_product(coeff, gx)
|
|
221
|
+
h = Simplify.rebuild_product(1, hy)
|
|
222
|
+
left = Integrate.integrate(1 / h, y)
|
|
223
|
+
right = Integrate.integrate(g, x)
|
|
224
|
+
return nil unless Integrate.complete?(left) && Integrate.complete?(right)
|
|
225
|
+
|
|
226
|
+
c1 = Var.new(:C1)
|
|
227
|
+
implicit = (left - right - c1).simplify
|
|
228
|
+
begin
|
|
229
|
+
Solve.univariate(implicit, y, 1).map { |s| Equation.new(y, s.simplify) }
|
|
230
|
+
rescue NotImplementedError, RCAS::Unsupported, ArgumentError
|
|
231
|
+
[Equation.new(left.simplify, (right + c1).simplify)]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# y' = q(x) - p(x) * y
|
|
236
|
+
def linear(rhs, y, x)
|
|
237
|
+
coeffs = Solve.polynomial_coefficients(rhs, y)
|
|
238
|
+
return nil unless coeffs && coeffs.size == 2
|
|
239
|
+
q = coeffs[0]
|
|
240
|
+
p = (-coeffs[1]).simplify
|
|
241
|
+
mu = Fn.new(:exp, [Integrate.integrate(p, x)]).simplify
|
|
242
|
+
integral = Integrate.integrate((q * mu).simplify, x)
|
|
243
|
+
c1 = Var.new(:C1)
|
|
244
|
+
[Equation.new(y, ((integral + c1) / mu).simplify)]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ---- linear, constant coefficients, any order ----------------------------
|
|
248
|
+
#
|
|
249
|
+
# a_n y^(n) + ... + a_1 y' + a_0 y = g(x)
|
|
250
|
+
#
|
|
251
|
+
# Homogeneous part from the roots of the characteristic polynomial (real
|
|
252
|
+
# roots and conjugate pairs, with multiplicity); particular solution by
|
|
253
|
+
# undetermined coefficients when g is a sum of terms
|
|
254
|
+
# polynomial * exp(a x) * (cos(b x) | sin(b x)), otherwise by variation
|
|
255
|
+
# of parameters for second-order equations.
|
|
256
|
+
def constant_coefficients(f, y, x, n)
|
|
257
|
+
ds = (0..n).map { |k| Var.new(:"_d#{k}") }
|
|
258
|
+
pattern = { y => ds[0] }
|
|
259
|
+
(1..n).each { |k| pattern[Derivative.new(y, x, k)] = ds[k] }
|
|
260
|
+
g = f.subs(pattern)
|
|
261
|
+
raise RCAS::Unsupported, "only linear equations with constant coefficients are supported" unless Solve.linear_in?(g, ds)
|
|
262
|
+
|
|
263
|
+
coeffs = ds.map { |d| Solve.polynomial_coefficients(g, d)[1] || Num.new(0) }
|
|
264
|
+
raise ArgumentError, "no derivative of order #{n} in the equation" if Scalar.zero?(coeffs[n])
|
|
265
|
+
coeffs.each do |k|
|
|
266
|
+
raise RCAS::Unsupported, "coefficient #{k} is not constant" if Solve.depends?(k, x) || Solve.depends?(k, y)
|
|
267
|
+
end
|
|
268
|
+
forcing = Simplify.negate(g.subs(ds.to_h { |d| [d, Num.new(0)] })).simplify
|
|
269
|
+
|
|
270
|
+
groups = root_groups(Solve.polynomial_roots(coeffs))
|
|
271
|
+
homogeneous = homogeneous_solution(groups, x)
|
|
272
|
+
particular =
|
|
273
|
+
if Scalar.zero?(forcing)
|
|
274
|
+
Num.new(0)
|
|
275
|
+
else
|
|
276
|
+
undetermined_coefficients(coeffs, forcing, x) ||
|
|
277
|
+
(n == 2 && variation_of_parameters(coeffs, forcing, groups, x)) ||
|
|
278
|
+
raise(RCAS::Unsupported, "no method for the forcing term #{forcing}")
|
|
279
|
+
end
|
|
280
|
+
[Equation.new(y, (homogeneous + particular).simplify)]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Roots with multiplicity => [[re, im, multiplicity], ...]; im is nil when
|
|
284
|
+
# the root cannot be split into real and imaginary part (symbolic
|
|
285
|
+
# coefficients, RootOf); conjugate pairs are merged (im > 0) and the
|
|
286
|
+
# groups ordered by their real part.
|
|
287
|
+
def root_groups(roots)
|
|
288
|
+
tally = []
|
|
289
|
+
roots.map(&:simplify).each do |r|
|
|
290
|
+
entry = tally.find { |root, _| root == r }
|
|
291
|
+
entry ? entry[1] += 1 : tally << [r, 1]
|
|
292
|
+
end
|
|
293
|
+
groups = tally.map do |r, m|
|
|
294
|
+
re, im = real_imaginary(r)
|
|
295
|
+
re.nil? || Scalar.zero?(im) ? [r, nil, m] : [re, im, m]
|
|
296
|
+
end
|
|
297
|
+
merged = []
|
|
298
|
+
until groups.empty?
|
|
299
|
+
re, im, m = groups.shift
|
|
300
|
+
if im
|
|
301
|
+
j = groups.index { |re2, im2, m2| im2 && m2 == m && Scalar.zero?(re - re2) && Scalar.zero?(im + im2) }
|
|
302
|
+
if j.nil?
|
|
303
|
+
# no conjugate (complex coefficients): exp(r*x) on its own, not
|
|
304
|
+
# a cos/sin pair, which would give the equation two constants
|
|
305
|
+
# too many (third review, L6)
|
|
306
|
+
merged << [(re + I * im).simplify, nil, m]
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
groups.delete_at(j)
|
|
310
|
+
im = Simplify.negate(im).simplify if evalf_or_nil(im).to_f.negative?
|
|
311
|
+
end
|
|
312
|
+
merged << [re, im, m]
|
|
313
|
+
end
|
|
314
|
+
# Numbering order: the zero root (C1 + C2*x) first, then by real part, symbolic roots last.
|
|
315
|
+
merged.sort_by do |re, im, _|
|
|
316
|
+
value = evalf_or_nil(re)
|
|
317
|
+
[value ? 0 : 1, Scalar.zero?(re) ? -Float::INFINITY : value.to_f, im ? 1 : 0, im ? evalf_or_nil(im).to_f : 0.0, re.to_s]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def evalf_or_nil(e)
|
|
322
|
+
v = e.evalf
|
|
323
|
+
v.is_a?(Numeric) && !v.is_a?(Complex) ? v : nil
|
|
324
|
+
rescue StandardError => rescued
|
|
325
|
+
RCAS.guard!(rescued)
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# -1 + 2*i => [-1, 2]; nil if some non-numeric factor may be complex.
|
|
330
|
+
def real_imaginary(r)
|
|
331
|
+
constant, table = Expand.table(r)
|
|
332
|
+
re = {}
|
|
333
|
+
im = {}
|
|
334
|
+
table.each do |factors, coeff|
|
|
335
|
+
return nil if factors.any? { |base, e| Simplify.imaginary_unit?(base) || (e.is_a?(Expression) && !e.variables.empty?) }
|
|
336
|
+
re[factors] = coeff.is_a?(Complex) ? coeff.real : coeff
|
|
337
|
+
im[factors] = coeff.is_a?(Complex) ? coeff.imaginary : 0
|
|
338
|
+
end
|
|
339
|
+
c_re = constant.is_a?(Complex) ? constant.real : constant
|
|
340
|
+
c_im = constant.is_a?(Complex) ? constant.imaginary : 0
|
|
341
|
+
[Simplify.rebuild_sum(c_re, re).simplify, Simplify.rebuild_sum(c_im, im).simplify]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Sum over the root groups of C_i * x**j * exp(re x) [* cos/sin(im x)],
|
|
345
|
+
# arranged the textbook way: exp(x)*(C1 + C2*x), exp(-x)*(C1*cos(2*x) + C2*sin(2*x)).
|
|
346
|
+
def homogeneous_solution(groups, x)
|
|
347
|
+
counter = 0
|
|
348
|
+
constant = -> { counter += 1; Var.new(:"C#{counter}") }
|
|
349
|
+
polynomial = ->(m) { (0...m).map { |j| j.zero? ? constant.call : constant.call * x**j }.reduce(:+) }
|
|
350
|
+
groups.map do |re, im, m|
|
|
351
|
+
exponential = Scalar.zero?(re) ? nil : Fn.new(:exp, [re * x])
|
|
352
|
+
body = im ? polynomial.call(m) * Fn.new(:cos, [im * x]) + polynomial.call(m) * Fn.new(:sin, [im * x]) : polynomial.call(m)
|
|
353
|
+
exponential ? body * exponential : body
|
|
354
|
+
end.reduce(:+)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# The fundamental system as a flat list of functions.
|
|
358
|
+
def fundamental_system(groups, x)
|
|
359
|
+
groups.flat_map do |re, im, m|
|
|
360
|
+
exponential = Scalar.zero?(re) ? Num.new(1) : Fn.new(:exp, [re * x])
|
|
361
|
+
(0...m).flat_map do |j|
|
|
362
|
+
base = x**j * exponential
|
|
363
|
+
im ? [base * Fn.new(:cos, [im * x]), base * Fn.new(:sin, [im * x])] : [base]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def apply_operator(coeffs, u, x)
|
|
369
|
+
coeffs.each_with_index.map { |a, k| a * u.diff(x, k) }.reduce(:+)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Undetermined coefficients [BD12, §3.5, §4.3]: the forcing term is sorted
|
|
373
|
+
# into classes (a, b) with a term x**k * exp(a x) * cos/sin(b x); the ansatz
|
|
374
|
+
# for a class is x**m * (A_0 + ... + A_d x**d) * exp(a x) * (cos, sin)
|
|
375
|
+
# where m is the multiplicity of a + i b as a characteristic root.
|
|
376
|
+
def undetermined_coefficients(coeffs, forcing, x)
|
|
377
|
+
classes = forcing_classes(forcing, x) or return nil
|
|
378
|
+
r = Var.new(:_r)
|
|
379
|
+
characteristic = coeffs.each_with_index.map { |a, k| a * r**k }.reduce(:+)
|
|
380
|
+
unknowns = []
|
|
381
|
+
new_unknown = -> { unknowns << Var.new(:"_A#{unknowns.size}"); unknowns.last }
|
|
382
|
+
ansatz = classes.map do |(a, b), degree|
|
|
383
|
+
m = root_multiplicity(characteristic, r, Scalar.zero?(b) ? a : a + I * b)
|
|
384
|
+
polynomial = -> { (0..degree).map { |k| new_unknown.call * x**(k + m) }.reduce(:+) }
|
|
385
|
+
exponential = Scalar.zero?(a) ? Num.new(1) : Fn.new(:exp, [a * x])
|
|
386
|
+
if Scalar.zero?(b)
|
|
387
|
+
polynomial.call * exponential
|
|
388
|
+
else
|
|
389
|
+
(polynomial.call * Fn.new(:cos, [b * x]) + polynomial.call * Fn.new(:sin, [b * x])) * exponential
|
|
390
|
+
end
|
|
391
|
+
end.reduce(:+)
|
|
392
|
+
residual = (apply_operator(coeffs, ansatz, x) - forcing).expand
|
|
393
|
+
equations = collect_by_function(residual, unknowns, x)
|
|
394
|
+
solution = Solve.linear_system(equations, unknowns).first or return nil
|
|
395
|
+
unknowns.each { |u| solution[u] ||= Num.new(0) }
|
|
396
|
+
ansatz.subs(solution).simplify
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# { [a, b] => degree } for a forcing term in the class of the method; nil otherwise.
|
|
400
|
+
def forcing_classes(forcing, x)
|
|
401
|
+
constant, terms = Simplify.termize(forcing, simplify: true)
|
|
402
|
+
classes = {}
|
|
403
|
+
add = lambda do |a, b, degree|
|
|
404
|
+
key = classes.keys.find { |a2, b2| Scalar.zero?(a - a2) && (Scalar.zero?(b - b2) || Scalar.zero?(b + b2)) }
|
|
405
|
+
key ||= [a, b]
|
|
406
|
+
classes[key] = [classes[key] || 0, degree].max
|
|
407
|
+
end
|
|
408
|
+
add.call(Num.new(0), Num.new(0), 0) unless constant.zero?
|
|
409
|
+
terms.each_key do |factors|
|
|
410
|
+
a = Num.new(0)
|
|
411
|
+
b = Num.new(0)
|
|
412
|
+
degree = 0
|
|
413
|
+
factors.each do |base, e|
|
|
414
|
+
if base == x
|
|
415
|
+
return nil unless e.is_a?(Integer) && e >= 0
|
|
416
|
+
degree += e
|
|
417
|
+
elsif base == Simplify.exp_base
|
|
418
|
+
_, slope = linear_coefficients(Expression.lift(e), x)
|
|
419
|
+
return nil unless slope
|
|
420
|
+
a = (a + slope).simplify
|
|
421
|
+
elsif base.is_a?(Fn) && %i[cos sin].include?(base.name) && e == 1 && Solve.depends?(base, x)
|
|
422
|
+
offset, slope = linear_coefficients(base.args.first, x)
|
|
423
|
+
return nil unless slope && Scalar.zero?(offset) && Scalar.zero?(b)
|
|
424
|
+
b = slope
|
|
425
|
+
elsif Solve.depends?(base, x) || (e.is_a?(Expression) && Solve.depends?(e, x))
|
|
426
|
+
return nil
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
add.call(a, b, degree)
|
|
430
|
+
end
|
|
431
|
+
classes
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# u = c0 + c1 x => [c0, c1]; [nil, nil] unless u is linear in x.
|
|
435
|
+
def linear_coefficients(u, x)
|
|
436
|
+
cs = Solve.polynomial_coefficients(u, x)
|
|
437
|
+
return [nil, nil] if cs.nil? || cs.size > 2
|
|
438
|
+
[cs[0], cs[1] || Num.new(0)]
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def root_multiplicity(characteristic, r, s)
|
|
442
|
+
m = 0
|
|
443
|
+
p = characteristic
|
|
444
|
+
while Scalar.zero?(p.subs(r => s).expand)
|
|
445
|
+
m += 1
|
|
446
|
+
p = p.diff(r)
|
|
447
|
+
end
|
|
448
|
+
m
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Group the terms of a residual linear in the unknowns by the function
|
|
452
|
+
# they multiply (x**k exp(a x) cos(b x), ...). exp(c0 + c1 x) is split so
|
|
453
|
+
# that exp(1 + x) and exp(x) land in the same group.
|
|
454
|
+
def collect_by_function(residual, unknowns, x)
|
|
455
|
+
constant, table = Expand.table(residual)
|
|
456
|
+
groups = { Num.new(1) => [Num.new(constant)] }
|
|
457
|
+
table.each do |factors, coeff|
|
|
458
|
+
coefficient = Num.new(coeff)
|
|
459
|
+
function = {}
|
|
460
|
+
factors.each do |base, e|
|
|
461
|
+
if unknowns.include?(base)
|
|
462
|
+
coefficient *= base
|
|
463
|
+
elsif base == Simplify.exp_base
|
|
464
|
+
offset, slope = linear_coefficients(Expression.lift(e), x)
|
|
465
|
+
coefficient *= Fn.new(:exp, [offset]) if offset && !Scalar.zero?(offset)
|
|
466
|
+
function[base] = slope ? (slope * x).expand : e
|
|
467
|
+
elsif Solve.depends?(base, x) || (e.is_a?(Expression) && Solve.depends?(e, x))
|
|
468
|
+
function[base] = e
|
|
469
|
+
else
|
|
470
|
+
coefficient *= Simplify.power_node(base, e) # a parameter
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
key = Simplify.rebuild_product(1, function).simplify
|
|
474
|
+
(groups[key] ||= []) << coefficient
|
|
475
|
+
end
|
|
476
|
+
groups.values.map { |parts| parts.reduce(:+) }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Variation of parameters [BD12, §3.6] for a y'' + b y' + c y = g:
|
|
480
|
+
# y_p = -y1 * int(y2 g / (a W)) + y2 * int(y1 g / (a W)), W = y1 y2' - y1' y2
|
|
481
|
+
def variation_of_parameters(coeffs, forcing, groups, x)
|
|
482
|
+
y1, y2 = fundamental_system(groups, x)
|
|
483
|
+
wronskian = Trigonometry.trigsimp((y1 * y2.diff(x) - y1.diff(x) * y2).expand)
|
|
484
|
+
scaled = (forcing / (coeffs[2] * wronskian)).simplify
|
|
485
|
+
(-y1 * Integrate.integrate((y2 * scaled).simplify, x) + y2 * Integrate.integrate((y1 * scaled).simplify, x)).simplify
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|