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