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,825 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Numbers where symbols run out: a root of an equation no formula solves,
5
+ # the value of an integral with no elementary antiderivative.
6
+ #
7
+ # nsolve(cos(x) - x, x: 0..1) # => 0.7390851332151607
8
+ # nsolve(cos(x) - x, x, 1) # from a starting point (Newton)
9
+ # nintegrate(sin(x)/x, x: 0..1) # => 0.9460830703671831
10
+ # integrate(sin(x)/x, x: 0..1).evalf # the same, from the unevaluated integral
11
+ #
12
+ # Results are Floats and are labelled as numeric wherever they appear; rcas
13
+ # never quietly replaces an exact answer with one of these.
14
+ #
15
+ # Sources (keys: MANUAL.md, Sources): bisection with the Newton-Raphson
16
+ # refinement and Brent's guarded step [PTVF07, §9.1-9.4]; adaptive Simpson
17
+ # quadrature [PTVF07, §4.2]; the infinite range is mapped to a finite one by
18
+ # the substitutions of [PTVF07, §4.5].
19
+ module Numerics
20
+ TOLERANCE = 1e-12
21
+ FLOAT_DIGITS = 17 # one more than a Float carries, so the last one is right
22
+ # Simpson accepts at this much, relative to the size of the integral.
23
+ # It decides how nearly the fast path agrees with the slow one: over ten
24
+ # smooth integrands, 1e-12 leaves three of them 1 to 23 ulp out, 1e-13
25
+ # one of them 1 ulp out, and 1e-15 none - at 0.22 s, 0.41 s and 1.17 s
26
+ # against the 0.70 s the doubly exponential quadrature takes for all
27
+ # ten. 1e-13 is where the two paths agree to the digit a Float carries
28
+ # and the fast one is still the fast one.
29
+ SIMPSON_TOLERANCE = 1e-13
30
+ SIMPSON_DEPTH = 20
31
+ MAX_STEPS = 200
32
+ MAX_DEPTH = 50
33
+
34
+ module_function
35
+
36
+ # ---- evaluation ------------------------------------------------------------
37
+
38
+ # A float from an expression with at most the one free variable bound.
39
+ # Compiled to a lambda over Floats when every node is one the compiler
40
+ # knows, which is a hundred times faster than substituting into the
41
+ # tree: adaptive Simpson over sin(1000*x) wants 300000 values and took
42
+ # ten seconds (third review, section 5). Anything else takes the tree.
43
+ # The integrand as a Float function: the compiled lambda where it has a
44
+ # value, the tree evaluator where it does not - asin(1.5) is complex, and
45
+ # |asin(x)| is still real (the compiled lambda said nil and nintegrate
46
+ # refused: fourth review) - and, for a tree whose exact constants are
47
+ # beyond the Floats (10**400), evalf with its wide fallback.
48
+ def caller_for(expr, var)
49
+ expr = Expression.lift(expr)
50
+ name = Expression.lift(var).name
51
+ huge = beyond_floats?(expr)
52
+ expr = expr.simplify if huge
53
+ huge &&= beyond_floats?(expr)
54
+ compiled = compile(expr, name)
55
+ tree = tree_caller(expr, name)
56
+ exact = huge ? exact_caller(expr, name) : nil
57
+ return tree if compiled.nil? && exact.nil?
58
+ lambda do |value|
59
+ fast = compiled&.call(value)
60
+ # a logarithm at 0 has no value, and the tree must not be asked:
61
+ # simplify absorbs 0*log(0) into 0, which made a pole of
62
+ # sign(x - 1)*log|x - 1| a root at 1
63
+ next nil if fast.equal?(POLE)
64
+ fast || tree.call(value) || exact&.call(value)
65
+ end
66
+ end
67
+
68
+ # What the compiled lambda answers where a logarithm meets 0: no value,
69
+ # and unlike a non-real one (asin(1.5), for |asin(x)|) none the tree can
70
+ # supply.
71
+ POLE = Object.new.freeze
72
+
73
+ def beyond_floats?(expr)
74
+ expr.each_node.any? do |n|
75
+ n.is_a?(Num) && (v = n.value).is_a?(Numeric) && v.real? && !v.is_a?(Float) && !v.zero? && !Expression.float_of(v.abs).between?(1e-300, 1e300)
76
+ end
77
+ end
78
+
79
+ def exact_caller(expr, name)
80
+ lambda do |value|
81
+ result = expr.subs(name => Num.new(value)).evalf
82
+ RCAS.real_float(result)
83
+ rescue StandardError => rescued
84
+ RCAS.guard!(rescued)
85
+ nil
86
+ end
87
+ end
88
+
89
+ def tree_caller(expr, name)
90
+ tree = Expression.floatify_tree(expr)
91
+ lambda do |value|
92
+ result = tree.call(name => value)
93
+ result = result.value if result.is_a?(Num)
94
+ unless result.is_a?(Numeric)
95
+ # Folding puts an exact constant back after floatify: exp(-1.0) is
96
+ # 1/e again, which is not a number until evalf looks at it. Without
97
+ # this the integrand has a hole at every such point - x = 1 for
98
+ # exp(-x**2) - and the quadrature chases a discontinuity that is
99
+ # not there.
100
+ result = Expression.lift(result).evalf
101
+ result = result.value if result.is_a?(Num)
102
+ end
103
+ result.is_a?(Numeric) && !result.is_a?(Complex) && result.finite? ? result.to_f : nil
104
+ rescue StandardError => rescued
105
+ RCAS.guard!(rescued)
106
+ nil
107
+ end
108
+ end
109
+
110
+ COMPILED = { sin: Math.method(:sin), cos: Math.method(:cos), tan: Math.method(:tan), exp: Math.method(:exp),
111
+ atan: Math.method(:atan), sinh: Math.method(:sinh), cosh: Math.method(:cosh), erf: Math.method(:erf),
112
+ erfc: Math.method(:erfc) }.freeze
113
+ # real only inside their domain: nil outside, as the tree gives
114
+ GUARDED = { log: ->(v) { v.positive? ? Math.log(v) : (v.zero? ? throw(:pole, POLE) : nil) },
115
+ asin: ->(v) { v.abs <= 1 ? Math.asin(v) : nil },
116
+ acos: ->(v) { v.abs <= 1 ? Math.acos(v) : nil },
117
+ abs: ->(v) { v.abs }, sign: ->(v) { (v <=> 0).to_f },
118
+ floor: ->(v) { v.floor.to_f }, ceil: ->(v) { v.ceil.to_f } }.freeze
119
+
120
+ # A lambda x -> Float (nil where the value is not a finite real), or nil
121
+ # when some node is not one of the kinds above.
122
+ def compile(expr, name)
123
+ body = compiled_node(expr, name) or return nil
124
+ lambda do |value|
125
+ v = catch(:pole) { body.call(value.to_f) }
126
+ next POLE if v.equal?(POLE)
127
+ v.is_a?(Float) && v.finite? ? v : nil
128
+ rescue ZeroDivisionError, Math::DomainError, FloatDomainError
129
+ nil
130
+ end
131
+ end
132
+
133
+ def compiled_node(e, name)
134
+ case e
135
+ when Num
136
+ v = e.value
137
+ return nil unless v.is_a?(Numeric) && v.real?
138
+ f = v.to_f
139
+ ->(_) { f }
140
+ when Const
141
+ return nil unless e.value.is_a?(Numeric) && e.value.real? && e.value.to_f.finite?
142
+ f = e.value.to_f
143
+ ->(_) { f }
144
+ when Var then e.name == name ? ->(x) { x } : nil
145
+ when Neg then (a = compiled_node(e.arg, name)) && ->(x) { (v = a.call(x)) && -v }
146
+ when Add, Sub, Mul, Div
147
+ a = compiled_node(e.left, name) or return nil
148
+ b = compiled_node(e.right, name) or return nil
149
+ op = { Add => :+, Sub => :-, Mul => :*, Div => :/ }[e.class]
150
+ lambda do |x|
151
+ l = a.call(x) or return nil
152
+ r = b.call(x) or return nil
153
+ return nil if op == :/ && r.zero?
154
+ l.public_send(op, r)
155
+ end
156
+ when Pow then compiled_power(e, name)
157
+ when Fn
158
+ return nil unless e.args.size == 1
159
+ a = compiled_node(e.args.first, name) or return nil
160
+ if (f = COMPILED[e.name]) then ->(x) { (v = a.call(x)) && f.call(v) }
161
+ elsif (g = GUARDED[e.name]) then ->(x) { (v = a.call(x)) && g.call(v) }
162
+ end
163
+ end
164
+ end
165
+
166
+ # A negative base has a real power only for an integer exponent here -
167
+ # the principal value of (-8)**(1/3) is not real, which is what the tree
168
+ # says too.
169
+ def compiled_power(e, name)
170
+ a = compiled_node(e.base, name) or return nil
171
+ exponent = e.exponent
172
+ if exponent.is_a?(Num) && exponent.value.is_a?(Integer)
173
+ n = exponent.value
174
+ return lambda do |x|
175
+ v = a.call(x) or return nil
176
+ return nil if v.zero? && n.negative?
177
+ v**n
178
+ end
179
+ end
180
+ b = compiled_node(exponent, name) or return nil
181
+ lambda do |x|
182
+ v = a.call(x) or return nil
183
+ w = b.call(x) or return nil
184
+ return nil if v.negative? && w != w.round
185
+ return nil if v.zero? && w.negative?
186
+ v**w
187
+ end
188
+ end
189
+
190
+ # ---- roots -------------------------------------------------------------------
191
+
192
+ # nsolve(f, x: a..b) brackets a sign change; nsolve(f, x, guess) starts
193
+ # Newton's method there. Returns a Float, or raises when no root is found.
194
+ def nsolve(f, var = nil, guess = nil, **range)
195
+ digits = range.delete(:digits)
196
+ expr = Solve.to_zero(f)
197
+ var, from, to = arguments(var, guess, range, expr)
198
+ g = caller_for(expr, var)
199
+ # bisection needs no derivative, and x + floor(x) has none to give
200
+ derivative = begin
201
+ caller_for(Expression.lift(expr).diff(var), var)
202
+ rescue ArgumentError
203
+ ->(_) { nil }
204
+ end
205
+ root = to.nil? ? newton(g, derivative, from, var, expr) : bisect(g, derivative, from, to, var, expr)
206
+ root = certified(expr, var, g, root, from, to) if to
207
+ case crossing(g, root, from, to)
208
+ when :pole
209
+ raise ArgumentError, "nsolve: #{expr} has a pole at #{root}, not a root; give a range on one side of it"
210
+ when :jump
211
+ raise ArgumentError, "nsolve: #{expr} changes sign at #{root} without passing through 0 - a jump, not a root"
212
+ end
213
+ digits ? Precision.refine(expr, var, root, digits) : root
214
+ end
215
+
216
+ # What the function does where the sign changes, read off |f| at
217
+ # distances growing tenfold from 40 units in the last place to a
218
+ # hundred million of them, on each side and inside the range: towards
219
+ # a root |f| shrinks, however steeply (the real cube root of x - 3/10 is
220
+ # 4e-6 an ulp away, which the old size test took for a jump), towards a
221
+ # pole it grows, across a jump it stays put. :root, :pole or :jump; a
222
+ # root when f is exactly 0 there or the sides disagree.
223
+ #
224
+ # Each side is read twice. Over the seven decades, a root of order a
225
+ # multiplies |f| by 10**(7*a) and a pole divides it; over the nearest
226
+ # decade alone, a jump changes |f| by next to nothing, however steep f
227
+ # is beside it - floor(x) - 1/2 + 10**7*(x - 1) grows seventeen
228
+ # hundredfold over the seven decades and by two parts in a million
229
+ # over the first, where a root of order 1/51 still gains 4.6%. So a
230
+ # side shrinks towards x when both readings say so, and is flat when
231
+ # the nearest decade moves by less than NEAR_DECADE.
232
+ def crossing(g, x, from, to)
233
+ value = g.call(x)
234
+ return :root if value&.zero?
235
+ low, high = [from, to.nil? ? from : to].minmax
236
+ low, high = -Float::INFINITY, Float::INFINITY if to.nil?
237
+ unit = 4 * Float::EPSILON * [x.abs, 1e-300].max
238
+ sides = [-1, 1].filter_map do |side|
239
+ values = (1..8).map do |j|
240
+ t = x + side * unit * 10**j
241
+ t >= low && t <= high ? g.call(t)&.abs : nil
242
+ end
243
+ next nil if values.compact.size < 3
244
+ side_kind(values)
245
+ end
246
+ return value.nil? ? :pole : :root if sides.empty?
247
+ return :pole if sides.all?(:grows)
248
+ return :jump if sides.all?(:flat)
249
+ return :pole if value.nil? && sides.none?(:shrinks)
250
+ :root
251
+ end
252
+
253
+ NEAR_DECADE = 1e-3
254
+
255
+ # :shrinks (towards x), :grows or :flat, from |f| at the eight
256
+ # distances (nil where outside the range or undefined)
257
+ def side_kind(values)
258
+ near, far = values.compact.first, values.compact.last
259
+ return :shrinks if near.zero?
260
+ overall = far / near
261
+ first = values.index(near)
262
+ step = values[first + 1] && values[first + 1] / near
263
+ step = overall**(1.0 / 7) if step.nil? # the next decade is missing
264
+ if step > 1 + NEAR_DECADE && overall > 1 then :shrinks
265
+ elsif step < 1 - NEAR_DECADE && overall < 1 then :grows
266
+ elsif (step - 1).abs <= NEAR_DECADE then :flat
267
+ else overall > 2 ? :shrinks : (overall < 0.5 ? :grows : :flat)
268
+ end
269
+ end
270
+
271
+ # The sign test of the bisection. A product of the two values is no
272
+ # test: two values of 1e-165 multiply to 0, and (x - 1)**41 then
273
+ # "changed sign" at 0.9999 and was called a jump.
274
+ def same_sign?(a, b) = (a.positive? && b.positive?) || (a.negative? && b.negative?)
275
+ def opposite_sign?(a, b) = (a.positive? && b.negative?) || (a.negative? && b.positive?)
276
+
277
+ # A bracketed root of an exact f, checked by exact signs. Float
278
+ # evaluation of expand((x - 1)**9) is noise for |x - 1| < 0.05 - the
279
+ # terms are of size 126 and cancel to 1e-16 - and the bisection
280
+ # followed the noise to 0.985. So the signs a little either side of the
281
+ # root are decided (`Decide.sign` at exact rational points: exactly for
282
+ # a polynomial, by certified digits otherwise) and, when they do not
283
+ # differ, the bisection is run again on decided signs alone. Float
284
+ # input has no exact signs to ask, and an undecided sign keeps the
285
+ # Float root, which is what the caller had before.
286
+ def certified(expr, var, g, root, from, to)
287
+ return root if Expression.lift(expr).each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Float) }
288
+ sign = exact_sign(expr, var)
289
+ return root if sign.call(root) == :zero
290
+ d = 16 * Float::EPSILON * [root.abs, Float::MIN].max
291
+ left, right = sign.call(root - d), sign.call(root + d)
292
+ return root if left.nil? || right.nil? || left != right
293
+ lo, hi = [from, to].minmax
294
+ slo, shi = sign.call(lo), sign.call(hi)
295
+ return root if slo.nil? || shi.nil? || slo == shi || [slo, shi].include?(:zero)
296
+ steps = 0
297
+ until narrow?(lo, hi) || steps > 4 * MAX_STEPS
298
+ steps += 1
299
+ x = (lo + hi) / 2.0
300
+ s = sign.call(x)
301
+ return root if s.nil? # nothing decided to go on: keep the Float answer
302
+ return x if s == :zero
303
+ s == slo ? lo = x : hi = x
304
+ end
305
+ g.call(lo)&.abs.to_f <= g.call(hi)&.abs.to_f ? lo : hi
306
+ end
307
+
308
+ def exact_sign(expr, var)
309
+ e = Expression.lift(expr)
310
+ lambda do |x|
311
+ Decide.sign(e.subs(var => Num.new(x.to_r)).simplify)
312
+ rescue StandardError => rescued
313
+ RCAS.guard!(rescued)
314
+ nil
315
+ end
316
+ end
317
+
318
+ # Converged when the bracket is a few units in the last place wide: a
319
+ # small value of f is not a root - (x - 3/10)/10**12 is -3e-13 at 0 and
320
+ # exp(-50*x)*(x - 1/2) is 1e-22 at 1, and |f| < 1e-12 answered with
321
+ # those (third review, S20).
322
+ def narrow?(lo, hi) = (hi - lo).abs <= 4 * Float::EPSILON * [lo.abs, hi.abs, Float::MIN].max
323
+
324
+ def arguments(var, guess, range, expr)
325
+ unless range.empty?
326
+ raise ArgumentError, "nsolve: give one variable, e.g. nsolve(f, x: 0..1)" unless range.size == 1 && var.nil?
327
+ name, value = range.first
328
+ raise ArgumentError, "nsolve: expected a range or a number, got #{value.inspect}" unless value.is_a?(Range) || value.is_a?(Numeric)
329
+ return [Var.new(name), Float(value.begin), Float(value.end)] if value.is_a?(Range)
330
+ return [Var.new(name), Float(value), nil]
331
+ end
332
+ if var.nil?
333
+ names = Expression.lift(expr).variables
334
+ raise ArgumentError, "nsolve: which variable? give a range, e.g. nsolve(f, x: 0..1)" unless names.size == 1
335
+ var = Var.new(names.first)
336
+ end
337
+ [Expression.lift(var), guess.nil? ? 0.0 : Float(Expression.lift(guess).evalf), nil]
338
+ end
339
+
340
+ # Bisection, taking a Newton step whenever it stays inside the bracket
341
+ # and shrinks it; stopping when the bracket is as narrow as Floats go,
342
+ # or f is exactly 0.
343
+ def bisect(g, derivative, lo, hi, var, expr)
344
+ flo = g.call(lo)
345
+ fhi = g.call(hi)
346
+ raise ArgumentError, "nsolve: #{expr} is not defined at both ends of #{lo}..#{hi}" if flo.nil? || fhi.nil?
347
+ return lo if flo.zero?
348
+ return hi if fhi.zero?
349
+ raise ArgumentError, "nsolve: #{expr} has the same sign at #{lo} and #{hi}; give a range that brackets a root" if same_sign?(flo, fhi)
350
+
351
+ return 0.0 if crosses_at_zero?(g, lo, hi, flo)
352
+ x = (lo + hi) / 2.0
353
+ steps = 0
354
+ until narrow?(lo, hi) || steps > 4 * MAX_STEPS
355
+ steps += 1
356
+ value = defined_near(g, x, lo, hi)
357
+ return x if value.nil? # undefined right across the bracket
358
+ slope = derivative.call(x)
359
+ step = slope && !slope.zero? ? x - value / slope : nil
360
+ # Newton only while it lands strictly inside and the bracket keeps
361
+ # halving at least every other step
362
+ x = step && step > lo && step < hi && steps.odd? ? step : (lo + hi) / 2.0
363
+ value = defined_near(g, x, lo, hi)
364
+ return x if value.nil?
365
+ return x if value.zero?
366
+ same_sign?(value, flo) ? (lo = x; flo = value) : (hi = x; fhi = value)
367
+ end
368
+ flo.abs <= fhi.abs ? lo : hi # the caller tells a root from a pole or a jump
369
+ end
370
+
371
+ # A bracket around 0 is never narrow relative to its ends, and where f
372
+ # has no value at 0 - x*log|x| - bisection could only halve towards it
373
+ # until the steps ran out, at 4e-165, where the samples of `crossing`
374
+ # are relative to a point that is not the crossing. The sign change is
375
+ # at 0 when it is there on either side of the smallest normal Float;
376
+ # `crossing` then says what kind it is.
377
+ def crosses_at_zero?(g, lo, hi, flo)
378
+ return false unless lo.negative? && hi.positive? && g.call(0.0).nil?
379
+ left = g.call(-Float::MIN)
380
+ right = g.call(Float::MIN)
381
+ !left.nil? && !right.nil? && same_sign?(left, flo) && opposite_sign?(right, flo)
382
+ end
383
+
384
+ # The value at x, or at the nearest point inside the bracket where the
385
+ # function is defined. Reading an undefined value as zero (which is what
386
+ # `g.call(x) || 0.0` did) hands the sign test a root that is not there.
387
+ def defined_near(g, x, lo, hi)
388
+ value = g.call(x)
389
+ return value unless value.nil?
390
+ span = (hi - lo).abs
391
+ [1e-3, 1e-2, 1e-1].each do |fraction|
392
+ [x + span * fraction, x - span * fraction].each do |y|
393
+ next unless y > lo && y < hi
394
+ value = g.call(y)
395
+ return value unless value.nil?
396
+ end
397
+ end
398
+ nil
399
+ end
400
+
401
+ # Newton's method from a starting point, with a bracketed retry.
402
+ # Converged when the step is small relative to x and a sign change or
403
+ # an exact zero confirms the point: x*exp(-x) from 5 runs off to where
404
+ # exp underflows, and a small value there is no root.
405
+ def newton(g, derivative, start, var, expr)
406
+ x = start
407
+ MAX_STEPS.times do
408
+ value = g.call(x)
409
+ break if value.nil?
410
+ return x if value.zero? && confirmed?(g, x)
411
+ slope = derivative.call(x)
412
+ break if slope.nil? || slope.abs < 1e-300
413
+ step = value / slope
414
+ x -= step
415
+ return x if step.abs <= 1e-14 * [1.0, x.abs].max && confirmed?(g, x)
416
+ end
417
+ widened = scan(g, start)
418
+ return bisect(g, derivative, widened.first, widened.last, var, expr) if widened
419
+ raise ArgumentError, "nsolve: no root found near #{start}; give a range that brackets one"
420
+ end
421
+
422
+ # A root found by Newton is believed when f changes sign across it, or
423
+ # f is small beside its neighbours as it is at a double root.
424
+ def confirmed?(g, x)
425
+ d = 1e-7 * [1.0, x.abs].max
426
+ left = g.call(x - d)
427
+ right = g.call(x + d)
428
+ here = g.call(x)
429
+ return false if left.nil? || right.nil? || here.nil?
430
+ return true if !same_sign?(left, right)
431
+ here.abs <= 1e-6 * [left.abs, right.abs].min && [left.abs, right.abs].min.positive?
432
+ end
433
+
434
+ # Look for a sign change around the starting point.
435
+ def scan(g, start)
436
+ step = 0.5
437
+ previous = g.call(start)
438
+ (1..60).each do |i|
439
+ [start + i * step, start - i * step].each do |x|
440
+ value = g.call(x)
441
+ next if value.nil? || previous.nil?
442
+ return [[x, start].min, [x, start].max] if !same_sign?(value, previous)
443
+ end
444
+ end
445
+ nil
446
+ end
447
+
448
+ # ---- quadrature ---------------------------------------------------------------
449
+
450
+ # nintegrate(f, x: a..b), with infinite ends mapped to a finite range.
451
+ #
452
+ # Three promises, each broken once (third review, 22 Sept 2026): an
453
+ # integrand that is not a number (an unknown function, a free
454
+ # parameter) is refused rather than integrated as 0; the range is cut
455
+ # at the kinks, jumps and poles rcas can name, so abs(x - 1) is no
456
+ # harder than x and 1/x over -1..1 is two divergent pieces rather than
457
+ # a principal value of 0; and a sample with no value inside the range
458
+ # is a removable hole or a refusal, never a zero.
459
+ def nintegrate(f, var = nil, from = nil, to = nil, **range)
460
+ digits = range.delete(:digits)
461
+ var, from, to = Functions.range_arguments(var, from, to, range, "nintegrate", discrete: false) if var.nil? || from
462
+ var = Expression.lift(var)
463
+ f = Expression.lift(f)
464
+ from = Expression.lift(from)
465
+ to = Expression.lift(to)
466
+ lo = bound(from)
467
+ hi = bound(to)
468
+ width = exact_width(from, to)
469
+ return(digits ? Decimal.new(BigDecimal(0), digits) : 0.0) if width ? Decide.zero?(width) == true : lo == hi
470
+ return rescaled(f, var, from, width, digits) if width && lost_width?(lo, hi, width)
471
+ numeric_integrand!(f, var, lo, hi)
472
+ inside = breakpoints(f, var, lo, hi)
473
+ ends = lo > hi ? [from, *inside.reverse, to] : [from, *inside, to]
474
+ not_integrable!(f, var, ends)
475
+ pieces = ends.each_cons(2).map { |a, b| piece(f, var, a, b, digits) }
476
+ return pieces.sum if digits.nil?
477
+ Decimal.new(pieces.map { |d| d.is_a?(Decimal) ? d.value : BigDecimal(d.to_s) }.sum, digits)
478
+ end
479
+
480
+ # The width of a range between two finite constant ends, exactly; nil
481
+ # for an infinite end or one with a parameter.
482
+ def exact_width(from, to)
483
+ return nil if [from, to].any? { |b| Limits.infinite?(b) || !b.variables.empty? }
484
+ (to - from).simplify
485
+ end
486
+
487
+ # Floats can lose a range entirely: 10**20 and 10**20 + 1 are one
488
+ # Float, and a width of 10**-400 underflows to nothing. Both integrals
489
+ # of a review (23 Sept 2026) were 1 and came back as 0.0.
490
+ def lost_width?(lo, hi, width)
491
+ w = width.evalf
492
+ return true unless w.is_a?(Numeric) && !w.is_a?(Complex) && w.to_f.finite?
493
+ w = w.to_f
494
+ w.zero? || lo == hi || ((hi - lo) - w).abs > 1e-12 * w.abs
495
+ end
496
+
497
+ # x = from + width*t over 0..1: the width becomes a factor of the
498
+ # integrand, where it is exact, and the range is one Floats can hold.
499
+ def rescaled(f, var, from, width, digits)
500
+ t = Var.new(Expression.fresh_variable(:t, f.variables).name)
501
+ g = (f.subs(var.name => from + width * t) * width).simplify
502
+ digits ? nintegrate(g, t, 0, 1, digits: digits) : nintegrate(g, t, 0, 1)
503
+ end
504
+
505
+ # One piece: no kink, jump or pole inside, whatever the ends do.
506
+ def piece(f, var, from, to, digits)
507
+ if digits
508
+ return Decimal.new(Precision.quadrature(f, var, from, to, digits), digits)
509
+ end
510
+ lo = bound(from)
511
+ hi = bound(to)
512
+ return -piece(f, var, to, from, nil) if lo > hi
513
+ return 0.0 if lo == hi
514
+ # Adaptive Simpson first: on a smooth integrand it settles in a
515
+ # millisecond, and to the digit the slower quadrature would give (see
516
+ # SIMPSON_TOLERANCE for how nearly), where the doubly exponential one
517
+ # pays 100 ms for its guard digits. Its budget is bounded, so a
518
+ # singular integrand fails fast and goes there instead of subdividing
519
+ # for ever.
520
+ unless lo.infinite? || hi.infinite?
521
+ quick = bounded_simpson(caller_for(f, var), lo, hi)
522
+ return quick if quick
523
+ end
524
+ exact = tanh_sinh(f, var, from, to)
525
+ return exact if exact
526
+ # no arbitrary-precision route (a function Precision lacks): Simpson
527
+ # again, on a larger budget, and a refusal when it does not settle
528
+ value = lo.infinite? || hi.infinite? ? transformed(f, var, lo, hi) : checked_simpson(caller_for(f, var), lo, hi)
529
+ value or raise ArgumentError, "nintegrate: the quadrature of #{f} over #{from}..#{to} did not settle; it may diverge"
530
+ end
531
+
532
+ # Where the integrand has no value at an end of a piece, it may not be
533
+ # integrable there at all: when (x - p)*f(x) does not tend to 0, f is
534
+ # at least as large as c/(x - p) and the integral diverges [Rud76, th.
535
+ # 6.20 by comparison]. Saying so from the limit is quick; letting the
536
+ # quadrature fail to settle took seconds. A limit of 0 proves nothing
537
+ # either way, and the quadrature decides then.
538
+ def not_integrable!(f, var, ends)
539
+ g = caller_for(f, var)
540
+ ends.each_with_index do |p, i|
541
+ next if Limits.infinite?(p)
542
+ value = Analysis.numeric(p)
543
+ next if value.nil? || g.call(value)
544
+ sides = []
545
+ sides << :left if i.positive?
546
+ sides << :right if i < ends.size - 1
547
+ sides.each do |side|
548
+ limit = begin
549
+ Limits.limit((var - p) * f, var, p, side)
550
+ rescue StandardError => rescued
551
+ RCAS.guard!(rescued)
552
+ nil
553
+ end
554
+ next if limit.nil? || limit.is_a?(Limit)
555
+ next if limit.is_a?(Num) && limit.value.is_a?(Numeric) && limit.value.zero?
556
+ next unless Limits.infinite?(limit) || limit.is_a?(Num) || limit.variables.empty?
557
+ raise ArgumentError, "nintegrate: #{f} is not integrable at #{var} = #{p}; it grows like 1/(#{(var - p).simplify}) or faster there, so the integral diverges"
558
+ end
559
+ end
560
+ end
561
+
562
+ # A number at some point of the range, or a refusal naming why not.
563
+ def numeric_integrand!(f, var, lo, hi)
564
+ free = f.variables - [var.name]
565
+ raise ArgumentError, "nintegrate: #{f} has the free variables #{free.join(', ')}; give them values first" unless free.empty?
566
+ g = caller_for(f, var)
567
+ probes = [0.2113, 0.5, 0.7887, 0.3719, 0.6281].map do |s|
568
+ if lo.infinite? && hi.infinite? then (s - 0.5) * 10
569
+ elsif hi.infinite? then lo + s * 10
570
+ elsif lo.infinite? then hi - s * 10
571
+ else lo + (hi - lo) * s
572
+ end
573
+ end
574
+ return if probes.any? { |t| g.call(t) }
575
+ raise ArgumentError, "nintegrate: #{f} has no numeric value on the range (an unknown function, or a value that is not real)"
576
+ end
577
+
578
+ # The points strictly inside the range where the integrand has a kink,
579
+ # a jump or a pole that rcas can name: the zeros of the argument of an
580
+ # abs or sign, the integers a linear argument of floor, ceil or round
581
+ # crosses, the edges of a piecewise branch, and the poles
582
+ # Integrate.singular_points finds. Sorted ascending; [] on an infinite
583
+ # range, where they cannot be counted out.
584
+ MAX_BREAKPOINTS = 256
585
+
586
+ def breakpoints(f, var, lo, hi)
587
+ a, b = [lo, hi].minmax
588
+ return [] if a.infinite? || b.infinite?
589
+ found = []
590
+ f.each_node do |node|
591
+ if node.is_a?(Fn) && %i[abs sign].include?(node.name) && node.args.first.variables.include?(var.name)
592
+ found.concat(zeros_between(node.args.first, var, a, b))
593
+ elsif node.is_a?(Fn) && %i[floor ceil round].include?(node.name) && node.args.first.variables.include?(var.name)
594
+ found.concat(integer_crossings(node, var, a, b))
595
+ elsif node.is_a?(Piecewise)
596
+ node.conditions.each do |c|
597
+ case c
598
+ when Inequality, Equation then found.concat(zeros_between((c.lhs - c.rhs).simplify, var, a, b))
599
+ when Interval then found.push(c.low, c.high)
600
+ when RealSet then c.intervals.each { |i| found.push(i.low, i.high) }
601
+ end
602
+ end
603
+ end
604
+ end
605
+ poles = begin
606
+ Integrate.singular_points(f, var, Num.new(Rational(a)), Num.new(Rational(b)))
607
+ rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
608
+ RCAS.guard!(rescued, refused: true)
609
+ nil
610
+ end
611
+ found.concat(poles) if poles
612
+ values = found.filter_map do |p|
613
+ v = Analysis.numeric(p)
614
+ v && v > a && v < b ? [v, p] : nil
615
+ end
616
+ values.uniq { |v, _| v }.sort_by(&:first).first(MAX_BREAKPOINTS).map(&:last)
617
+ rescue StandardError => rescued
618
+ RCAS.guard!(rescued)
619
+ []
620
+ end
621
+
622
+ def zeros_between(u, var, a, b)
623
+ return [] unless u.variables.include?(var.name)
624
+ roots = Solve.solve(u, var, domain: RR)
625
+ return [] unless roots.is_a?(Array)
626
+ roots.flat_map { |r| r.is_a?(ImageSet) ? (r.between(a, b, limit: MAX_BREAKPOINTS) || []) : [r] }
627
+ rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
628
+ RCAS.guard!(rescued, refused: true)
629
+ []
630
+ end
631
+
632
+ def integer_crossings(node, var, a, b)
633
+ ab = Integrate.linear(node.args.first, var) or return []
634
+ slope, offset = ab.map { |v| Analysis.numeric(v) }
635
+ return [] if slope.nil? || offset.nil? || slope.zero?
636
+ ends = [slope * a + offset, slope * b + offset].minmax
637
+ shift = node.name == :round ? 0.5 : 0
638
+ first = (ends[0] - shift).floor + 1
639
+ last = (ends[1] - shift).ceil - 1
640
+ return [] if last - first > MAX_BREAKPOINTS
641
+ (first..last).map { |n| ((Num.new(n) + Num.new(Rational(shift)) - ab[1]) / ab[0]).simplify }
642
+ end
643
+
644
+ # Adaptive Simpson with a convergence test and no free zeros: nil when
645
+ # a sample has no value or the refinement does not settle.
646
+ def checked_simpson(g, lo, hi)
647
+ g = patch_ends(g, lo, hi)
648
+ whole = simpson_rule(g, lo, hi)
649
+ return nil unless whole&.finite?
650
+ refine(g, lo, hi, whole, 1e-11 * [1.0, whole.abs].max, 30)
651
+ end
652
+
653
+ # nil when the budget runs out before the estimate settles: the caller
654
+ # then asks the slower quadrature, which is the one that copes with a
655
+ # singularity. The tolerance follows the size of the integral, so a
656
+ # large smooth one is not driven to the end of the budget for nothing.
657
+ def bounded_simpson(g, lo, hi)
658
+ g = patch_ends(g, lo, hi)
659
+ whole = simpson_rule(g, lo, hi)
660
+ return nil unless whole&.finite?
661
+ value = refine(g, lo, hi, whole, SIMPSON_TOLERANCE * [1.0, whole.abs].max, SIMPSON_DEPTH)
662
+ value.nil? || resonant?(g, lo, hi, value) ? nil : value
663
+ end
664
+
665
+ # Gauss-Legendre on [-1, 1], seven nodes: irrational positions, which is
666
+ # the whole point of them here.
667
+ GAUSS_NODES = [0.0, -0.4058451513773972, 0.4058451513773972, -0.7415311855993945,
668
+ 0.7415311855993945, -0.9491079123427585, 0.9491079123427585].freeze
669
+ GAUSS_WEIGHTS = [0.4179591836734694, 0.3818300505051189, 0.3818300505051189, 0.2797053914892766,
670
+ 0.2797053914892766, 0.1294849661688697, 0.1294849661688697].freeze
671
+ GAUSS_PANELS = 8
672
+ GAUSS_ESCALATIONS = 3 # 8, 32, 128, 512 panels
673
+ GAUSS_TOLERANCE = 1e-9
674
+
675
+ # A second opinion, from nodes that are not on the halving grid.
676
+ # Adaptive Simpson refines by halving, so an integrand whose period
677
+ # divides the interval puts every sample on the same phase: the
678
+ # refinement then agrees with itself all the way down and returns a
679
+ # confident wrong number. sin(x)**2 over 0..100*pi came back as 0, and
680
+ # 1/(2 + cos(x)) over 0..1000*pi as 1047 where the answer is 1814. No
681
+ # period lines up with the Gauss nodes, so the two answers part company
682
+ # there - by half and more - while on a smooth integrand they agree to
683
+ # the last digit (5.6e-16 at worst over the ten measured for
684
+ # SIMPSON_TOLERANCE). No second opinion (the integrand is undefined at
685
+ # one of the nodes) is not a veto.
686
+ # An integrand that really does oscillate is not resonant, and Gauss on
687
+ # eight panels cannot see 500 periods either: when the two disagree the
688
+ # panels are multiplied until the second opinion settles. Settling on
689
+ # Simpson's answer clears it; settling anywhere else, or not settling
690
+ # at all inside the budget, does not.
691
+ def resonant?(g, lo, hi, value)
692
+ panels = GAUSS_PANELS
693
+ previous = nil
694
+ (GAUSS_ESCALATIONS + 1).times do
695
+ estimate = gauss_legendre(g, lo, hi, panels) or return false
696
+ return false if agree?(estimate, value)
697
+ return true if previous && agree?(estimate, previous)
698
+ previous = estimate
699
+ panels *= 4
700
+ end
701
+ true
702
+ end
703
+
704
+ def agree?(a, b) = (a - b).abs <= GAUSS_TOLERANCE * [1.0, a.abs, b.abs].max
705
+
706
+ def gauss_legendre(g, lo, hi, panels = GAUSS_PANELS)
707
+ width = (hi - lo) / panels.to_f
708
+ half = width / 2
709
+ total = 0.0
710
+ panels.times do |i|
711
+ centre = lo + width * (i + 0.5)
712
+ GAUSS_NODES.each_with_index do |node, j|
713
+ value = g.call(centre + half * node)
714
+ return nil if value.nil?
715
+ total += GAUSS_WEIGHTS[j] * value * half
716
+ end
717
+ end
718
+ total.finite? ? total : nil
719
+ end
720
+
721
+ # An end the caller cannot evaluate takes the value just inside it -
722
+ # sin(x)/x at 0 is a hole in the caller, not in the integral, and
723
+ # reading it as 0 (which is what simpson_rule does with a nil) invents
724
+ # a discontinuity. The interval itself is not moved, so no area is
725
+ # lost. An end that is genuinely singular gives a huge value here, does
726
+ # not settle, and goes to the quadrature below as before.
727
+ def patch_ends(g, lo, hi)
728
+ step = (hi - lo) * 1e-8
729
+ ends = {}
730
+ ends[lo] = g.call(lo + step) if g.call(lo).nil?
731
+ ends[hi] = g.call(hi - step) if g.call(hi).nil?
732
+ return g if ends.empty? || ends.value?(nil)
733
+ ->(t) { ends.key?(t) ? ends[t] : g.call(t) }
734
+ end
735
+
736
+ def refine(g, lo, hi, whole, tolerance, depth)
737
+ middle = (lo + hi) / 2.0
738
+ left = simpson_rule(g, lo, middle)
739
+ right = simpson_rule(g, middle, hi)
740
+ return nil if left.nil? || right.nil? || !(left + right).finite?
741
+ return left + right + (left + right - whole) / 15.0 if (left + right - whole).abs <= 15 * tolerance
742
+ return nil if depth.zero?
743
+ a = refine(g, lo, middle, left, tolerance / 2, depth - 1) or return nil
744
+ b = refine(g, middle, hi, right, tolerance / 2, depth - 1) or return nil
745
+ a + b
746
+ end
747
+
748
+ # The same tanh-sinh quadrature the digits: form uses, at Float
749
+ # precision: the one that copes with an endpoint singularity, and the
750
+ # one that says when an integral does not settle at all. nil hands the
751
+ # integrand back to Simpson when arbitrary precision has no route for
752
+ # it (an unknown function, a value that is not real).
753
+ def tanh_sinh(f, var, from, to)
754
+ value = Precision.quadrature(Expression.lift(f), var, Expression.lift(from), Expression.lift(to), FLOAT_DIGITS)
755
+ value.to_f
756
+ rescue Precision::NoConvergence => e
757
+ raise ArgumentError, "nintegrate: #{e.message.sub('evalf: ', '')}"
758
+ rescue Precision::Unsupported, ZeroDivisionError
759
+ nil
760
+ end
761
+
762
+ def bound(value)
763
+ return Float::INFINITY if value == OO
764
+ return -Float::INFINITY if value == Neg.new(OO).simplify || value == Neg.new(OO)
765
+ v = Expression.lift(value).evalf
766
+ raise ArgumentError, "nintegrate: #{value} is not a real bound" unless v.is_a?(Numeric) && !v.is_a?(Complex)
767
+ v.to_f
768
+ end
769
+
770
+ # x = t/(1 - t**2) on (-1, 1) for a doubly infinite range, x = a + t/(1 - t)
771
+ # for one that is infinite on one side only; checked Simpson on the
772
+ # open interval (the ends are skipped), nil when it does not settle.
773
+ def transformed(f, var, lo, hi)
774
+ g = caller_for(f, var)
775
+ if lo.infinite? && hi.infinite?
776
+ integrand = ->(t) { value = g.call(t / (1 - t * t)); value && value * (1 + t * t) / (1 - t * t)**2 }
777
+ quadrature(integrand, -1.0, 1.0)
778
+ elsif hi.infinite?
779
+ integrand = ->(t) { value = g.call(lo + t / (1 - t)); value && value / (1 - t)**2 }
780
+ quadrature(integrand, 0.0, 1.0)
781
+ else
782
+ integrand = ->(t) { value = g.call(hi - t / (1 - t)); value && value / (1 - t)**2 }
783
+ (v = quadrature(integrand, 0.0, 1.0)) && -v
784
+ end
785
+ end
786
+
787
+ def quadrature(g, lo, hi)
788
+ inset = (hi - lo) * 1e-10
789
+ checked_simpson(g, lo + inset, hi - inset)
790
+ end
791
+
792
+ # nil when a sample has no value: an undefined point is not a zero.
793
+ def simpson_rule(g, lo, hi)
794
+ middle = (lo + hi) / 2.0
795
+ values = [g.call(lo), g.call(middle), g.call(hi)]
796
+ return nil if values.any?(&:nil?)
797
+ (hi - lo) / 6.0 * (values[0] + 4 * values[1] + values[2])
798
+ end
799
+
800
+ # ---- evalf on unevaluated nodes --------------------------------------------------
801
+
802
+ # Replace every definite integral that has no free variables left by its
803
+ # numeric value; evalf calls this, so an integral rcas cannot do in closed
804
+ # form still gives a number.
805
+ def resolve(expr)
806
+ return expr unless expr.is_a?(Expression)
807
+ expr = expr.map_children { |c| resolve(c) }
808
+ return expr unless expr.is_a?(Integral) && expr.definite?
809
+ free = expr.integrand.variables - [expr.var.name]
810
+ return expr unless free.empty?
811
+ # evalf floated the bounds, and oo became Float::INFINITY on the way:
812
+ # give nintegrate the infinity back, or it takes the finite route
813
+ value = nintegrate(expr.integrand, expr.var, infinite_bound(expr.from), infinite_bound(expr.to))
814
+ value.is_a?(Numeric) && value.finite? ? Num.new(value) : expr
815
+ rescue StandardError => rescued
816
+ RCAS.guard!(rescued)
817
+ expr
818
+ end
819
+
820
+ def infinite_bound(b)
821
+ return b unless b.is_a?(Num) && b.value.is_a?(Float) && b.value.infinite?
822
+ b.value.positive? ? OO : Neg.new(OO).simplify
823
+ end
824
+ end
825
+ end