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,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