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,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
module Integrate
|
|
5
|
+
# Substitutions that turn an integrand into a rational function of a new
|
|
6
|
+
# variable, which the exact rational integrator then finishes:
|
|
7
|
+
#
|
|
8
|
+
# R(x, sqrt(a x^2 + b x + c)) split into R0(x) + N(x) / (D(x) sqrt(Q));
|
|
9
|
+
# after partial fractions, P(x)/sqrt(Q) = d/dx[S(x) sqrt(Q)] +
|
|
10
|
+
# lambda/sqrt(Q) and x - alpha = 1/t for linear denominators
|
|
11
|
+
# R(x, (a x + b)^(1/n)) t = (a x + b)^(1/n)
|
|
12
|
+
# R(exp(k x)), R(sinh, cosh) t = exp(r x), r the gcd of the rates
|
|
13
|
+
# R(sin u, cos u) t = tan(u/2)
|
|
14
|
+
#
|
|
15
|
+
# The results are formal antiderivatives: differentiating them gives the
|
|
16
|
+
# integrand wherever both are defined, but across a singularity of the
|
|
17
|
+
# integrand the constant may jump, as in every CAS.
|
|
18
|
+
#
|
|
19
|
+
# Sources (keys: MANUAL.md, Sources): [Zor15, §5.7]; [Har16, ch. V-VI].
|
|
20
|
+
module Substitutions
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def depends?(e, x) = Integrate.depends?(e, x)
|
|
24
|
+
def exact_num?(e) = e.is_a?(Num) && Integrate.exact?(e.value)
|
|
25
|
+
|
|
26
|
+
# ---- R(x, sqrt(Q)), Q quadratic with rational coefficients ------------
|
|
27
|
+
|
|
28
|
+
def radical(f, x, depth)
|
|
29
|
+
ring = QQ[x.name]
|
|
30
|
+
root = radicand(f, x, ring) or return normalized_radical(f, x, depth)
|
|
31
|
+
even = Num.new(0) # R0(x)
|
|
32
|
+
odd = Num.new(0) # R1(x), to be multiplied by sqrt(Q)
|
|
33
|
+
constant, table = Expand.table(f)
|
|
34
|
+
even += Num.new(constant)
|
|
35
|
+
table.each do |factors, coeff|
|
|
36
|
+
return nil unless Integrate.exact?(coeff)
|
|
37
|
+
rest = Num.new(coeff)
|
|
38
|
+
k = 0 # power of sqrt(Q)
|
|
39
|
+
factors.each do |base, e|
|
|
40
|
+
if base == root
|
|
41
|
+
return nil unless e.is_a?(Integer) || (e.is_a?(Rational) && e.denominator == 2)
|
|
42
|
+
k += e.is_a?(Integer) ? 2 * e : e.numerator
|
|
43
|
+
elsif e.is_a?(Integer) && polynomial?(base, ring)
|
|
44
|
+
rest *= base**e
|
|
45
|
+
else
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
q, r = k.divmod(2)
|
|
50
|
+
rest *= root**q
|
|
51
|
+
r.zero? ? even += rest : odd += rest
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = Num.new(0)
|
|
55
|
+
even = even.simplify
|
|
56
|
+
unless Scalar.zero?(even)
|
|
57
|
+
part = Integrate.attempt(even.cancel, x, depth + 1)
|
|
58
|
+
return nil unless part && Integrate.complete?(part)
|
|
59
|
+
result += part
|
|
60
|
+
end
|
|
61
|
+
odd = odd.simplify
|
|
62
|
+
unless Scalar.zero?(odd)
|
|
63
|
+
part = over_root((odd * root).cancel, root, ring, x, depth) or return nil
|
|
64
|
+
result += part
|
|
65
|
+
end
|
|
66
|
+
result.simplify
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A quadratic with real constant coefficients, a*x**2 + b*x + c, is
|
|
70
|
+
# K*(e1*v**2 + e2) with e1, e2 = +-1, K > 0, x = lambda*v + mu: complete
|
|
71
|
+
# the square, a*(u**2 + d) with u = x + b/(2a), and scale u = sqrt|d|*v.
|
|
72
|
+
# The integrand in v has a rational radicand, which the rest of this
|
|
73
|
+
# rule takes: sqrt(1 + 4*pi**2*x**2) was out of reach, and the surface
|
|
74
|
+
# of revolution of sin(2*pi*x) ran through the whole heuristic instead
|
|
75
|
+
# (fourth review).
|
|
76
|
+
def normalized_radical(f, x, depth)
|
|
77
|
+
bases = f.each_node.select { |n| n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) && depends?(n.base, x) }
|
|
78
|
+
.map(&:base).uniq
|
|
79
|
+
return nil unless bases.size == 1
|
|
80
|
+
base = bases.first
|
|
81
|
+
c, b, a = Solve.polynomial_coefficients(base, x)
|
|
82
|
+
return nil unless a && Solve.polynomial_coefficients(base, x).size == 3 && [a, b, c].all? { |k| k.variables.empty? }
|
|
83
|
+
sa = Decide.sign(a)
|
|
84
|
+
return nil unless %i[positive negative].include?(sa)
|
|
85
|
+
mu = (Neg.new(b) / (2 * a)).simplify
|
|
86
|
+
d = (c / a - b**2 / (4 * a**2)).simplify
|
|
87
|
+
sd = Decide.sign(d)
|
|
88
|
+
return nil unless %i[positive negative].include?(sd)
|
|
89
|
+
e1 = sa == :positive ? 1 : -1
|
|
90
|
+
e2 = e1 * (sd == :positive ? 1 : -1)
|
|
91
|
+
magnitude = sd == :positive ? d : Neg.new(d).simplify
|
|
92
|
+
lambda = RCAS.sqrt(magnitude).simplify
|
|
93
|
+
k = ((sa == :positive ? a : Neg.new(a)) * magnitude).simplify
|
|
94
|
+
v = Var.new(:"_n#{depth}")
|
|
95
|
+
inner = (Num.new(e1) * v**2 + Num.new(e2)).simplify
|
|
96
|
+
g = (replace_quadratic(f, base, k, inner).subs(x => lambda * v + mu) * lambda).simplify
|
|
97
|
+
return nil if depends?(g, x)
|
|
98
|
+
r = Integrate.attempt(g, v, depth + 1)
|
|
99
|
+
return nil unless r && Integrate.complete?(r)
|
|
100
|
+
r.subs(v => (x - mu) / lambda).simplify
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def replace_quadratic(e, base, k, inner)
|
|
104
|
+
if e.is_a?(Pow) && e.base == base && e.exponent.is_a?(Num)
|
|
105
|
+
return (k**e.exponent) * inner**e.exponent
|
|
106
|
+
end
|
|
107
|
+
return k * inner if e == base
|
|
108
|
+
e.map_children { |child| replace_quadratic(child, base, k, inner) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The quadratic under a square root in f, or nil.
|
|
112
|
+
def radicand(f, x, ring)
|
|
113
|
+
bases = f.each_node.select { |n| n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) && depends?(n.base, x) }
|
|
114
|
+
.map(&:base).uniq
|
|
115
|
+
return nil unless bases.size == 1
|
|
116
|
+
base = bases.first
|
|
117
|
+
poly = polynomial?(base, ring) or return nil
|
|
118
|
+
poly.degree == 2 ? base : nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def polynomial?(e, ring)
|
|
122
|
+
ring.call(e)
|
|
123
|
+
rescue DomainError, ArgumentError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def quadratic_coefficients(root, ring)
|
|
128
|
+
poly = ring.call(root)
|
|
129
|
+
[2, 1, 0].map { |k| poly.coeff(k).value } # [a, b, c]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# int m / sqrt(Q) dx for m rational in x.
|
|
133
|
+
def over_root(m, root, ring, x, depth)
|
|
134
|
+
a, b, c = quadratic_coefficients(root, ring)
|
|
135
|
+
return nil if (b * b - 4 * a * c).zero?
|
|
136
|
+
parts = begin
|
|
137
|
+
RationalFunction.apart(m, x)
|
|
138
|
+
rescue DomainError
|
|
139
|
+
return nil if depends?(m, x) # not a rational function of x
|
|
140
|
+
m
|
|
141
|
+
end
|
|
142
|
+
constant, terms = Simplify.termize(parts, simplify: true)
|
|
143
|
+
polynomial = Hash.new(0)
|
|
144
|
+
polynomial[0] = constant
|
|
145
|
+
result = Num.new(0)
|
|
146
|
+
terms.each do |factors, coeff|
|
|
147
|
+
base, e = factors.first
|
|
148
|
+
if factors.size == 1 && base == x && e.is_a?(Integer) && e.positive?
|
|
149
|
+
polynomial[e] += coeff
|
|
150
|
+
elsif factors.size == 1 && e.is_a?(Integer) && e.negative? && (ab = Integrate.linear(base, x)) && ab.all? { |v| exact_num?(v) }
|
|
151
|
+
slope, offset = ab.map(&:value)
|
|
152
|
+
alpha = (-offset).quo(slope) # Integers: -1/2 is not -1
|
|
153
|
+
part = linear_denominator(alpha, -e, root, [a, b, c], x, depth) or return nil
|
|
154
|
+
result += Num.new(coeff.quo(slope**(-e))) * part
|
|
155
|
+
else
|
|
156
|
+
return nil # an irreducible quadratic denominator: not covered
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
part = polynomial_over_root(polynomial, root, [a, b, c], x) or return nil
|
|
160
|
+
(result + part).simplify
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# int P(x)/sqrt(Q) dx = S(x) sqrt(Q) + lambda int dx/sqrt(Q), deg S = deg P - 1,
|
|
164
|
+
# from P = S' Q + S Q'/2 + lambda by comparing coefficients.
|
|
165
|
+
def polynomial_over_root(polynomial, root, abc, x)
|
|
166
|
+
a, b, c = abc
|
|
167
|
+
n = polynomial.keys.max
|
|
168
|
+
base = base_integral(root, abc, x) or return nil
|
|
169
|
+
return (Num.new(polynomial[0]) * base).simplify if n.zero?
|
|
170
|
+
|
|
171
|
+
rows = (0..n).map do |i|
|
|
172
|
+
row = (0...n).map do |j| # coefficient of x^i in j x^(j-1) Q + x^j (a x + b/2)
|
|
173
|
+
v = 0r
|
|
174
|
+
v += a * (j + 1) if i == j + 1
|
|
175
|
+
v += b * (j + Rational(1, 2)) if i == j
|
|
176
|
+
v += c * j if i == j - 1
|
|
177
|
+
Num.new(v)
|
|
178
|
+
end
|
|
179
|
+
row << Num.new(i.zero? ? 1 : 0) # lambda
|
|
180
|
+
row << Num.new(polynomial[i])
|
|
181
|
+
row
|
|
182
|
+
end
|
|
183
|
+
reduced, pivots = Elimination.rref(rows)
|
|
184
|
+
return nil unless pivots == (0..n).to_a
|
|
185
|
+
s = (0...n).map { |j| reduced[j][n + 1] }
|
|
186
|
+
lambda = reduced[n][n + 1]
|
|
187
|
+
shape = s.each_with_index.map { |v, j| v * x**j }.reduce(:+)
|
|
188
|
+
(shape * root**Rational(1, 2) + lambda * base).simplify
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# int dx / sqrt(a x^2 + b x + c)
|
|
192
|
+
def base_integral(root, abc, x)
|
|
193
|
+
a, b, c = abc
|
|
194
|
+
y = root**Rational(1, 2)
|
|
195
|
+
disc = b * b - 4 * a * c
|
|
196
|
+
if a.positive?
|
|
197
|
+
sa = RCAS.sqrt(Num.new(a))
|
|
198
|
+
Fn.new(:log, [sa * y + Num.new(a) * x + Num.new(b.quo(2))]) / sa
|
|
199
|
+
elsif disc.positive?
|
|
200
|
+
sa = RCAS.sqrt(Num.new(-a))
|
|
201
|
+
Fn.new(:asin, [(Num.new(-2 * a) * x - Num.new(b)) / RCAS.sqrt(Num.new(disc))]) / sa
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# int dx / ((x - alpha)^k sqrt(Q)) with x - alpha = 1/t:
|
|
206
|
+
# Q(x) = (Q(alpha) t^2 + Q'(alpha) t + a) / t^2 =: Qt(t) / t^2
|
|
207
|
+
# integrand -> -sign(t) t^(k-1) / sqrt(Qt) dt, and sqrt(Qt) = sqrt(Q)*|t|.
|
|
208
|
+
# The sign is the part that is easy to lose: sqrt(Q) = sqrt(Qt)/|t|,
|
|
209
|
+
# not sqrt(Qt)/t, and without it the antiderivative had the wrong sign
|
|
210
|
+
# left of alpha (the fourth review, D1: int_{-3}^{-1} dx/((2x + 1)
|
|
211
|
+
# sqrt(x**2 + 1)) came out positive).
|
|
212
|
+
def linear_denominator(alpha, k, root, abc, x, depth)
|
|
213
|
+
a, b, c = abc
|
|
214
|
+
t = Var.new(:"_t#{depth}")
|
|
215
|
+
q0 = a * alpha * alpha + b * alpha + c
|
|
216
|
+
q1 = 2 * a * alpha + b
|
|
217
|
+
qt = (Num.new(q0) * t**2 + Num.new(q1) * t + Num.new(a)).simplify
|
|
218
|
+
g = (-t**(k - 1) / qt**Rational(1, 2)).simplify
|
|
219
|
+
r = Integrate.attempt(g, t, depth + 1)
|
|
220
|
+
return nil unless r && Integrate.complete?(r)
|
|
221
|
+
shifted = (x - Num.new(alpha)).simplify
|
|
222
|
+
r = replace_root(r, qt, root**Rational(1, 2) / Fn.new(:abs, [shifted]), 2)
|
|
223
|
+
(Fn.new(:sign, [shifted]) * r.subs(t => 1 / shifted)).simplify
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Replace base**e by root**(n e) (root stands for base**(1/n)) and a bare
|
|
227
|
+
# base by root**n, comparing canonical forms.
|
|
228
|
+
def replace_root(expr, base, root, n)
|
|
229
|
+
if expr.is_a?(Pow) && expr.exponent.is_a?(Num) && expr.base.simplify == base
|
|
230
|
+
return root**Num.new(n * expr.exponent.value)
|
|
231
|
+
end
|
|
232
|
+
return root**n if (expr.is_a?(Add) || expr.is_a?(Sub) || expr.is_a?(Fn)) && expr.simplify == base
|
|
233
|
+
expr.map_children { |c| replace_root(c, base, root, n) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# ---- exp(a x^2 + b x + c), a < 0: the error function ---------------------------
|
|
237
|
+
#
|
|
238
|
+
# int exp(q) dx = exp(c - b^2/(4a)) * sqrt(pi) / (2 sqrt(-a)) * erf(sqrt(-a) x - b / (2 sqrt(-a)))
|
|
239
|
+
def gaussian(f, x)
|
|
240
|
+
return gaussian_moment(f, x) unless f.is_a?(Fn)
|
|
241
|
+
return nil unless f.name == :exp && f.args.size == 1
|
|
242
|
+
cs = Solve.polynomial_coefficients(f.args.first, x)
|
|
243
|
+
return nil unless cs && cs.size == 3 && cs[2].is_a?(Num) && cs[2].value.real? && cs[2].value.negative?
|
|
244
|
+
c, b, a = cs
|
|
245
|
+
s = RCAS.sqrt(Num.new(-a.value))
|
|
246
|
+
argument = (s * x - b / (2 * s)).simplify
|
|
247
|
+
scale = (Fn.new(:exp, [c - b**2 / (4 * a)]) * RCAS.sqrt(PI) / (2 * s)).simplify
|
|
248
|
+
(scale * RCAS.erf(argument)).simplify
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# int x^n e^q = x^(n-1) e^q / (2a) - b/(2a) int x^(n-1) e^q - (n-1)/(2a) int x^(n-2) e^q
|
|
252
|
+
def gaussian_moment(f, x)
|
|
253
|
+
_, factors = Simplify.factorize(f)
|
|
254
|
+
return nil unless factors.size == 2 && factors[x].is_a?(Integer) && factors[x].positive? && factors.key?(Simplify.exp_base)
|
|
255
|
+
n = factors[x]
|
|
256
|
+
q = Expression.lift(factors[Simplify.exp_base])
|
|
257
|
+
cs = Solve.polynomial_coefficients(q, x)
|
|
258
|
+
return nil unless cs && cs.size == 3 && cs[2].is_a?(Num) && cs[2].value.real? && cs[2].value.negative?
|
|
259
|
+
_, b, a = cs
|
|
260
|
+
e = Fn.new(:exp, [q])
|
|
261
|
+
lower = ->(k) { k.zero? ? gaussian(e, x) : gaussian_moment((x**k * e).simplify, x) }
|
|
262
|
+
first = lower.call(n - 1) or return nil
|
|
263
|
+
result = x**(n - 1) * e / (2 * a) - b / (2 * a) * first
|
|
264
|
+
if n >= 2
|
|
265
|
+
second = lower.call(n - 2) or return nil
|
|
266
|
+
result -= Num.new(n - 1) / (2 * a) * second
|
|
267
|
+
end
|
|
268
|
+
result.simplify
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# ---- R(x, (a x + b)^(1/n)) -------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def root_of_linear(f, x, depth)
|
|
274
|
+
powers = f.each_node.select { |n| n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) && depends?(n.base, x) }
|
|
275
|
+
return nil if powers.empty?
|
|
276
|
+
bases = powers.map(&:base).uniq
|
|
277
|
+
return nil unless bases.size == 1
|
|
278
|
+
base = bases.first
|
|
279
|
+
ab = Integrate.linear(base, x)
|
|
280
|
+
return nil unless ab && ab.all? { |v| exact_num?(v) }
|
|
281
|
+
slope, offset = ab
|
|
282
|
+
n = powers.map { |p| p.exponent.value.denominator }.reduce(1) { |l, d| l.lcm(d) }
|
|
283
|
+
t = Var.new(:"_w#{depth}")
|
|
284
|
+
g = replace_root(f, base.simplify, t, n).subs(x => (t**n - offset) / slope)
|
|
285
|
+
g = (g * Num.new(n) * t**(n - 1) / slope).cancel
|
|
286
|
+
return nil if depends?(g, x)
|
|
287
|
+
r = Integrate.attempt(g, t, depth + 1)
|
|
288
|
+
return nil unless r && Integrate.complete?(r)
|
|
289
|
+
r.subs(t => base**Rational(1, n)).simplify
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# ---- R(x, ((a x + b)/(c x + d))^(1/n)) -------------------------------------
|
|
293
|
+
|
|
294
|
+
# The Moebius substitution: t = ((a*x + b)/(c*x + d))**(1/n) turns
|
|
295
|
+
# x = (d*t**n - b)/(a - c*t**n) and the whole integrand into a rational
|
|
296
|
+
# function of t. Covers sqrt((1 - x)/(1 + x)) and its relatives, which
|
|
297
|
+
# the root-of-a-linear-form rule above cannot reach. [Har16, ch. III]
|
|
298
|
+
def root_of_ratio(f, x, depth)
|
|
299
|
+
powers = f.each_node.select { |n| n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) && !n.exponent.value.integer? && depends?(n.base, x) }
|
|
300
|
+
bases = powers.map(&:base).uniq
|
|
301
|
+
return nil unless bases.size == 1
|
|
302
|
+
base = bases.first.simplify
|
|
303
|
+
a, b = Integrate.linear(RationalFunction.numer(base), x)
|
|
304
|
+
c, d = Integrate.linear(RationalFunction.denom(base), x)
|
|
305
|
+
return nil unless [a, b, c, d].all? { |v| v && exact_num?(v) }
|
|
306
|
+
return nil if Scalar.zero?(c) # a linear form: root_of_linear did that
|
|
307
|
+
det = (a * d - b * c).simplify
|
|
308
|
+
return nil if Scalar.zero?(det)
|
|
309
|
+
|
|
310
|
+
n = powers.map { |p| p.exponent.value.denominator }.reduce(1) { |l, e| l.lcm(e) }
|
|
311
|
+
t = Var.new(:"_m#{depth}")
|
|
312
|
+
xt = ((d * t**n - b) / (a - c * t**n)).simplify
|
|
313
|
+
g = replace_root(f, base, t, n).subs(x => xt)
|
|
314
|
+
g = (g * xt.diff(t)).cancel
|
|
315
|
+
return nil if depends?(g, x)
|
|
316
|
+
r = Integrate.attempt(g, t, depth + 1)
|
|
317
|
+
return nil unless r && Integrate.complete?(r)
|
|
318
|
+
r.subs(t => base**Rational(1, n)).simplify
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# ---- R(exp(k x)) ---------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def exponential(f, x, depth)
|
|
324
|
+
g = rewrite_hyperbolic(f)
|
|
325
|
+
exps = g.each_node.select { |n| n.is_a?(Fn) && n.name == :exp && depends?(n.args.first, x) }.uniq
|
|
326
|
+
return nil if exps.empty?
|
|
327
|
+
rates = exps.map do |e|
|
|
328
|
+
ab = Integrate.linear(e.args.first, x)
|
|
329
|
+
return nil unless ab && exact_num?(ab.first)
|
|
330
|
+
ab
|
|
331
|
+
end
|
|
332
|
+
denominator = rates.map { |c, _| Rational(c.value).denominator }.reduce(1) { |l, d| l.lcm(d) }
|
|
333
|
+
numerator = rates.map { |c, _| (Rational(c.value) * denominator).to_i }.reduce(0) { |acc, m| acc.gcd(m) }
|
|
334
|
+
rate = Rational(numerator, denominator)
|
|
335
|
+
t = Var.new(:"_e#{depth}")
|
|
336
|
+
map = exps.zip(rates).to_h do |e, (c, d)|
|
|
337
|
+
power = t**Integer(Rational(c.value) / rate)
|
|
338
|
+
[e, Scalar.zero?(d) ? power : Fn.new(:exp, [d]) * power]
|
|
339
|
+
end
|
|
340
|
+
h = g.subs(map).simplify
|
|
341
|
+
return nil if depends?(h, x)
|
|
342
|
+
r = Integrate.attempt((h / (Num.new(rate) * t)).cancel, t, depth + 1)
|
|
343
|
+
return nil unless r && Integrate.complete?(r)
|
|
344
|
+
real_log_exp(r.subs(t => Fn.new(:exp, [Num.new(rate) * x])), x).simplify
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# log(exp(u)) is u only on the principal strip, so the general fold
|
|
348
|
+
# leaves it alone (log(exp(2*pi*i)) is 0). The integration variable is
|
|
349
|
+
# real, and the substitution t = exp(r*x) has just put that exp there
|
|
350
|
+
# itself, so here the rule does hold: without it the antiderivative of
|
|
351
|
+
# 1/(1 + exp(x)) came back as -log(1 + exp(x)) + log(exp(x)).
|
|
352
|
+
def real_log_exp(expr, x)
|
|
353
|
+
if expr.is_a?(Fn) && expr.name == :log && expr.args.size == 1 &&
|
|
354
|
+
(inner = expr.args.first).is_a?(Fn) && inner.name == :exp && depends?(inner.args.first, x)
|
|
355
|
+
return inner.args.first
|
|
356
|
+
end
|
|
357
|
+
expr.map_children { |c| real_log_exp(c, x) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# sin(u)**(2k) / cos(u)**n = (1 - cos(u)**2)**k / cos(u)**n, and the same with
|
|
361
|
+
# the roles swapped: a sum of powers of one function, which the table and
|
|
362
|
+
# the heuristic handle in their familiar forms (1/cos(x) => log((1 + sin(x))/cos(x))).
|
|
363
|
+
def reduce_even_power(f, x, depth)
|
|
364
|
+
_, factors = Simplify.factorize(Trigonometry.expand_trig(f).simplify)
|
|
365
|
+
return nil unless factors.size == 2 && factors.values.all? { |e| e.is_a?(Integer) }
|
|
366
|
+
fns = factors.keys
|
|
367
|
+
return nil unless fns.all? { |b| b.is_a?(Fn) && %i[sin cos].include?(b.name) } && fns.map(&:name).sort == %i[cos sin]
|
|
368
|
+
return nil unless fns.map { |b| b.args.first } .uniq.size == 1
|
|
369
|
+
sin_f, cos_f = fns.sort_by { |b| b.name == :sin ? 0 : 1 }
|
|
370
|
+
m = factors[sin_f]
|
|
371
|
+
n = factors[cos_f]
|
|
372
|
+
g =
|
|
373
|
+
if m.positive? && m.even? && n.negative?
|
|
374
|
+
(1 - cos_f**2)**(m / 2) * cos_f**n
|
|
375
|
+
elsif n.positive? && n.even? && m.negative?
|
|
376
|
+
(1 - sin_f**2)**(n / 2) * sin_f**m
|
|
377
|
+
end
|
|
378
|
+
return nil unless g
|
|
379
|
+
r = Integrate.attempt(g.expand, x, depth + 1)
|
|
380
|
+
r && Integrate.complete?(r) ? r : nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# atan(tan(u)) => u: the same formal antiderivative, without the jumps
|
|
384
|
+
# - for the atan(tan(u)) the substitution t = tan(u/2) put there. One
|
|
385
|
+
# the integrand already had (`keep`) is a sawtooth, not u:
|
|
386
|
+
# integrate(atan(tan(x)), x) is not x**2/2 (third review, D7).
|
|
387
|
+
def unwind(e, keep = [])
|
|
388
|
+
e = e.map_children { |c| unwind(c, keep) }
|
|
389
|
+
if e.is_a?(Fn) && e.name == :atan && e.args.first.is_a?(Fn) && e.args.first.name == :tan && !keep.include?(e)
|
|
390
|
+
return e.args.first.args.first
|
|
391
|
+
end
|
|
392
|
+
e
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def rewrite_hyperbolic(e)
|
|
396
|
+
e = e.map_children { |c| rewrite_hyperbolic(c) }
|
|
397
|
+
return e unless e.is_a?(Fn) && %i[sinh cosh tanh].include?(e.name) && e.args.size == 1
|
|
398
|
+
u = e.args.first
|
|
399
|
+
plus = Fn.new(:exp, [u])
|
|
400
|
+
minus = Fn.new(:exp, [Simplify.negate(u)])
|
|
401
|
+
case e.name
|
|
402
|
+
when :sinh then (plus - minus) / 2
|
|
403
|
+
when :cosh then (plus + minus) / 2
|
|
404
|
+
else (plus - minus) / (plus + minus)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# ---- R(sin u, cos u), the Weierstrass substitution t = tan(u/2) ------------
|
|
409
|
+
|
|
410
|
+
def trigonometric(f, x, depth)
|
|
411
|
+
return nil if f.each_node.none? { |n| n.is_a?(Fn) && %i[sin cos tan].include?(n.name) && depends?(n.args.first, x) }
|
|
412
|
+
reduced = reduce_even_power(f, x, depth)
|
|
413
|
+
return reduced if reduced
|
|
414
|
+
g = Trigonometry.expand_trig(f).simplify
|
|
415
|
+
trigs = g.each_node.select { |n| n.is_a?(Fn) && %i[sin cos].include?(n.name) && depends?(n.args.first, x) }.uniq
|
|
416
|
+
return nil if trigs.empty?
|
|
417
|
+
args = trigs.map { |n| n.args.first.simplify }.uniq
|
|
418
|
+
return nil unless args.size == 1
|
|
419
|
+
u = args.first
|
|
420
|
+
ab = Integrate.linear(u, x) or return nil
|
|
421
|
+
slope = ab.first
|
|
422
|
+
t = Var.new(:"_h#{depth}")
|
|
423
|
+
map = trigs.to_h do |n|
|
|
424
|
+
[n, n.name == :sin ? 2 * t / (1 + t**2) : (1 - t**2) / (1 + t**2)]
|
|
425
|
+
end
|
|
426
|
+
h = g.subs(map).simplify
|
|
427
|
+
return nil if depends?(h, x)
|
|
428
|
+
r = Integrate.attempt((h * 2 / (slope * (1 + t**2))).cancel, t, depth + 1)
|
|
429
|
+
return nil unless r && Integrate.complete?(r)
|
|
430
|
+
r.subs(t => Fn.new(:tan, [u / 2])).simplify
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Polynomial interpolation.
|
|
5
|
+
#
|
|
6
|
+
# interpolate([[0, 1], [1, 3], [2, 7]], x) # => 1 + x + x**2
|
|
7
|
+
# interpolate({ 0 => a, 1 => b }, x) # => a + (b - a)*x (expanded)
|
|
8
|
+
#
|
|
9
|
+
# Newton's divided differences, exact in the coefficient arithmetic
|
|
10
|
+
# (rationals, algebraic numbers, parameters). The result is the unique
|
|
11
|
+
# polynomial of degree < n through the n points, expanded.
|
|
12
|
+
#
|
|
13
|
+
# Sources (keys: MANUAL.md, Sources): [Knu98, §4.6.4]; [vzGG13, ch. 5].
|
|
14
|
+
module Interpolate
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def newton(points, var)
|
|
18
|
+
x = Expression.lift(var)
|
|
19
|
+
raise ArgumentError, "interpolate: the indeterminate must be a symbol, got #{x}" unless x.is_a?(Var)
|
|
20
|
+
pairs = points.is_a?(Hash) ? points.to_a : Array(points)
|
|
21
|
+
raise ArgumentError, "interpolate: give the points as [[x0, y0], [x1, y1], ...] or a hash" unless pairs.all? { |p| p.is_a?(Array) && p.size == 2 }
|
|
22
|
+
nodes = pairs.map { |p, _| Expression.lift(p) }
|
|
23
|
+
values = pairs.map { |_, v| Expression.lift(v) }
|
|
24
|
+
nodes.combination(2).each do |a, b|
|
|
25
|
+
raise ArgumentError, "interpolate: the points #{a} and #{b} coincide" if Scalar.zero?(Scalar.sub(a, b))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
n = nodes.size
|
|
29
|
+
coeffs = values.dup
|
|
30
|
+
(1...n).each do |level|
|
|
31
|
+
(n - 1).downto(level) do |i|
|
|
32
|
+
coeffs[i] = Scalar.div(Scalar.sub(coeffs[i], coeffs[i - 1]), Scalar.sub(nodes[i], nodes[i - level]))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
poly = Num.new(0)
|
|
36
|
+
(n - 1).downto(0) { |i| poly = poly * (x - nodes[i]) + coeffs[i] }
|
|
37
|
+
poly.expand
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/rcas/irb.rb
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "irb"
|
|
4
|
+
require_relative "chat/usage"
|
|
5
|
+
|
|
6
|
+
module RCAS
|
|
7
|
+
# bin/rcas prints rcas's refusals as one line (RCAS::IRB::BRIEF); true
|
|
8
|
+
# gives every error irb's full backtrace again.
|
|
9
|
+
def self.backtrace? = @backtrace.nil? ? ENV["RCAS_BACKTRACE"] == "1" : @backtrace
|
|
10
|
+
|
|
11
|
+
def self.backtrace=(value)
|
|
12
|
+
@backtrace = value.nil? ? nil : !!value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# irb integration. `RCAS::IRB.setup` prepares the session in which it is
|
|
16
|
+
# called (normally the top-level `main` object in bin/rcas):
|
|
17
|
+
#
|
|
18
|
+
# * bare, not-yet-defined identifiers become symbols and are assigned as
|
|
19
|
+
# local variables, so `x + 1` works like `:x + 1` and `x` is `:x` afterwards
|
|
20
|
+
# * sin/cos/tan/exp/log/sqrt are available as plain functions
|
|
21
|
+
# * a friendlier prompt
|
|
22
|
+
module IRB
|
|
23
|
+
IDENTIFIER = RCAS::IDENTIFIER
|
|
24
|
+
|
|
25
|
+
module AutoSymbol
|
|
26
|
+
def method_missing(name, *args, &block)
|
|
27
|
+
return super unless block.nil? && name.match?(IDENTIFIER)
|
|
28
|
+
return RCAS.unknown_function(name, args) || super unless args.empty?
|
|
29
|
+
|
|
30
|
+
binding_for_session&.local_variable_set(name, name)
|
|
31
|
+
name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Deliberately false: Ruby's implicit conversions (to_ary, to_str, ...)
|
|
35
|
+
# probe respond_to? first and must not be satisfied by a Symbol.
|
|
36
|
+
def respond_to_missing?(_name, _include_private = false) = false
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def binding_for_session
|
|
41
|
+
::IRB.CurrentContext&.workspace&.binding
|
|
42
|
+
rescue StandardError # user code: a NameError is an answer here
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.setup(main = TOPLEVEL_BINDING.receiver)
|
|
48
|
+
RubyVM.keep_script_lines = true # lets hold { ... } read blocks typed into irb
|
|
49
|
+
main.singleton_class.include(Functions)
|
|
50
|
+
main.singleton_class.prepend(AutoSymbol)
|
|
51
|
+
RCAS.undefine_kernel_printers(main)
|
|
52
|
+
Object.include(Sets) unless Object.include?(Sets)
|
|
53
|
+
Object.include(Constants) unless Object.include?(Constants)
|
|
54
|
+
main
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# irb resets its configuration inside IRB.setup, so ours is applied after.
|
|
58
|
+
def self.configure(conf = ::IRB.conf)
|
|
59
|
+
conf[:PROMPT][:RCAS] = {
|
|
60
|
+
PROMPT_I: "rcas> ",
|
|
61
|
+
PROMPT_S: "rcas%l ",
|
|
62
|
+
PROMPT_C: "rcas* ",
|
|
63
|
+
RETURN: "=> %s\n"
|
|
64
|
+
}
|
|
65
|
+
conf[:PROMPT_MODE] = :RCAS if STDIN.tty?
|
|
66
|
+
conf[:ECHO_ON_ASSIGNMENT] = true # show whole matrices, not "[1 2]..."
|
|
67
|
+
conf
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Every echoed result is remembered in RCAS::Results, so that `Out[3]`
|
|
71
|
+
# can reach it later; irb formats and pages the value itself.
|
|
72
|
+
module RecordResult
|
|
73
|
+
def output_value(*args)
|
|
74
|
+
RCAS::Results.record(@context.last_value)
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# And every input line, so that `In[3]` can give it back held. irb's own
|
|
80
|
+
# commands and empty input are not lines of the session and get no
|
|
81
|
+
# number.
|
|
82
|
+
module RecordInput
|
|
83
|
+
def evaluate(statement, *args, **options)
|
|
84
|
+
if statement.is_a?(::IRB::Statement::Expression)
|
|
85
|
+
RCAS::Results.record_input(statement.code, workspace.binding)
|
|
86
|
+
RCAS::Lint.warnings(statement.code).each { |message| warn "warning: #{message}" }
|
|
87
|
+
end
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# rcas's refusals and the errors of a call gone wrong are answers, and
|
|
93
|
+
# for them irb's backtrace through its own internals is noise: they
|
|
94
|
+
# print as one line, with the usage of the function when the arguments
|
|
95
|
+
# were wrong. Everything else - a NoMethodError, a TypeError - is a bug
|
|
96
|
+
# somewhere and keeps the whole backtrace. `RCAS.backtrace = true` (or
|
|
97
|
+
# RCAS_BACKTRACE=1) shows it for every error.
|
|
98
|
+
BRIEF = [
|
|
99
|
+
RCAS::Unsupported, NotImplementedError, ArgumentError, RCAS::SeriesError,
|
|
100
|
+
RCAS::Plot::Error, RCAS::Render::Error, RCAS::Docs::NotFound,
|
|
101
|
+
RCAS::OpenMath::ParseError, RCAS::OpenMath::EncodeError
|
|
102
|
+
].freeze
|
|
103
|
+
|
|
104
|
+
def self.brief?(exception) = !RCAS.backtrace? && BRIEF.any? { |kind| exception.is_a?(kind) }
|
|
105
|
+
|
|
106
|
+
# The lines printed for a brief error.
|
|
107
|
+
def self.brief(exception)
|
|
108
|
+
first, *rest = exception.message.lines.map(&:rstrip)
|
|
109
|
+
lines = ["#{exception.class}: #{first}", *rest]
|
|
110
|
+
hint = RCAS::Chat::Usage.hint(exception, user_frame: "(irb)")
|
|
111
|
+
lines.concat(hint.map { |line| " #{line}" }) if hint
|
|
112
|
+
lines
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
module BriefErrors
|
|
116
|
+
def handle_exception(exc)
|
|
117
|
+
return super unless RCAS::IRB.brief?(exc)
|
|
118
|
+
puts RCAS::IRB.brief(exc)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# With RCAS.numbered the prompt carries the number of the line to come
|
|
123
|
+
# (`rcas[3]> `); the results keep irb's `=>`. The internals we lean on
|
|
124
|
+
# here (Statement::Expression, Context#evaluate, Irb#handle_exception,
|
|
125
|
+
# prompt_i, return_format) are those of the irb bundled with Ruby 3.3.
|
|
126
|
+
def self.record_session(irb)
|
|
127
|
+
::IRB::Irb.prepend(RecordResult) unless ::IRB::Irb.include?(RecordResult)
|
|
128
|
+
::IRB::Context.prepend(RecordInput) unless ::IRB::Context.include?(RecordInput)
|
|
129
|
+
::IRB::Irb.prepend(BriefErrors) unless ::IRB::Irb.include?(BriefErrors)
|
|
130
|
+
plain = irb.context.return_format
|
|
131
|
+
irb.context.define_singleton_method(:return_format) { RCAS::Results.return_format(plain) }
|
|
132
|
+
prompt = irb.context.prompt_i
|
|
133
|
+
irb.context.define_singleton_method(:prompt_i) { RCAS::Results.prompt(prompt) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.start(main = TOPLEVEL_BINDING.receiver)
|
|
137
|
+
setup(main)
|
|
138
|
+
::IRB.setup(__FILE__)
|
|
139
|
+
configure
|
|
140
|
+
irb = ::IRB::Irb.new
|
|
141
|
+
::IRB.conf[:MAIN_CONTEXT] = irb.context
|
|
142
|
+
record_session(irb)
|
|
143
|
+
irb.run(::IRB.conf)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|