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,726 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ OO = Const.new(:oo, Float::INFINITY)
5
+ register_infinity
6
+
7
+ # Unevaluated limit.
8
+ class Limit < Expression
9
+ attr_reader :expr, :var, :point
10
+
11
+ def initialize(expr, var, point)
12
+ @expr = expr
13
+ @var = var
14
+ @point = point
15
+ freeze
16
+ end
17
+
18
+ def bound_variable = var
19
+ def children = [expr, var, point]
20
+ def rebuild(expr, var, point) = Limit.new(expr, var, point)
21
+ def to_sexp = [:limit, expr.to_sexp, var.to_sexp, point.to_sexp]
22
+ end
23
+
24
+ class SeriesError < StandardError; end
25
+
26
+ # Truncated Puiseux series in a local variable t: sum of coeff * t**exp for
27
+ # rational exponents below +order+. Coefficients are Expressions and may
28
+ # contain the marker variable LOG for log(t).
29
+ #
30
+ # Sources: series arithmetic (product, quotient, composition, powers) as
31
+ # in [Knu98, §4.7]. Limits take the leading term of this expansion at the
32
+ # point; that is the textbook strategy, not Gruntz's MRV algorithm
33
+ # [Gru96]. What has no series at the point is left to the squeeze rule
34
+ # and to the fallbacks in Limits. Keys: MANUAL.md, Sources.
35
+ class Series
36
+ LOG = Var.new(:_L)
37
+
38
+ attr_reader :terms, :order
39
+
40
+ def initialize(terms, order)
41
+ @order = terms.is_a?(Hash) ? order : order
42
+ @terms = terms.select { |e, c| e < order && !Scalar.zero?(c) }.sort.to_h.freeze
43
+ freeze
44
+ end
45
+
46
+ def self.constant(c, order)
47
+ c = Expression.lift(c)
48
+ new(Scalar.zero?(c) ? {} : { 0 => c }, order)
49
+ end
50
+
51
+ def self.variable(order) = new({ 1 => Num.new(1) }, order)
52
+
53
+ def zero? = terms.empty?
54
+ def min_exponent = terms.keys.min
55
+ def low = min_exponent || order
56
+ def constant_term = terms[0] || Num.new(0)
57
+ def leading = terms.first
58
+
59
+ def +(other) = Series.new(terms.merge(other.terms) { |_, a, b| (a + b).simplify }, [order, other.order].min)
60
+ def -(other) = self + other.scale(Num.new(-1))
61
+ def scale(c) = Series.new(terms.transform_values { |v| (v * c).simplify }, order)
62
+ def shift(k) = Series.new(terms.to_h { |e, c| [norm(e + k), c] }, norm(order + k))
63
+ def truncate(n) = Series.new(terms, [order, n].min)
64
+
65
+ def *(other)
66
+ new_order = [order + other.low, other.order + low].min
67
+ out = {}
68
+ terms.each do |e1, c1|
69
+ other.terms.each do |e2, c2|
70
+ e = norm(e1 + e2)
71
+ next if e >= new_order
72
+ out[e] = out[e] ? (out[e] + c1 * c2).simplify : (c1 * c2).simplify
73
+ end
74
+ end
75
+ Series.new(out, new_order)
76
+ end
77
+
78
+ def **(n)
79
+ return inverse**(-n) if n.negative?
80
+ result = Series.constant(1, order + low * [n - 1, 0].max)
81
+ n.times { result *= self }
82
+ result
83
+ end
84
+
85
+ # 1 / self: pull out c*t**m, then a geometric series in the rest.
86
+ def inverse
87
+ raise SeriesError, "cannot invert a series that is zero to the known order" if zero?
88
+ m = min_exponent
89
+ c = terms[m]
90
+ u = shift(-m).scale((Num.new(1) / c).simplify) - Series.constant(1, order - m)
91
+ Series.compose(u, order - m) { |k| Num.new((-1)**k) }.scale((Num.new(1) / c).simplify).shift(-m)
92
+ end
93
+
94
+ # sum_{k>=0} coeff(k) * u**k, u with positive minimal exponent.
95
+ def self.compose(u, order)
96
+ c0 = Expression.lift(yield(0))
97
+ # u is known only to its own order, and so is anything built from it:
98
+ # sqrt(x**2 + x) at oo claimed O(1/x**3) with the 1/2 missing (D6)
99
+ order = [order, u.order].min
100
+ return Series.constant(c0, order) if u.zero?
101
+ mu = u.min_exponent
102
+ raise SeriesError, "composition needs a series without constant term" unless mu.positive?
103
+ kmax = (order / mu).ceil
104
+ acc = Series.constant(c0, order)
105
+ pow = Series.constant(1, order)
106
+ (1..kmax).each do |k|
107
+ pow = (pow * u).truncate(order)
108
+ break if pow.zero?
109
+ acc += pow.scale(Expression.lift(yield(k)))
110
+ end
111
+ acc
112
+ end
113
+
114
+ def to_expr(t, log_of_t)
115
+ sum = terms.reduce(Num.new(0)) { |acc, (e, c)| acc + c * Simplify.power_node(t, e) }
116
+ sum.subs(LOG => log_of_t).simplify
117
+ end
118
+
119
+ private
120
+
121
+ def norm(e) = Simplify.normalize_number(e.is_a?(Rational) ? e : Rational(e))
122
+ end
123
+
124
+ # Series expansion and limits.
125
+ #
126
+ # series(sin(x), x, 0, 6) # => x - x**3/6 + x**5/120 + O(x**6)
127
+ # limit(sin(x)/x, x, 0) # => 1
128
+ # limit((1 + 1/x)**x, x, oo) # => e
129
+ module Limits
130
+ module_function
131
+
132
+ T = Var.new(:_t)
133
+
134
+ def series(f, x, a = 0, n = 6, big_o: true)
135
+ f = Expression.lift(f)
136
+ x = Expression.lift(x)
137
+ a = Expression.lift(a)
138
+ g, back, oh = localize(f, x, a)
139
+ s = adaptive(g, n)
140
+ result = back.call(s.to_expr(T, Fn.new(:log, [T])))
141
+ result = result + Fn.new(:O, [oh.call(s.order)]) if big_o
142
+ result.simplify
143
+ end
144
+
145
+ def taylor(f, x, a = 0, n = 6) = series(f, x, a, n, big_o: false)
146
+
147
+ # dir: :both (default for finite points), :right or :left
148
+ def limit(f, x, a, dir = nil)
149
+ f = Expression.lift(f)
150
+ x = Expression.lift(x)
151
+ a = Expression.lift(a)
152
+ f = Piecewises.hoist(f)
153
+ return Piecewises.limit(f, x, a, dir) if f.is_a?(Piecewise)
154
+ known = IntegralFunctions.limit(f, x, a)
155
+ return known if known
156
+ if infinite?(a) && (through = through_exponential(f, x, a))
157
+ return through
158
+ end
159
+ # erf(u) = 1 - erfc(u): as a constant up to an exponentially small tail
160
+ # erf lost that tail, which exp(x**2) multiplies back - x*(1 - erf(x))*
161
+ # exp(x**2) came out as oo, not 1/sqrt(pi) (fourth review, D4)
162
+ if infinite?(a) && f.each_node.any? { |n| n.is_a?(Fn) && n.name == :erf }
163
+ tail = f.subs(f.each_node.select { |n| n.is_a?(Fn) && n.name == :erf }.uniq.to_h { |n| [n, Num.new(1) - Fn.new(:erfc, n.args)] }).simplify
164
+ found = limit(tail, x, a, dir)
165
+ return found unless found.is_a?(Limit)
166
+ end
167
+ if (through = through_logarithm(f, x, a, dir))
168
+ return through
169
+ end
170
+ g, = localize(f, x, a)
171
+ side = infinite?(a) ? :right : (dir || :both)
172
+ if side == :both
173
+ right = one_sided(g, :right)
174
+ left = one_sided(g, :left)
175
+ return right if right == left || (right.is_a?(Num) && left.is_a?(Num) && right == left)
176
+ return Limit.new(f, x, a)
177
+ end
178
+ one_sided(g, side)
179
+ rescue SeriesError, ZeroDivisionError, NotImplementedError, RCAS::Unsupported
180
+ Limit.new(f, x, a)
181
+ rescue ArgumentError => e
182
+ # An integrand rcas cannot differentiate reaches here through the
183
+ # series expansion; "don't know the derivative of floor" is the
184
+ # l'Hopital attempt talking, not an answer about the limit.
185
+ raise unless e.message.start_with?(Integrate::NO_DERIVATIVE)
186
+ Limit.new(f, x, a)
187
+ end
188
+
189
+ # f depends on x only through exp(a_i*x + b_i) with rational a_i: with
190
+ # u = exp(l*x), l the greatest common measure of the a_i, it is a
191
+ # function of u alone, and x -> +-oo is u -> oo or u -> 0+. The logistic
192
+ # exp(x)/(1 + exp(x)) tends to 1 that way; the series at infinity meets
193
+ # exp(1/t), which has none, and discuss reported "no asymptote" for a
194
+ # limit it had not taken (fourth review, S11). nil when f is not such a
195
+ # function or the limit in u is not taken either.
196
+ def through_exponential(f, x, a)
197
+ exps = f.each_node.select { |n| n.is_a?(Fn) && n.name == :exp && n.args.first.variables.include?(x.name) }.uniq
198
+ return nil if exps.empty?
199
+ slopes = exps.map do |e|
200
+ coefficients = Solve.polynomial_coefficients(e.args.first, x)
201
+ return nil unless coefficients&.size == 2 && coefficients.all? { |c| !c.variables.include?(x.name) }
202
+ slope = coefficients[1].simplify
203
+ return nil unless slope.is_a?(Num) && (slope.value.is_a?(Integer) || slope.value.is_a?(Rational))
204
+ [e, slope.value.to_r, coefficients[0]]
205
+ end
206
+ measure = slopes.map { |_, m, _| m.abs }.reduce { |g, m| Rational(g.numerator.gcd(m.numerator), g.denominator.lcm(m.denominator)) }
207
+ u = Var.new(:_u)
208
+ replaced = slopes.reduce(f) { |acc, (e, m, b)| acc.subs(e => Fn.new(:exp, [b]) * u**Num.new(m / measure)) }.simplify
209
+ return nil if replaced.variables.include?(x.name)
210
+ value = a == OO ? limit(replaced, u, OO) : limit(replaced, u, Num.new(0), :right)
211
+ value.is_a?(Limit) ? nil : value
212
+ rescue SeriesError, ZeroDivisionError, ArgumentError, NotImplementedError, RCAS::Unsupported
213
+ nil
214
+ end
215
+
216
+ # f depends on x only through log(x): with u = log(x), x -> 0+ is
217
+ # u -> -oo and x -> oo is u -> oo. 1/log(x) tends to 0 at 0 from the
218
+ # right, which the series cannot say (1/log(t) is below every constant
219
+ # and above every power). Only from the right at 0: on the left log(x)
220
+ # is not real.
221
+ def through_logarithm(f, x, a, dir)
222
+ target = if a == OO then OO
223
+ elsif a.is_a?(Num) && a.value.zero? && dir == :right then Neg.new(OO).simplify
224
+ end
225
+ return nil if target.nil?
226
+ logs = f.each_node.select { |n| n.is_a?(Fn) && n.name == :log && n.args.first.variables.include?(x.name) }.uniq
227
+ return nil unless logs == [Fn.new(:log, [x])]
228
+ u = Var.new(:_v)
229
+ replaced = f.subs(logs.first => u)
230
+ return nil if replaced.variables.include?(x.name)
231
+ value = limit(replaced, u, target)
232
+ value.is_a?(Limit) ? nil : value
233
+ rescue SeriesError, ZeroDivisionError, ArgumentError, NotImplementedError, RCAS::Unsupported
234
+ nil
235
+ end
236
+
237
+ # ---- helpers -----------------------------------------------------------
238
+
239
+ def infinite?(a) = a == OO || a == Neg.new(OO)
240
+
241
+ # [g(t), back-substitution, O-term builder] with the limit at t -> 0+
242
+ def localize(f, x, a)
243
+ if a == OO
244
+ [f.subs(x => 1 / T), ->(e) { e.subs(T => 1 / x) }, ->(o) { Pow.new(x, Num.new(-o)) }]
245
+ elsif a == Neg.new(OO)
246
+ [f.subs(x => -1 / T), ->(e) { e.subs(T => -1 / x) }, ->(o) { Pow.new(x, Num.new(-o)) }]
247
+ else
248
+ shift = Scalar.zero?(a) ? x : x - a
249
+ [f.subs(x => T + a), ->(e) { e.subs(T => shift) }, ->(o) { Simplify.power_node(shift, o) }]
250
+ end
251
+ end
252
+
253
+ def adaptive(g, n)
254
+ order = n
255
+ s = nil
256
+ 6.times do
257
+ s = expand(g, order)
258
+ return s.truncate(n) if s.order >= n
259
+ order += (n - s.order) + 1
260
+ end
261
+ s
262
+ end
263
+
264
+ def one_sided(g, side)
265
+ outer = Thread.current[:rcas_one_sided]
266
+ Thread.current[:rcas_one_sided] = true
267
+ one_sided_limit(g, side)
268
+ ensure
269
+ Thread.current[:rcas_one_sided] = outer
270
+ end
271
+
272
+ def one_sided_limit(g, side)
273
+ g = g.subs(T => -T) if side == :left
274
+ s = nil
275
+ begin
276
+ [4, 8, 16, 32].each do |n|
277
+ s = adaptive(g, n)
278
+ break unless s.zero?
279
+ end
280
+ rescue SeriesError
281
+ return squeeze(g) || termwise(g) || dominant(g) || exponential_fallback(g)
282
+ end
283
+ return Num.new(0) if s.zero?
284
+
285
+ e, c = s.leading
286
+ if c.variables.include?(Series::LOG.name)
287
+ m = Var.new(:_M)
288
+ poly = Solve.polynomial_coefficients(c.subs(Series::LOG => -m).expand, m) or raise SeriesError, "log coefficient"
289
+ degree = poly.size - 1
290
+ lc = poly.last
291
+ return Num.new(0) if e.positive?
292
+ return c.simplify if e.zero? && degree.zero?
293
+ return signed_infinity(lc)
294
+ end
295
+ return Num.new(0) if e.positive?
296
+ return c.simplify if e.zero?
297
+ signed_infinity(c)
298
+ end
299
+
300
+ # Functions whose real values stay in a bounded interval.
301
+ BOUNDED = %i[sin cos sign atan erf erfc tanh].freeze
302
+
303
+ # The squeeze rule [Rud76, th. 3.19]: |sin(u)| <= 1, so a bounded factor
304
+ # times a factor that tends to zero tends to zero, however wildly the
305
+ # first one oscillates. x*sin(1/x) at 0 and exp(-x)*cos(3*x) at infinity
306
+ # need this - the oscillating factor has no series at the point, so the
307
+ # expansion above cannot see them. nil when the rule does not apply.
308
+ def squeeze(g)
309
+ coeff, factors = Simplify.factorize(g)
310
+ bounded, rest = factors.partition { |base, exp| bounded?(base, exp) }
311
+ return nil unless bounded.any? { |base, _| base.variables.include?(T.name) }
312
+ value = one_sided(Simplify.rebuild_product(coeff, rest.to_h), :right)
313
+ value.is_a?(Num) && value.value.zero? ? Num.new(0) : nil
314
+ rescue SeriesError, ZeroDivisionError
315
+ nil
316
+ end
317
+
318
+ # A non-negative integer power of a bounded function is bounded again.
319
+ def bounded?(base, exponent)
320
+ base.is_a?(Fn) && BOUNDED.include?(base.name) && base.args.size == 1 &&
321
+ exponent.is_a?(Integer) && !exponent.negative?
322
+ end
323
+
324
+ # exp(-1/t)*t and friends: look at log(g) instead, whose leading term
325
+ # decides between 0, oo and exp(finite).
326
+ def exponential_fallback(g)
327
+ coeff, factors = Simplify.factorize(g)
328
+ exponential = factors.any? { |base, exp| base == Simplify.exp_base || (base.is_a?(Fn) && base.name == :exp) || (exp.is_a?(Expression) && exp.variables.include?(T.name)) }
329
+ raise SeriesError, "no exponential factor in #{g}" unless exponential
330
+ negative = Simplify.negative?(coeff)
331
+ positive = negative ? Simplify.rebuild_product(-coeff, factors) : g
332
+ value = one_sided(log_of(positive), :right)
333
+ raise SeriesError, "unexpected log limit #{value}" if value.is_a?(Limit)
334
+ result =
335
+ if value == Neg.new(OO) then Num.new(0)
336
+ elsif value == OO then OO
337
+ else Fn.new(:exp, [value]).simplify
338
+ end
339
+ negative ? Neg.new(result).simplify : result
340
+ end
341
+
342
+ # The limit of a sum is the sum of the limits when every term has a
343
+ # finite one (exp(-1/t)/t + erf(1/t) at t -> 0+, say); nil otherwise, and
344
+ # nil rather than the error when a term has no series at all, so that the
345
+ # rules after this one still get their turn.
346
+ def termwise(g)
347
+ constant, terms = sum_terms(g) || return
348
+ total = Num.new(constant)
349
+ terms.each do |factors, coeff|
350
+ value = one_sided(Simplify.rebuild_product(coeff, factors), :right)
351
+ return nil if value.is_a?(Limit) || infinite?(value) || value.each_node.any? { |n| n == OO }
352
+ total += value
353
+ end
354
+ total.simplify
355
+ rescue SeriesError, ZeroDivisionError
356
+ nil
357
+ end
358
+
359
+ # The terms of a sum; c * (a + b) is distributed first, because a single
360
+ # term is nothing to compare. A sum that cancels away entirely comes back
361
+ # with no terms and is its constant - sin(x) - sin(x) is 0 wherever both
362
+ # halves are defined, whether or not either half has a series there.
363
+ # nil when what is left is one term and nothing else.
364
+ def sum_terms(g)
365
+ constant, terms = Simplify.termize(g)
366
+ return [constant, terms] if sum?(constant, terms)
367
+ return nil unless g.each_node.any? { |n| n.is_a?(Add) || n.is_a?(Sub) }
368
+ constant, terms = Simplify.termize(Expand.expand(g))
369
+ sum?(constant, terms) ? [constant, terms] : nil
370
+ end
371
+
372
+ def sum?(constant, terms) = terms.empty? || terms.size + (constant.zero? ? 0 : 1) >= 2
373
+
374
+ # The squeeze rule additively [Rud76, th. 3.19]: a sum follows the term
375
+ # that runs away, as long as the others cannot catch it. Three criteria,
376
+ # each of them sound and each doing what the one before it cannot - let
377
+ # the oscillation grow, and compare two runaways:
378
+ #
379
+ # x**2/4 - sin(x) every other term is bounded, so the sum is caught
380
+ # between x**2/4 - 1 and x**2/4 + 1
381
+ # x**2 - x*sin(x) |x*sin(x)| <= |x| is of strictly smaller order than
382
+ # x**2, so the sum is caught between x**2 - x and
383
+ # x**2 + x
384
+ # exp(x) - x x/exp(x) is 0, so what is left is exp(x)*(1 + o(1))
385
+ #
386
+ # nil as soon as a term cannot be placed, and nil whenever two of them
387
+ # could cancel: x + x*sin(x) swings between 0 and 2*x, x - x**2*sin(x)
388
+ # swings through both infinities, and neither of them has a limit.
389
+ def dominant(g)
390
+ terms = (sum_terms(g) || return).last
391
+ measured = terms.map do |factors, coeff|
392
+ term = Simplify.rebuild_product(coeff, factors)
393
+ (measure(term) || return) + [term]
394
+ end
395
+ bounded_rest(measured) || least_order(measured) || ratio_dominance(measured)
396
+ end
397
+
398
+ # [kind, order, value] for one term of the sum: :divergent with the signed
399
+ # infinity it goes to, :bounded for one that stays in an interval,
400
+ # :oscillating for a bounded factor times something that does not stay in
401
+ # one (x*sin(x)). The order is the exponent of the leading term in t and
402
+ # is nil for a :divergent term that only exponential_fallback could
403
+ # decide - exp(1/t) has no series to read an order off. nil for a term
404
+ # that cannot be placed at all.
405
+ def measure(term)
406
+ if (value = value_of(term))
407
+ return nil if !infinite?(value) && value.each_node.any? { |n| n == OO }
408
+ return [infinite?(value) ? :divergent : :bounded, order_of(term), value]
409
+ end
410
+ coeff, factors = Simplify.factorize(term)
411
+ bounded, rest = factors.partition { |base, exp| bounded?(base, exp) }
412
+ return nil if bounded.empty?
413
+ rest = Simplify.rebuild_product(coeff, rest.to_h)
414
+ value = value_of(rest) || return
415
+ order = order_of(rest) || return
416
+ runaway = infinite?(value) || value.each_node.any? { |n| n == OO }
417
+ [runaway ? :oscillating : :bounded, order, nil]
418
+ end
419
+
420
+ # Everything that is not a runaway stays in a bounded interval and the
421
+ # runaways agree on a direction; the constant of the sum is bounded too,
422
+ # so the terms decide alone. exp(x) + 1 needs this rather than the orders:
423
+ # exp has no series at infinity, so only its limit is known.
424
+ def bounded_rest(measured)
425
+ return nil if measured.any? { |kind,| kind == :oscillating }
426
+ values = measured.filter_map { |kind, _, value| value if kind == :divergent }
427
+ return nil if values.empty?
428
+ values.all? { |value| value == values.first } ? values.first : nil
429
+ end
430
+
431
+ # One term is of strictly smallest order (most negative in t, so fastest
432
+ # in x) and runs away; every other term, oscillation included, is o() of
433
+ # it and cannot reach back.
434
+ def least_order(measured)
435
+ orders = measured.map { |_, order,| order }
436
+ return nil if orders.any?(&:nil?)
437
+ least = orders.min
438
+ return nil unless orders.count(least) == 1
439
+ kind, _, value = measured[orders.index(least)]
440
+ kind == :divergent ? value : nil
441
+ end
442
+
443
+ # The last resort: divide by a term that runs away and ask whether all the
444
+ # others vanish against it, which leaves the sum as that term times
445
+ # 1 + o(1). exp(x) - x needs this - the exponential has no series at
446
+ # infinity, so there is no order to compare it by, but x/exp(x) is a limit
447
+ # of its own and it is zero. The rule does not re-enter itself: the
448
+ # quotients it builds are limits again, and one comparison is enough.
449
+ def ratio_dominance(measured)
450
+ return nil if @comparing
451
+
452
+ begin
453
+ @comparing = true
454
+ measured.each do |kind, _, value, term|
455
+ next unless kind == :divergent
456
+ others = measured.reject { |_, _, _, other| other.equal?(term) }
457
+ return value if others.all? { |_, _, _, other| vanishes?(other, term) }
458
+ end
459
+ nil
460
+ ensure
461
+ @comparing = false
462
+ end
463
+ end
464
+
465
+ def vanishes?(term, against)
466
+ value = value_of((term / against).simplify)
467
+ value.is_a?(Num) && value.value.zero?
468
+ end
469
+
470
+ # The one-sided limit at t -> 0+, and the leading exponent of the series
471
+ # there; nil when the expansion cannot supply them.
472
+ def value_of(e)
473
+ value = one_sided(e, :right)
474
+ value.is_a?(Limit) ? nil : value
475
+ rescue SeriesError, ZeroDivisionError
476
+ nil
477
+ end
478
+
479
+ def order_of(e)
480
+ s = nil
481
+ [4, 8, 16, 32].each do |n|
482
+ s = adaptive(e, n)
483
+ break unless s.zero?
484
+ end
485
+ s.zero? ? nil : s.leading.first
486
+ rescue SeriesError, ZeroDivisionError
487
+ nil
488
+ end
489
+
490
+ def log_of(g)
491
+ coeff, factors = Simplify.factorize(g)
492
+ raise SeriesError, "sign of #{g}" unless coeff.is_a?(Numeric) && coeff.real? && coeff.positive?
493
+ factors.reduce(Fn.new(:log, [Num.new(coeff)])) do |acc, (base, exp)|
494
+ piece =
495
+ if base == Simplify.exp_base then Expression.lift(exp)
496
+ elsif (tail = erfc_tail_log(base)) then Expression.lift(exp) * tail
497
+ else Expression.lift(exp) * Fn.new(:log, [base])
498
+ end
499
+ acc + piece
500
+ end
501
+ end
502
+
503
+ # log(erfc(u)) for u -> +oo is -u**2 - log(u) - log(pi)/2 + o(1)
504
+ # [AS64, 7.1.23]. erfc(u) itself expands to the zero series there - it
505
+ # is smaller than every power - and the logarithm of that zero was how
506
+ # erfc(x)*exp(x**2)*x came out as oo instead of 1/sqrt(pi) (third
507
+ # review, D4). nil when the factor is not such an erfc.
508
+ def erfc_tail_log(base)
509
+ return nil unless base.is_a?(Fn) && base.name == :erfc && base.args.size == 1
510
+ u = base.args.first
511
+ return nil unless value_of(u) == OO
512
+ (Neg.new(u**2) - Fn.new(:log, [u]) - Fn.new(:log, [PI]) / 2).simplify
513
+ end
514
+
515
+ def signed_infinity(c)
516
+ coefficient_sign(c).negative? ? Neg.new(OO).simplify : OO
517
+ end
518
+
519
+ # -1 or 1 for a coefficient whose sign is decided - exactly (Decide) for
520
+ # a constant, by the assumptions for a parameter; a Float read off
521
+ # evalf took cos(10**-30) - 1 for 0 (fourth review). SeriesError when
522
+ # the sign is not known.
523
+ def coefficient_sign(c)
524
+ c = Expression.lift(c)
525
+ sign = c.variables.empty? ? Decide.sign(c) : RCAS.sign_of(c)
526
+ return 1 if sign == :positive
527
+ return -1 if sign == :negative
528
+ raise SeriesError, "sign of #{c} unknown"
529
+ end
530
+
531
+ # ---- the expansion ----------------------------------------------------------
532
+
533
+ def expand(f, order)
534
+ case f
535
+ when Num, Const then Series.constant(f, order)
536
+ when Var then f == T ? Series.variable(order) : Series.constant(f, order)
537
+ when Add then expand(f.left, order) + expand(f.right, order)
538
+ when Sub then expand(f.left, order) - expand(f.right, order)
539
+ when Neg then expand(f.arg, order).scale(Num.new(-1))
540
+ when Mul then product(f, order)
541
+ when Div then product(Mul.new(f.left, Pow.new(f.right, Num.new(-1))), order)
542
+ when Pow then power(f, order)
543
+ when Fn then function(f, order)
544
+ else raise SeriesError, "no series for #{f.class}"
545
+ end
546
+ end
547
+
548
+ # Factors are expanded to an order that keeps the product exact to +order+.
549
+ def product(f, order)
550
+ _, factors = Simplify.factorize(f)
551
+ coeff, = Simplify.factorize(f)
552
+ parts = factors.map { |base, exp| [base, exp] }
553
+ known = parts.map { |base, exp| expand_power(base, exp, order) }
554
+ lows = known.map(&:low)
555
+ total_low = lows.sum
556
+ series = parts.each_with_index.map do |(base, exp), i|
557
+ needed = order - (total_low - lows[i])
558
+ needed > order ? expand_power(base, exp, needed) : known[i]
559
+ end
560
+ series.reduce(Series.constant(coeff, order + [total_low, 0].min)) { |acc, s| acc * s }.truncate(order)
561
+ end
562
+
563
+ def expand_power(base, exp, order)
564
+ return expand(base, order) if exp == 1
565
+ power(Pow.new(base, Expression.lift(exp)), order)
566
+ end
567
+
568
+ def power(f, order)
569
+ base, exp = f.base, f.exponent
570
+ if exp.is_a?(Num) && exp.integer?
571
+ n = exp.value
572
+ s = expand(base, order)
573
+ return s.truncate(order)**0 if n.zero?
574
+ return (expand(base, order + [n - 1, 0].max)**n).truncate(order) if n.positive?
575
+ return (expand(base, order + 2 * s.low.abs)**n).truncate(order) if n.negative?
576
+ end
577
+ if base.is_a?(Fn) && base.name == :exp
578
+ return function(Fn.new(:exp, [base.args.first * exp]), order)
579
+ end
580
+ if exp.variables.include?(T.name)
581
+ return function(Fn.new(:exp, [exp * Fn.new(:log, [base])]), order)
582
+ end
583
+ s = expand(base, order)
584
+ raise SeriesError, "power of a series that is zero to the known order" if s.zero?
585
+ m = s.min_exponent
586
+ c = s.terms[m]
587
+ r = exp.is_a?(Num) ? exp.value : exp
588
+ raise SeriesError, "#{f}: symbolic exponent on a vanishing base" if !r.is_a?(Numeric) && !m.zero?
589
+ u = s.shift(-m).scale((Num.new(1) / c).simplify) - Series.constant(1, order - m)
590
+ binomial = ->(k) { (0...k).reduce(Num.new(1)) { |acc, j| (acc * (Expression.lift(r) - j) / (j + 1)).simplify } }
591
+ lead = Simplify.power_node(c, r)
592
+ Series.compose(u, order - m, &binomial).scale(lead.simplify).shift(r.is_a?(Numeric) ? m * r : 0)
593
+ end
594
+
595
+ # Functions that have no Taylor series at some points: abs and sign at 0
596
+ # (and abs anywhere off the real line), floor, ceil and round where they
597
+ # jump. Differentiating them there gave abs(x) the series 0 and
598
+ # sign(x) the limit 0 at 0 (third review, D3).
599
+ KINKED = %i[abs sign floor ceil round].freeze
600
+
601
+ # |u| and sign(u) where u -> 0 have no series, but on one side of the
602
+ # point they do: for t -> 0+ the sign of u is the sign of its leading
603
+ # coefficient, so |u| is that sign times u. one_sided sets the side
604
+ # (the fourth review: limit(abs(x), x, 0) had become formal, and abs is
605
+ # continuous there). nil where this does not apply.
606
+ def one_sided_kink(name, s, order)
607
+ return nil unless Thread.current[:rcas_one_sided] && %i[abs sign].include?(name)
608
+ # u -> 0 (the kink) or u -> oo (|1/t| at t -> 0+): the leading term decides
609
+ return nil unless s.min_exponent.negative? || (s.constant_term.is_a?(Num) && s.constant_term.value.zero?)
610
+ lead = s.terms[s.min_exponent]
611
+ return nil if lead.variables.include?(Series::LOG.name)
612
+ sign = begin
613
+ coefficient_sign(lead)
614
+ rescue SeriesError
615
+ return nil
616
+ end
617
+ name == :abs ? s.scale(Num.new(sign)) : Series.constant(Num.new(sign), order)
618
+ end
619
+
620
+ def removable(derivative, w, c)
621
+ value = Limits.limit(derivative, w, c)
622
+ return nil if value.is_a?(Limit) || infinite?(value) || value == UNDEFINED || !value.variables.empty? && value.variables != c.variables
623
+ value
624
+ rescue SeriesError, ZeroDivisionError, NotImplementedError, RCAS::Unsupported
625
+ nil
626
+ end
627
+
628
+ def non_analytic!(f, c)
629
+ return unless KINKED.include?(f.name)
630
+ if %i[abs sign].include?(f.name) && !c.variables.empty? && %i[positive negative].include?(RCAS.sign_of(c))
631
+ return # |x + a| at x = 0 for a > 0 is x + a
632
+ end
633
+ value = begin
634
+ Expression.lift(c).evalf
635
+ rescue StandardError => rescued
636
+ RCAS.guard!(rescued)
637
+ nil
638
+ end
639
+ value = value.value if value.is_a?(Num)
640
+ real = value.is_a?(Numeric) && value.real?
641
+ at_kink =
642
+ case f.name
643
+ when :abs, :sign then !real || Decide.zero?(c) != false
644
+ when :floor, :ceil then !real || (Decide.zero?((c - Fn.new(:round, [c])).simplify) != false)
645
+ when :round then !real || Decide.zero?((c - Fn.new(:floor, [c]) - Num.new(Rational(1, 2))).simplify) != false
646
+ end
647
+ raise SeriesError, "#{f} has no series where its argument is #{c}" if at_kink
648
+ end
649
+
650
+ # The argument's series to the order asked for: a cancellation costs
651
+ # known terms (sqrt(x**2 + x) - x at infinity is 1/2 - 1/(8x) + ...,
652
+ # and at order 3 nothing of it was known), and a function of it then
653
+ # read an unknown constant term as 0 - exp of it came out as 1 + O(1/x**3)
654
+ # instead of starting with e**(1/2) (fourth review, D6).
655
+ # Only the constant term has to be known for the answer to be right; a
656
+ # shorter known order is carried by the result, and the adaptive loop at
657
+ # the top asks again for more.
658
+ def known_argument(argument, order)
659
+ s = expand(argument, order)
660
+ extra = order
661
+ 4.times do
662
+ break if s.order >= 1
663
+ extra += 2 - s.order
664
+ s = expand(argument, extra)
665
+ end
666
+ s.order > order ? s.truncate(order) : s
667
+ end
668
+
669
+ def function(f, order)
670
+ raise SeriesError, "#{f.name} takes one argument" unless f.args.size == 1
671
+ name = f.name
672
+ return expand(Fn.new(:sin, f.args) / Fn.new(:cos, f.args), order) if name == :tan
673
+
674
+ s = known_argument(f.args.first, order)
675
+ return Series.constant(Fn.new(name, [s.constant_term]).simplify, order) if s.zero? || (s.terms.keys - [0]).empty?
676
+
677
+ if name == :log
678
+ m = s.min_exponent
679
+ c = s.terms[m]
680
+ u = s.shift(-m).scale((Num.new(1) / c).simplify) - Series.constant(1, order - m)
681
+ tail = Series.compose(u, order - m) { |k| k.zero? ? Num.new(0) : Num.new(Rational((-1)**(k + 1), k)) }
682
+ head = { 0 => (Fn.new(:log, [c]) + Num.new(m) * Series::LOG).simplify }
683
+ return Series.new(head, order) + tail.truncate(order)
684
+ end
685
+
686
+ if name == :atan && s.min_exponent.negative?
687
+ sign = coefficient_sign(s.terms[s.min_exponent])
688
+ half_pi = Series.constant((sign.negative? ? -PI / 2 : PI / 2).simplify, order)
689
+ return half_pi - function(Fn.new(:atan, [Pow.new(f.args.first, Num.new(-1))]), order)
690
+ end
691
+ if %i[erf erfc].include?(name) && s.min_exponent.negative?
692
+ # erf(+-oo) = +-1 up to an exponentially small tail: a constant, for limits
693
+ sign = coefficient_sign(s.terms[s.min_exponent])
694
+ value = name == :erf ? (sign.negative? ? -1 : 1) : (sign.negative? ? 2 : 0)
695
+ return Series.constant(Num.new(value), order)
696
+ end
697
+ if (kink = one_sided_kink(name, s, order))
698
+ return kink
699
+ end
700
+ raise SeriesError, "#{f} has an essential singularity" if s.min_exponent.negative?
701
+ c = s.constant_term
702
+ non_analytic!(f, c)
703
+ s0 = s - Series.constant(c, order)
704
+ w = Var.new(:_w)
705
+ derivative = Fn.new(name, [w])
706
+ factorial = 1
707
+ coefficients = []
708
+ Series.compose(s0, order) do |k|
709
+ while coefficients.size <= k
710
+ value = begin
711
+ derivative.subs(w => c).simplify
712
+ rescue ZeroDivisionError
713
+ # Si's derivative sin(t)/t at 0 has a removable hole, whose value
714
+ # is the limit; asin's 1/sqrt(1 - t**2) at 1 has a pole, and a
715
+ # derivative with no finite value is no Taylor coefficient
716
+ removable(derivative, w, c) or raise SeriesError, "#{f} has no Taylor series where its argument is #{c}"
717
+ end
718
+ coefficients << (value / factorial).simplify
719
+ derivative = derivative.diff(w)
720
+ factorial *= coefficients.size
721
+ end
722
+ coefficients[k]
723
+ end
724
+ end
725
+ end
726
+ end