rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -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
|