rcas 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
@@ -0,0 +1,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