rcas 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
data/lib/rcas/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