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,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Matrix factorizations, exact: LU with row swaps, QR by Gram-Schmidt,
|
|
5
|
+
# Cholesky, diagonalization and the Jordan normal form.
|
|
6
|
+
#
|
|
7
|
+
# l, u, p = matrix([[0, 1], [2, 3]]).lu # p*a == l*u
|
|
8
|
+
# q, r = a.qr # a == q*r, q orthogonal
|
|
9
|
+
# l = a.cholesky # a == l*l.transpose
|
|
10
|
+
# pm, d = a.diagonalize # a == pm*d*pm.inverse
|
|
11
|
+
# pm, j = a.jordan # a == pm*j*pm.inverse
|
|
12
|
+
#
|
|
13
|
+
# The arithmetic is the exact Scalar arithmetic the rest of the matrix
|
|
14
|
+
# code uses, so pivots are swapped only when they are zero (there is no
|
|
15
|
+
# rounding to steer away from): a symbolic pivot that is not identically
|
|
16
|
+
# zero is taken to be non-zero, the generic case, as everywhere else.
|
|
17
|
+
#
|
|
18
|
+
# Sources (keys: MANUAL.md, Sources): LU, QR and Cholesky as in
|
|
19
|
+
# [Str16, ch. 2, 4, 6]; the Jordan form built from chains of generalized
|
|
20
|
+
# eigenvectors as in [HK71, ch. 7].
|
|
21
|
+
module Decompositions
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# P*A = L*U with L unit lower triangular, U upper triangular and P a
|
|
25
|
+
# permutation. Rows are swapped only to get away from a zero pivot.
|
|
26
|
+
def lu(matrix)
|
|
27
|
+
square!(matrix, "lu")
|
|
28
|
+
n = matrix.rows
|
|
29
|
+
u = matrix.to_a
|
|
30
|
+
l = identity_rows(n)
|
|
31
|
+
p = identity_rows(n)
|
|
32
|
+
(0...n).each do |k|
|
|
33
|
+
pivot = (k...n).find { |i| !Scalar.zero?(u[i][k]) }
|
|
34
|
+
next if pivot.nil?
|
|
35
|
+
if pivot != k
|
|
36
|
+
u[k], u[pivot] = u[pivot], u[k]
|
|
37
|
+
p[k], p[pivot] = p[pivot], p[k]
|
|
38
|
+
(0...k).each { |j| l[k][j], l[pivot][j] = l[pivot][j], l[k][j] }
|
|
39
|
+
end
|
|
40
|
+
((k + 1)...n).each do |i|
|
|
41
|
+
factor = Scalar.div(u[i][k], u[k][k])
|
|
42
|
+
l[i][k] = factor
|
|
43
|
+
(k...n).each { |j| u[i][j] = Scalar.sub(u[i][j], Scalar.mul(factor, u[k][j])).simplify }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
[field_matrix(matrix, l), field_matrix(matrix, u), field_matrix(matrix, p)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A = Q*R with the columns of Q orthonormal and R upper triangular.
|
|
50
|
+
# Exact, so the entries of Q carry square roots.
|
|
51
|
+
def qr(matrix)
|
|
52
|
+
basis = LinearAlgebra.gram_schmidt(matrix.column_vectors, normalize: true)
|
|
53
|
+
raise ArgumentError, "qr: the columns of the matrix are not independent" unless basis.size == matrix.cols
|
|
54
|
+
q = field_matrix(matrix, basis.map(&:entries).transpose, matrix.rows, matrix.cols)
|
|
55
|
+
# Q^H A: the conjugate transpose, which is the transpose for real Q
|
|
56
|
+
adjoint = field_matrix(matrix, q.transpose.to_a.map { |row| row.map { |e| LinearAlgebra.conjugate(e) } }, matrix.cols, matrix.rows)
|
|
57
|
+
r = (adjoint * matrix).simplify
|
|
58
|
+
[q, r]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A = L*L' for a symmetric positive definite A.
|
|
62
|
+
def cholesky(matrix)
|
|
63
|
+
square!(matrix, "cholesky")
|
|
64
|
+
raise ArgumentError, "cholesky: the matrix is not symmetric" unless matrix.symmetric?
|
|
65
|
+
n = matrix.rows
|
|
66
|
+
l = Array.new(n) { Array.new(n, Num.new(0)) }
|
|
67
|
+
(0...n).each do |i|
|
|
68
|
+
(0..i).each do |j|
|
|
69
|
+
rest = (0...j).reduce(matrix[i, j]) { |acc, k| Scalar.sub(acc, Scalar.mul(l[i][k], l[j][k])) }.simplify
|
|
70
|
+
l[i][j] =
|
|
71
|
+
if i == j
|
|
72
|
+
raise ArgumentError, "cholesky: the matrix is not positive definite" if nonpositive?(rest)
|
|
73
|
+
Pow.new(rest, Num.new(Rational(1, 2))).simplify
|
|
74
|
+
else
|
|
75
|
+
Scalar.div(rest, l[j][j]).simplify
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
field_matrix(matrix, l)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A = P*D*P**-1 with D diagonal: the eigenvectors as the columns of P.
|
|
83
|
+
def diagonalize(matrix)
|
|
84
|
+
square!(matrix, "diagonalize")
|
|
85
|
+
pairs = matrix.eigenvectors.flat_map { |value, _, vectors| vectors.map { |v| [value, v] } }
|
|
86
|
+
unless pairs.size == matrix.rows
|
|
87
|
+
raise ArgumentError, "diagonalize: #{pairs.size} independent eigenvectors for #{matrix.rows} dimensions; " \
|
|
88
|
+
"the matrix is not diagonalizable - jordan gives its Jordan form"
|
|
89
|
+
end
|
|
90
|
+
[basis_matrix(matrix, pairs.map { |_, v| v.entries }), diagonal(matrix, pairs.map(&:first))]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A = P*J*P**-1 with J in Jordan normal form: one block per chain of
|
|
94
|
+
# generalized eigenvectors.
|
|
95
|
+
def jordan(matrix)
|
|
96
|
+
square!(matrix, "jordan")
|
|
97
|
+
n = matrix.rows
|
|
98
|
+
columns = []
|
|
99
|
+
blocks = []
|
|
100
|
+
Solve.dedupe(matrix.eigenvalues).each do |value|
|
|
101
|
+
chains = chains_for(matrix, value)
|
|
102
|
+
chains.each do |chain|
|
|
103
|
+
columns.concat(chain)
|
|
104
|
+
blocks << [value, chain.size]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
unless columns.size == n
|
|
108
|
+
raise RCAS::Unsupported, "jordan: only #{columns.size} of #{n} basis vectors were found; " \
|
|
109
|
+
"the eigenvalues have to be exact for the chains to be built"
|
|
110
|
+
end
|
|
111
|
+
[basis_matrix(matrix, columns), jordan_matrix(matrix, blocks)]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ---- the Jordan chains ---------------------------------------------------
|
|
115
|
+
|
|
116
|
+
# The chains for one eigenvalue, longest first. A chain is
|
|
117
|
+
# [N**(k-1) u, ..., N u, u] with N = A - lambda and u in ker(N**k) but
|
|
118
|
+
# not in ker(N**(k-1)), so that N maps each vector to the one before it
|
|
119
|
+
# and the first one is an eigenvector.
|
|
120
|
+
def chains_for(matrix, value)
|
|
121
|
+
shifted = matrix - matrix.space.identity.scale(value)
|
|
122
|
+
kernels = nested_kernels(shifted)
|
|
123
|
+
chains = []
|
|
124
|
+
taken = []
|
|
125
|
+
kernels.size.downto(1) do |k|
|
|
126
|
+
candidates(kernels, k).each do |u|
|
|
127
|
+
chain = build_chain(shifted, u, k)
|
|
128
|
+
next if chain.nil? || !independent?(taken + chain.flatten(0), matrix)
|
|
129
|
+
chains << chain
|
|
130
|
+
taken.concat(chain)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
chains
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# [basis of ker(N), basis of ker(N**2), ...] up to the point where the
|
|
137
|
+
# kernel stops growing.
|
|
138
|
+
def nested_kernels(shifted)
|
|
139
|
+
out = []
|
|
140
|
+
power = shifted
|
|
141
|
+
loop do
|
|
142
|
+
basis = power.kernel.map { |v| v.entries.map { |e| Expression.lift(e).simplify } }
|
|
143
|
+
break if !out.empty? && basis.size <= out.last.size
|
|
144
|
+
out << basis
|
|
145
|
+
break if basis.size >= shifted.rows
|
|
146
|
+
power = (power * shifted).simplify
|
|
147
|
+
end
|
|
148
|
+
out
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Vectors of ker(N**k) that are not already in ker(N**(k-1)).
|
|
152
|
+
def candidates(kernels, k)
|
|
153
|
+
return [] if kernels[k - 1].nil?
|
|
154
|
+
lower = k >= 2 ? kernels[k - 2] : []
|
|
155
|
+
kernels[k - 1].reject { |v| in_span?(lower, v) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_chain(shifted, top, length)
|
|
159
|
+
chain = [top]
|
|
160
|
+
(length - 1).times do
|
|
161
|
+
previous = apply(shifted, chain.first)
|
|
162
|
+
return nil if previous.all? { |e| Scalar.zero?(e) }
|
|
163
|
+
chain.unshift(previous)
|
|
164
|
+
end
|
|
165
|
+
chain
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def apply(matrix, vector)
|
|
169
|
+
(0...matrix.rows).map do |i|
|
|
170
|
+
(0...matrix.cols).reduce(Num.new(0)) { |acc, j| Scalar.add(acc, Scalar.mul(matrix[i, j], vector[j])) }.simplify
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def in_span?(vectors, v)
|
|
175
|
+
return false if vectors.empty?
|
|
176
|
+
rank_of(vectors) == rank_of(vectors + [v])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def independent?(vectors, _matrix) = rank_of(vectors) == vectors.size
|
|
180
|
+
|
|
181
|
+
def rank_of(vectors)
|
|
182
|
+
return 0 if vectors.empty?
|
|
183
|
+
Elimination.rref(vectors.map { |v| v.map { |e| Expression.lift(e) } }).last.size
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ---- building matrices ---------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def square!(matrix, name)
|
|
189
|
+
raise ArgumentError, "#{name}: the matrix must be square" unless matrix.square?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def identity_rows(n) = Array.new(n) { |i| Array.new(n) { |j| Num.new(i == j ? 1 : 0) } }
|
|
193
|
+
|
|
194
|
+
def field_matrix(matrix, rows, r = nil, c = nil)
|
|
195
|
+
space = MatrixSpace.new(matrix.base.fraction_field, r || rows.size, c || rows.first.size)
|
|
196
|
+
space.unchecked(rows)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The vectors as the columns of one matrix.
|
|
200
|
+
def basis_matrix(matrix, vectors) = field_matrix(matrix, vectors.map { |v| v.map { |e| Expression.lift(e).simplify } }.transpose)
|
|
201
|
+
|
|
202
|
+
def diagonal(matrix, values)
|
|
203
|
+
n = values.size
|
|
204
|
+
field_matrix(matrix, Array.new(n) { |i| Array.new(n) { |j| i == j ? Expression.lift(values[i]) : Num.new(0) } })
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# One Jordan block per [eigenvalue, size], on the diagonal.
|
|
208
|
+
def jordan_matrix(matrix, blocks)
|
|
209
|
+
n = blocks.sum(&:last)
|
|
210
|
+
rows = Array.new(n) { Array.new(n, Num.new(0)) }
|
|
211
|
+
offset = 0
|
|
212
|
+
blocks.each do |value, size|
|
|
213
|
+
(0...size).each do |i|
|
|
214
|
+
rows[offset + i][offset + i] = Expression.lift(value)
|
|
215
|
+
rows[offset + i][offset + i + 1] = Num.new(1) if i + 1 < size
|
|
216
|
+
end
|
|
217
|
+
offset += size
|
|
218
|
+
end
|
|
219
|
+
field_matrix(matrix, rows)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def nonpositive?(entry)
|
|
223
|
+
return true if Scalar.zero?(entry)
|
|
224
|
+
value = Expression.lift(entry).evalf
|
|
225
|
+
value.is_a?(Numeric) && value.real? && value.negative?
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
class Matrix
|
|
230
|
+
# P*A = L*U: l, u, p = a.lu
|
|
231
|
+
def lu = Decompositions.lu(self)
|
|
232
|
+
# A = Q*R with Q orthogonal: q, r = a.qr
|
|
233
|
+
def qr = Decompositions.qr(self)
|
|
234
|
+
# A = L*L' for a symmetric positive definite matrix
|
|
235
|
+
def cholesky = Decompositions.cholesky(self)
|
|
236
|
+
# A = P*D*P**-1 with D diagonal: p, d = a.diagonalize
|
|
237
|
+
def diagonalize = Decompositions.diagonalize(self)
|
|
238
|
+
# A = P*J*P**-1 with J in Jordan normal form: p, j = a.jordan
|
|
239
|
+
def jordan = Decompositions.jordan(self)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Symbolic differentiation. Produces an unsimplified tree; Expression#diff
|
|
5
|
+
# simplifies the result.
|
|
6
|
+
module Differentiate
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def diff(expr, var)
|
|
10
|
+
# an expression without var (free) is a constant in it: sqrt(y - y)
|
|
11
|
+
# has derivative 0, and the chain rule divided by sqrt(0) on the way
|
|
12
|
+
# (third review, C11)
|
|
13
|
+
return Num.new(0) if expr.is_a?(Expression) && !expr.is_a?(Derivative) && !expr.variables.include?(var.name) &&
|
|
14
|
+
!(expr.is_a?(Const) && expr.name == :undefined)
|
|
15
|
+
case expr
|
|
16
|
+
when Const then expr.name == :undefined ? expr : Num.new(0)
|
|
17
|
+
when Num, RootOf then Num.new(0)
|
|
18
|
+
when Var then Num.new(expr == var ? 1 : 0)
|
|
19
|
+
when Neg then Neg.new(diff(expr.arg, var))
|
|
20
|
+
when Add then Add.new(diff(expr.left, var), diff(expr.right, var))
|
|
21
|
+
when Sub then Sub.new(diff(expr.left, var), diff(expr.right, var))
|
|
22
|
+
when Mul then Add.new(Mul.new(diff(expr.left, var), expr.right), Mul.new(expr.left, diff(expr.right, var)))
|
|
23
|
+
when Div then quotient(expr, var)
|
|
24
|
+
when Pow then power(expr, var)
|
|
25
|
+
when Fn
|
|
26
|
+
if %i[re im conj].include?(expr.name) then Fn.new(expr.name, [diff(expr.args.first, var)])
|
|
27
|
+
elsif expr.name == :surd && expr.args.size == 2 then surd(expr, var)
|
|
28
|
+
else chain(expr, var)
|
|
29
|
+
end
|
|
30
|
+
when Integral then integral(expr, var)
|
|
31
|
+
when Derivative then derivative(expr, var)
|
|
32
|
+
when Piecewise then Piecewise.new(expr.branches.map { |cond, value| [cond, diff(value, var)] })
|
|
33
|
+
else raise ArgumentError, "can't differentiate #{expr.class}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A derivative with respect to another variable. D(y, x) of an unknown
|
|
38
|
+
# function y stands for y(x), which depends on x alone, so its
|
|
39
|
+
# derivative in anything else is 0. An explicit expression is another
|
|
40
|
+
# matter: D(x*y, x) is y, and its y-derivative is 1, not the 0 this
|
|
41
|
+
# returned for every other variable (a review, 23 Sept 2026). It is
|
|
42
|
+
# taken first and differentiated again; one that cannot be taken stays
|
|
43
|
+
# a formal mixed derivative rather than becoming 0.
|
|
44
|
+
def derivative(d, var)
|
|
45
|
+
return Derivative.new(d.expr, d.var, d.order + 1) if d.var == var
|
|
46
|
+
return Num.new(0) if d.expr.is_a?(Var) || !d.expr.variables.include?(var.name)
|
|
47
|
+
taken = begin
|
|
48
|
+
d.evaluate
|
|
49
|
+
rescue ArgumentError => e
|
|
50
|
+
raise unless e.message.start_with?(Integrate::NO_DERIVATIVE) # floor(x*y)
|
|
51
|
+
d
|
|
52
|
+
end
|
|
53
|
+
return diff(taken, var) unless taken.each_node.any? { |n| n.is_a?(Derivative) }
|
|
54
|
+
Derivative.new(d, var)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A definite integral is a number, but a number that still depends on
|
|
58
|
+
# the parameters of its integrand: d/dx integral(x*t, t, 0, 1) is 1/2,
|
|
59
|
+
# not 0. Only the *integration* variable is bound, and looking at the
|
|
60
|
+
# bounds alone answered every such derivative with zero (22 Sept 2026,
|
|
61
|
+
# from a review). Bounds that move add Leibniz's two boundary terms.
|
|
62
|
+
#
|
|
63
|
+
# The rule is applied, not checked: differentiating under the integral
|
|
64
|
+
# sign needs the integrand and its parameter derivative continuous on
|
|
65
|
+
# the rectangle (and, over an infinite range, the differentiated
|
|
66
|
+
# integral uniformly convergent) - [DLMF, 1.5(iv)]. rcas does not verify
|
|
67
|
+
# that, which is why the answer is another integral rather than a value:
|
|
68
|
+
# what comes out is the derivative wherever the rule applies, and the
|
|
69
|
+
# caller keeps the hypotheses.
|
|
70
|
+
def integral(expr, var)
|
|
71
|
+
unless expr.definite?
|
|
72
|
+
return expr.var == var ? expr.integrand : Integral.new(differentiated(expr, var), expr.var)
|
|
73
|
+
end
|
|
74
|
+
moving = [expr.from, expr.to].any? { |c| c.variables.include?(var.name) }
|
|
75
|
+
# A name bound by the integral is not the free one outside it, even
|
|
76
|
+
# where the two are spelled alike: integral(sin(x), x, 0, x) is the
|
|
77
|
+
# integral(sin(t), t, 0, x) that renaming the bound variable gives, so
|
|
78
|
+
# the integrand contributes nothing and only the bound moves. Refusing
|
|
79
|
+
# it as ambiguous was wrong (22 Sept 2026, the second review).
|
|
80
|
+
bound = expr.var == var
|
|
81
|
+
inside = !bound && expr.integrand.variables.include?(var.name) ? Integral.new(differentiated(expr, var), expr.var, expr.from, expr.to) : Num.new(0)
|
|
82
|
+
return inside unless moving
|
|
83
|
+
at = ->(edge) { Mul.new(expr.integrand.subs(expr.var => edge), diff(edge, var)) }
|
|
84
|
+
Add.new(inside, Sub.new(at[expr.to], at[expr.from]))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The new integrand is simplified on the way in: an Integral is an atom
|
|
88
|
+
# to Simplify, so nothing would tidy it afterwards.
|
|
89
|
+
def differentiated(expr, var) = diff(expr.integrand, var).simplify
|
|
90
|
+
|
|
91
|
+
def quotient(expr, var)
|
|
92
|
+
u, v = expr.left, expr.right
|
|
93
|
+
Div.new(Sub.new(Mul.new(diff(u, var), v), Mul.new(u, diff(v, var))), Pow.new(v, Num.new(2)))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def power(expr, var)
|
|
97
|
+
u, n = expr.base, expr.exponent
|
|
98
|
+
if !n.variables.include?(var.name)
|
|
99
|
+
# d(u**n) = n * u**(n-1) * u' - and 0 when u does not move: sqrt(x - x)
|
|
100
|
+
# is the zero function, and 0**(-1/2)*0 divided by zero (fourth review)
|
|
101
|
+
du = diff(u, var)
|
|
102
|
+
flat = du.is_a?(Num) ? du : du.simplify
|
|
103
|
+
return Num.new(0) if flat.is_a?(Num) && flat.value.is_a?(Numeric) && flat.value.zero?
|
|
104
|
+
Mul.new(Mul.new(n, Pow.new(u, Sub.new(n, Num.new(1)))), du)
|
|
105
|
+
elsif !u.variables.include?(var.name)
|
|
106
|
+
# d(a**v) = a**v * log(a) * v'
|
|
107
|
+
Mul.new(Mul.new(expr, Fn.new(:log, [u])), diff(n, var))
|
|
108
|
+
else
|
|
109
|
+
# general case: u**v * (v' * log(u) + v * u' / u)
|
|
110
|
+
Mul.new(expr, Add.new(Mul.new(diff(n, var), Fn.new(:log, [u])), Div.new(Mul.new(n, diff(u, var)), u)))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# surd(u, n)**n = u, so surd(u, n)' = u'/(n*surd(u, n)**(n - 1)) where u != 0
|
|
115
|
+
def surd(expr, var)
|
|
116
|
+
u, n = expr.args
|
|
117
|
+
Div.new(diff(u, var), Mul.new(n, Pow.new(expr, Sub.new(n, Num.new(1)))))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def chain(expr, var)
|
|
121
|
+
raise ArgumentError, "#{expr.name} takes one argument" unless expr.args.size == 1
|
|
122
|
+
u = expr.args.first
|
|
123
|
+
outer =
|
|
124
|
+
case expr.name
|
|
125
|
+
when :sin then Fn.new(:cos, [u])
|
|
126
|
+
when :cos then Neg.new(Fn.new(:sin, [u]))
|
|
127
|
+
when :tan then Add.new(Num.new(1), Pow.new(Fn.new(:tan, [u]), Num.new(2)))
|
|
128
|
+
when :exp then expr
|
|
129
|
+
when :log then Div.new(Num.new(1), u)
|
|
130
|
+
when :atan then Div.new(Num.new(1), Add.new(Num.new(1), Pow.new(u, Num.new(2))))
|
|
131
|
+
when :abs then Fn.new(:sign, [u])
|
|
132
|
+
when :sign then Num.new(0)
|
|
133
|
+
when :asin then Pow.new(Sub.new(Num.new(1), Pow.new(u, Num.new(2))), Num.new(Rational(-1, 2)))
|
|
134
|
+
when :acos then Neg.new(Pow.new(Sub.new(Num.new(1), Pow.new(u, Num.new(2))), Num.new(Rational(-1, 2))))
|
|
135
|
+
when :sinh then Fn.new(:cosh, [u])
|
|
136
|
+
when :cosh then Fn.new(:sinh, [u])
|
|
137
|
+
when :erf then Div.new(Mul.new(Num.new(2), Fn.new(:exp, [Neg.new(Pow.new(u, Num.new(2)))])), Pow.new(PI, Num.new(Rational(1, 2))))
|
|
138
|
+
when :erfc then Neg.new(Div.new(Mul.new(Num.new(2), Fn.new(:exp, [Neg.new(Pow.new(u, Num.new(2)))])), Pow.new(PI, Num.new(Rational(1, 2)))))
|
|
139
|
+
else IntegralFunctions.derivative(expr.name, u) || raise(ArgumentError, "don't know the derivative of #{expr.name}")
|
|
140
|
+
end
|
|
141
|
+
Mul.new(outer, diff(u, var))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|