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,1136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # A bare name that may become an indeterminate: a Ruby local/method name
5
+ # that does not start with an uppercase ASCII letter. Ruby treats every
6
+ # non-ASCII character as an identifier character, so α, β₁ and ∞ qualify.
7
+ IDENTIFIER = /\A(?:[a-z_]|[^\x00-\x7F])(?:[a-zA-Z0-9_]|[^\x00-\x7F])*\z/
8
+
9
+ # Elementary functions. Available as RCAS.sin(:x) or, after
10
+ # `include RCAS::Functions`, as bare sin(:x).
11
+ module Functions
12
+ NAMES = %i[sin cos tan exp log atan asin acos sinh cosh zeta factorial gamma abs sign erf erfc Ei Si Ci li].freeze
13
+
14
+ # Symbolic arguments build an Fn node; constant arguments fold right
15
+ # away, the way Ruby folds 1 + 2: sin(PI/6) is 1/2, sin(x) stays sin(x).
16
+ NAMES.each do |name|
17
+ define_method(name) do |arg|
18
+ fn = Fn.new(name, [arg])
19
+ fn.args.first.constant? ? Functions.fold(fn) : fn
20
+ end
21
+ end
22
+
23
+ # sqrt(8) is 2*sqrt(2); sqrt(-4) is 2*i; sqrt(x) stays sqrt(x)
24
+ def sqrt(arg)
25
+ root = Expression.lift(arg)**Rational(1, 2)
26
+ root.constant? ? root.simplify : root
27
+ end
28
+
29
+ # pi and oo (also π and ∞): the constants PI and OO under bare names
30
+ def pi = PI
31
+ def oo = OO
32
+ def π = PI
33
+ def ∞ = OO
34
+
35
+ # undefined: what 0*oo, oo - oo and oo/oo come back as, under the name it prints
36
+ def undefined = UNDEFINED
37
+
38
+ # root(2, 3) is the exact cube root; Ruby would turn 2**(1/3r) into a float.
39
+ def root(x, n)
40
+ r = Expression.lift(x)**Rational(1, n)
41
+ r.constant? ? r.simplify : r
42
+ end
43
+
44
+ # surd(-8, 3) is -2: the real n-th root (for odd n, -|x|**(1/n) below 0),
45
+ # where x**(1/n) and root(x, n) are the principal one, 1 + i*sqrt(3) at -8
46
+ def surd(x, n) = Functions.fold(Fn.new(:surd, [Expression.lift(x), Expression.lift(n)]))
47
+
48
+ # cbrt(-8) is -2, the real cube root: surd(x, 3); cbrt(2) stays 2**(1/3)
49
+ def cbrt(x) = surd(x, 3)
50
+
51
+ # binomial(5, 2) is 10; binomial(n, 2) stays symbolic (expand it with expand)
52
+ def binomial(n, k) = Functions.fold(Fn.new(:binomial, [n, k]))
53
+
54
+ # GF(7), GF(8), GF(9, :b), GF(3, 4): finite fields
55
+ def GF(q, gen_or_n = :a, n = nil)
56
+ gen_or_n.is_a?(Integer) ? FiniteField.of(q, :a, gen_or_n) : FiniteField.of(q, gen_or_n, n)
57
+ end
58
+
59
+ # series(sin(x), x, 0, 6) or series(sin(x), x: 0, n: 6); formal: true for the general coefficient
60
+ def series(f, x = nil, a = 0, n = 6, **opts)
61
+ formal = opts.delete(:formal)
62
+ x, a, n = Functions.point_arguments(x, a, n, opts, "series")
63
+ formal ? Functions.formal_series(f, x, a) : Limits.series(f, x, a, n)
64
+ end
65
+
66
+ # fps(exp(x), x): the formal power series, sum(x**k/k!, k, 0, oo), coefficient and all
67
+ def fps(f, x = nil, a = 0, **opts)
68
+ x, a, = Functions.point_arguments(x, a, nil, opts, "fps")
69
+ Functions.formal_series(f, x, a)
70
+ end
71
+
72
+ # fourier(x, x: -pi..pi, n: 4): the Fourier partial sum; formal: true gives
73
+ # the general coefficient, kind: :sine or :cosine the half-range expansion
74
+ def fourier(f, x = nil, from = nil, to = nil, **opts)
75
+ n = opts.delete(:n) || Fourier::DEFAULT_TERMS
76
+ kind = opts.delete(:kind) || :full
77
+ formal = opts.delete(:formal) || false
78
+ index = opts.delete(:k)
79
+ x, from, to = Functions.range_arguments(x, from, to, opts, "fourier", discrete: false)
80
+ Fourier.series(f, x, from, to, n: n, kind: kind, formal: formal, index: index)
81
+ end
82
+
83
+ # taylor(exp(x), x, 0, 5): the series without the O term
84
+ def taylor(f, x = nil, a = 0, n = 6, **opts)
85
+ x, a, n = Functions.point_arguments(x, a, n, opts, "taylor")
86
+ Limits.taylor(f, x, a, n)
87
+ end
88
+
89
+ # limit(sin(x)/x, x, 0) or limit(sin(x)/x, x: 0, dir: :right); oo for infinity
90
+ def limit(f, x = nil, a = nil, dir = nil, **opts)
91
+ dir = opts.delete(:dir) || dir
92
+ x, a, = Functions.point_arguments(x, a, nil, opts, "limit")
93
+ Limits.limit(f, x, a, dir)
94
+ end
95
+
96
+ # product(k, k, 1, n) or product(k, k: 1..n): n!; closed forms through factorials and gamma
97
+ def product(f, k = nil, from = nil, to = nil, **range)
98
+ k, from, to = Functions.range_arguments(k, from, to, range, "product", discrete: true)
99
+ Products.product(f, k, from, to)
100
+ end
101
+
102
+ # sum(k**2, k, 1, n) or sum(k**2, k: 1..n); an endless range means infinity
103
+ def sum(f, k = nil, from = nil, to = nil, **range)
104
+ k, from, to = Functions.range_arguments(k, from, to, range, "sum", discrete: true)
105
+ Summation.sum(f, k, from, to)
106
+ end
107
+
108
+ # The formal power series, or the reason there is none: the truncated
109
+ # expansion stays available, and saying so is more use than a bare nil.
110
+ def self.formal_series(f, x, a)
111
+ FPS.expansion(f, x, a) ||
112
+ raise(SeriesError, "no formal power series for #{Expression.lift(f)}: " \
113
+ "its coefficients are not hypergeometric, or the equation for it is too long. " \
114
+ "series(f, #{x}) gives the expansion up to an order.")
115
+ end
116
+
117
+ def self.point_arguments(x, a, n, opts, name)
118
+ n = opts.delete(:n) || n
119
+ unless opts.empty?
120
+ raise ArgumentError, "#{name}: give one variable, e.g. #{name}(f, x: 0)" unless opts.size == 1 && x.nil?
121
+ x, a = opts.first
122
+ end
123
+ raise ArgumentError, "#{name}: which variable?" if x.nil?
124
+ [x, infinity(a), n]
125
+ end
126
+
127
+ # boxplot("a" => xs, width: 40): Ruby hands both over as keywords, so the
128
+ # named series and the plot options have to be told apart here.
129
+ PLOT_OPTIONS = %i[title label labels x y bins density fit width height].freeze
130
+
131
+ def self.split_plot_options(opts)
132
+ [opts.select { |k, _| PLOT_OPTIONS.include?(k) }, opts.reject { |k, _| PLOT_OPTIONS.include?(k) }]
133
+ end
134
+
135
+ # One definite integral per range, innermost first.
136
+ def self.iterated_integral(expr, ranges)
137
+ ranges.reduce(Expression.lift(expr)) do |acc, (name, r)|
138
+ var, from, to = range_arguments(nil, nil, nil, { name => r }, "integrate", discrete: false)
139
+ Integrate.definite(acc, Expression.lift(var), from, to)
140
+ end
141
+ end
142
+
143
+ # The ranges of an integral over a region, in the order they were given,
144
+ # which is the order they are integrated in: the first one innermost.
145
+ def self.range_list(ranges, count, name)
146
+ wanted = count.is_a?(Range) ? count : (count..count)
147
+ unless wanted.cover?(ranges.size)
148
+ example = name == "green" ? "#{name}(f, x: 0..1, y: 0..1)" : "#{name}(f, s, u: 0..1, v: 0..1)"
149
+ raise ArgumentError, "#{name}: give #{wanted.to_a.join(' or ')} ranges, e.g. #{example}"
150
+ end
151
+ ranges.map { |k, r| range_arguments(nil, nil, nil, { k => r }, name, discrete: false) }
152
+ end
153
+
154
+ def self.range_arguments(k, from, to, range, name, discrete:)
155
+ unless range.empty?
156
+ raise ArgumentError, "#{name}: give one variable, e.g. #{name}(f, k: 1..n)" unless range.size == 1 && k.nil?
157
+ k, r = range.first
158
+ raise ArgumentError, "#{name}: expected a range, got #{r.inspect}" unless r.is_a?(Range)
159
+ raise ArgumentError, "#{name}: the range needs a start" if r.begin.nil?
160
+ from = r.begin
161
+ to = r.end
162
+ to = Expression.lift(to) - 1 if discrete && r.exclude_end? && !to.nil?
163
+ end
164
+ raise ArgumentError, "#{name}: which variable?" if k.nil?
165
+ [k, infinity(from), infinity(to.nil? ? OO : to)]
166
+ end
167
+
168
+ def self.infinity(v)
169
+ return v unless v.is_a?(Float) && v.infinite?
170
+ v.positive? ? OO : Neg.new(OO)
171
+ end
172
+
173
+ # factor(x**2 - 1), factor(360), factor(f, extension: sqrt(2)); recombination: :van_hoeij, :zassenhaus or :auto
174
+ def factor(obj, extension: nil, recombination: nil)
175
+ value = obj.is_a?(Num) ? obj.value : obj
176
+ return NumberTheory.factor(value) if value.is_a?(Integer) || value.is_a?(Rational)
177
+ Factor.with_recombination(recombination) do
178
+ obj.is_a?(Polynomial) ? obj.factor(extension: extension) : Expression.lift(obj).factor(extension: extension)
179
+ end
180
+ end
181
+ # minpoly(sqrt(2) + 1, x): the minimal polynomial of an algebraic number
182
+ def minpoly(expr, var = :x) = Expression.lift(expr).minpoly(var)
183
+
184
+ # diff(f, x) or diff(f, x, 2): derivatives
185
+ def diff(f, x, n = 1) = Expression.lift(f).diff(x, n)
186
+ # subs(f, x => 2), subs(f, x: 2) or subs(f, x**2, z): substitution
187
+ def subs(f, pattern, replacement = nil) = Expression.lift(f).subs(pattern, replacement)
188
+ # evalf(pi), evalf(sqrt(2)*x, x: 3): the numeric value as a Float;
189
+ # evalf(pi, 50) or evalf(pi, digits: 50) for as many digits as you like
190
+ def evalf(f, digits = nil, **bindings) = Expression.lift(f).evalf(digits, **bindings)
191
+
192
+ # congruence(3*x - 4, x, 7) solves modulo 7; legendre(a, p), jacobi(a, n), order(a, m),
193
+ # primitive_root(m), continued_fraction(x, n), convergents(x, n)
194
+ def congruence(f, x, m) = NumberTheory.congruence(f, x, m)
195
+ def legendre(a, p) = NumberTheory.legendre(a, p)
196
+ def jacobi(a, n) = NumberTheory.jacobi(a, n)
197
+ def order(a, m) = NumberTheory.order(a, m)
198
+ def primitive_root(m) = NumberTheory.primitive_root(m)
199
+ def continued_fraction(x, terms = 10) = NumberTheory.continued_fraction(x, terms)
200
+ def convergents(x, terms = 10) = NumberTheory.convergents(x, terms)
201
+
202
+ # laplace(exp(3*t), t, s) and inverse_laplace(1/(s - 3), s, t): the transform
203
+ # that turns differentiation into multiplication by s
204
+ def laplace(f, t = :t, s = :s) = Laplace.transform(f, t, s)
205
+ def inverse_laplace(f, s = :s, t = :t) = Laplace.inverse(f, s, t)
206
+
207
+ # gram_schmidt(vectors, normalize: false), least_squares(A, b), project(v, onto: u),
208
+ # orthogonal?(u, v): orthogonality and the normal equations
209
+ def gram_schmidt(vectors, normalize: false) = LinearAlgebra.gram_schmidt(vectors, normalize: normalize)
210
+ def least_squares(matrix, target) = LinearAlgebra.least_squares(matrix, target)
211
+ def project(v, onto:) = LinearAlgebra.project(v, onto: onto)
212
+ def orthogonal?(u, v) = LinearAlgebra.orthogonal?(u, v)
213
+
214
+ # lll(basis, delta: 3/4r, transform: false): the LLL-reduced basis of a lattice (rows of a matrix, or a list of vectors)
215
+ def lll(basis, delta: Lattice::DELTA, transform: false) = Lattice.lll(basis, delta: delta, transform: transform)
216
+
217
+ # maximize(f, [constraints], vars = nil, nonnegative: false, integer: nil): linear optimization by the simplex method, exact; integer: true or [x, y] for whole numbers
218
+ def maximize(objective, constraints, vars = nil, nonnegative: false, integer: nil)
219
+ LinearProgram.maximize(objective, constraints, vars, nonnegative: nonnegative, integer: integer)
220
+ end
221
+
222
+ # minimize(f, [constraints], vars = nil, nonnegative: false, integer: nil): the smallest value of a linear f under linear constraints
223
+ def minimize(objective, constraints, vars = nil, nonnegative: false, integer: nil)
224
+ LinearProgram.minimize(objective, constraints, vars, nonnegative: nonnegative, integer: integer)
225
+ end
226
+
227
+ # lu(a) gives [l, u, p] with p*a = l*u; qr(a) gives [q, r] with a = q*r
228
+ def lu(matrix) = Decompositions.lu(matrix)
229
+ def qr(matrix) = Decompositions.qr(matrix)
230
+ # cholesky(a) is the l with a = l*l.transpose, for a symmetric positive definite a
231
+ def cholesky(matrix) = Decompositions.cholesky(matrix)
232
+ # diagonalize(a) gives [p, d] and jordan(a) gives [p, j] with a = p*d*p**-1
233
+ def diagonalize(matrix) = Decompositions.diagonalize(matrix)
234
+ def jordan(matrix) = Decompositions.jordan(matrix)
235
+
236
+ # point(0, 0), line(p, q) or line(p, slope: 2), circle(centre, r): plane geometry
237
+ def point(x, y = nil) = Geometry.point(x, y)
238
+ def line(first, second = nil, slope: nil) = Geometry.line(first, second, slope: slope)
239
+ def circle(centre, radius) = Geometry.circle(centre, radius)
240
+ # distance(a, b) between points, a point and a line or parallel lines; midpoint, angle(a, b, c),
241
+ # area(a, b, c), perimeter, collinear?, centroid, intersect(a, b), circumcircle(a, b, c),
242
+ # perpendicular_bisector(p, q), parallel_through(l, p), perpendicular_through(l, p)
243
+ def distance(a, b) = Geometry.distance(a, b)
244
+ def midpoint(p, q) = Geometry.midpoint(p, q)
245
+ def angle(a, b, c = nil) = b.is_a?(Geometry::Line) ? Geometry.line_angle(a, b) : Geometry.angle(a, b, c)
246
+ def area(a, b = nil, c = nil) = Geometry.area(a, b, c)
247
+ def perimeter(a, b = nil, c = nil) = Geometry.perimeter(a, b, c)
248
+ def collinear?(a, b, c) = Geometry.collinear?(a, b, c)
249
+ def centroid(*points) = Geometry.centroid(*points)
250
+ def intersect(a, b) = Geometry.intersect(a, b)
251
+ def circumcircle(a, b, c) = Geometry.circumcircle(a, b, c)
252
+ def perpendicular_bisector(p, q) = Geometry.perpendicular_bisector(p, q)
253
+ def parallel_through(l, p) = Geometry.parallel_through(l, p)
254
+ def perpendicular_through(l, p) = Geometry.perpendicular_through(l, p)
255
+
256
+ # critical_points(f, x), extrema(f, x) => [[x, f(x), :minimum|:maximum|:saddle], ...],
257
+ # inflections(f, x), asymptotes(f, x), tangent(f, x, a), normal(f, x, a), real_domain(f, x)
258
+ def critical_points(f, var = nil) = Analysis.critical_points(f, var)
259
+ def extrema(f, var = nil) = Analysis.extrema(f, var)
260
+ def inflections(f, var = nil) = Analysis.inflections(f, var)
261
+ def asymptotes(f, var = nil, at: nil) = Analysis.asymptotes(f, var, at: at)
262
+ def tangent(f, var = nil, at = nil) = Analysis.tangent(f, var, at)
263
+ def normal(f, var = nil, at = nil) = Analysis.normal(f, var, at)
264
+ def real_domain(f, var = nil) = Analysis.real_domain(f, var)
265
+
266
+ # discuss(f, x): the whole curve discussion - domain, symmetry, zeros,
267
+ # gaps, behaviour at infinity, extrema, monotonicity, inflections, curvature
268
+ def discuss(f, var = nil) = Discussion.discuss(f, var)
269
+
270
+ # gradient(f, [x, y]), hessian(f, vars), jacobian([f, g], vars), divergence(field, vars),
271
+ # curl(field, [x, y, z]), laplacian(f, vars), lagrange(f, [g], vars): several variables
272
+ def gradient(f, vars = nil) = Analysis.gradient(f, vars)
273
+ def hessian(f, vars = nil) = Analysis.hessian(f, vars)
274
+ def jacobian(fs, vars = nil) = Analysis.jacobian(fs, vars)
275
+ def divergence(field, vars = nil) = Analysis.divergence(field, vars)
276
+ def curl(field, vars = nil) = Analysis.curl(field, vars)
277
+ def laplacian(f, vars = nil) = Analysis.laplacian(f, vars)
278
+ def lagrange(f, constraints, vars = nil) = Analysis.lagrange(f, constraints, vars)
279
+
280
+ # arclength(x**2, x: 0..1) or arclength([cos(t), sin(t)], t: 0..pi): the length of a curve
281
+ def arclength(f, var = nil, from = nil, to = nil, **range)
282
+ var, from, to = Functions.range_arguments(var, from, to, range, "arclength", discrete: false)
283
+ Analysis.arclength(f, var, from, to)
284
+ end
285
+
286
+ # revolution_volume(sqrt(x), x: 0..1): the volume swept out around the x-axis (axis: :y for the other)
287
+ def revolution_volume(f, var = nil, from = nil, to = nil, axis: :x, **range)
288
+ var, from, to = Functions.range_arguments(var, from, to, range, "revolution_volume", discrete: false)
289
+ Analysis.revolution_volume(f, var, from, to, axis: axis)
290
+ end
291
+
292
+ # revolution_surface(sqrt(x), x: 0..1): the area of that surface of revolution
293
+ def revolution_surface(f, var = nil, from = nil, to = nil, axis: :x, **range)
294
+ var, from, to = Functions.range_arguments(var, from, to, range, "revolution_surface", discrete: false)
295
+ Analysis.revolution_surface(f, var, from, to, axis: axis)
296
+ end
297
+
298
+ # line_integral(x*y, [cos(t), sin(t)], t: 0..pi/2): a scalar field along a curve,
299
+ # the integral of f ds; with a vector field, line_integral([-y, x], curve, t: 0..2*pi)
300
+ # is the integral of F.dr, the work done along it. The field is read in x, y, z
301
+ # unless vars: names other coordinates.
302
+ def line_integral(f, curve, var = nil, from = nil, to = nil, vars: nil, **range)
303
+ var, from, to = Functions.range_arguments(var, from, to, range, "line_integral", discrete: false)
304
+ VectorCalculus.line_integral(f, curve, var, from, to, vars: vars)
305
+ end
306
+
307
+ # surface_integral(1, [u, v, u + v], u: 0..1, v: 0..1): a scalar field over a
308
+ # parametrized surface, the integral of f dS; a vector field is integrated
309
+ # against the normal, F.dS, which is the flux through it
310
+ def surface_integral(f, surface, vars: nil, **ranges)
311
+ VectorCalculus.surface_integral(f, surface, Functions.range_list(ranges, 2, "surface_integral"), vars: vars)
312
+ end
313
+
314
+ # flux([x, y], [cos(t), sin(t)], t: 0..2*pi) across a plane curve (outwards when
315
+ # it runs anticlockwise), or flux(field, surface, u: .., v: ..) through a surface
316
+ def flux(field, boundary, vars: nil, **ranges)
317
+ list = Functions.range_list(ranges, (1..2), "flux")
318
+ return VectorCalculus.surface_integral(field, boundary, list, vars: vars) if list.size == 2
319
+ VectorCalculus.curve_flux(field, boundary, *list.first, vars: vars)
320
+ end
321
+
322
+ # enclosed_area([cos(t)**3, sin(t)**3], t: 0..2*pi): the area a closed plane curve
323
+ # encloses, as the line integral Green's theorem turns it into
324
+ def enclosed_area(curve, var = nil, from = nil, to = nil, **range)
325
+ var, from, to = Functions.range_arguments(var, from, to, range, "enclosed_area", discrete: false)
326
+ VectorCalculus.enclosed_area(curve, var, from, to)
327
+ end
328
+
329
+ # green([-y, x], x: 0..1, y: 0..1): Green's theorem, the circulation of a plane
330
+ # field around the boundary of a region as the double integral of Q_x - P_y over
331
+ # it. The ranges describe the region, innermost first, as for integrate.
332
+ def green(field, vars: nil, **ranges)
333
+ VectorCalculus.green(field, Functions.range_list(ranges, 2, "green"), vars: vars)
334
+ end
335
+
336
+ # stokes([-y, x, 0], [u*cos(v), u*sin(v), 0], u: 0..1, v: 0..2*pi): Stokes's theorem,
337
+ # the circulation around the edge of a surface as the flux of the curl through it
338
+ def stokes(field, surface, vars: nil, **ranges)
339
+ VectorCalculus.stokes(field, surface, Functions.range_list(ranges, 2, "stokes"), vars: vars)
340
+ end
341
+
342
+ # divergence_theorem([x, y, z], x: 0..1, y: 0..1, z: 0..1): Gauss's theorem, the flux
343
+ # out of the boundary of a solid as the triple integral of the divergence over it
344
+ def divergence_theorem(field, vars: nil, **ranges)
345
+ VectorCalculus.divergence_theorem(field, Functions.range_list(ranges, 3, "divergence_theorem"), vars: vars)
346
+ end
347
+
348
+ # conservative?([2*x*y, x**2]) asks whether a field is a gradient; potential(field)
349
+ # is the function it is the gradient of, up to a constant
350
+ def conservative?(field, vars = nil) = VectorCalculus.conservative?(field, vars)
351
+ def potential(field, vars = nil) = VectorCalculus.potential(field, vars)
352
+
353
+ # nsolve(cos(x) - x, x: 0..1) or nsolve(f, x, guess): a root as a Float when no formula applies
354
+ def nsolve(f, var = nil, guess = nil, **range) = Numerics.nsolve(f, var, guess, **range)
355
+ # nintegrate(sin(x)/x, x: 0..1): a definite integral as a Float, infinite bounds included
356
+ def nintegrate(f, var = nil, from = nil, to = nil, **range) = Numerics.nintegrate(f, var, from, to, **range)
357
+
358
+ # integrate(x**2 * exp(x), x); definite: integrate(x**2, x, 0, 1) or integrate(x**2, x: 0..1);
359
+ # iterated: integrate(x*y, x: 0..1, y: 0..2) integrates over x first
360
+ def integrate(expr, var = nil, from = nil, to = nil, generic: false, **range)
361
+ return Functions.iterated_integral(expr, range) if range.size > 1
362
+ var, from, to = Functions.range_arguments(var, from, to, range, "integrate", discrete: false) if var.nil? || from
363
+ from.nil? ? Integrate.with_special_cases(expr, var, generic: generic) : Integrate.definite_with_special_cases(expr, var, from, to, generic: generic)
364
+ end
365
+
366
+ # polynomial structure: degree(f, x), lcoeff(f, x), coeff(f, x, 2), collect(f, x)
367
+ def degree(f, x = nil) = Coefficients.degree(f, x)
368
+ def ldegree(f, x = nil) = Coefficients.ldegree(f, x)
369
+ def lcoeff(f, x = nil) = Coefficients.lcoeff(f, x)
370
+ def tcoeff(f, x = nil) = Coefficients.tcoeff(f, x)
371
+ # coeff(f, x, k) or coeff(f, x**k): the coefficient of x**k
372
+ def coeff(f, x, k = 1) = Coefficients.coeff(f, x, k)
373
+ # coeffs(f, x): coefficients of x**0 .. x**degree; coeffs(f): of every term
374
+ def coeffs(f, x = nil) = Coefficients.coeffs(f, x)
375
+ # collect(f, x): f as a sum of coefficient * x**k
376
+ def collect(f, x) = Coefficients.collect(f, x)
377
+
378
+ # simplify(f), expand(f), cancel(f), rationalize(f): the methods as functions
379
+ def simplify(f) = Expression.lift(f).simplify
380
+ def expand(f) = Expression.lift(f).expand
381
+ def cancel(f) = Expression.lift(f).cancel
382
+ def rationalize(f) = Expression.lift(f).rationalize
383
+
384
+ # resultant(f, g, x), discriminant(f, x): via the Sylvester matrix; other symbols are parameters
385
+ def resultant(f, g, x = nil)
386
+ _, (pf, pg) = Groebner.lift([f, g], x && [x])
387
+ pf.resultant(pg, x && Expression.lift(x).name).to_expr
388
+ end
389
+
390
+ # discriminant(x**2 + b*x + c, x): the discriminant of a polynomial
391
+ def discriminant(f, x = nil)
392
+ pf = Groebner.lift([f], x && [x]).last.first
393
+ pf.discriminant(x && Expression.lift(x).name).to_expr
394
+ end
395
+
396
+ # interpolate([[0, 1], [1, 3], [2, 7]], x): the polynomial through the points (Newton)
397
+ def interpolate(points, x) = Interpolate.newton(points, x)
398
+
399
+ # numer(f), denom(f): numerator and denominator of the normal form
400
+ def numer(f) = RationalFunction.numer(f)
401
+ def denom(f) = RationalFunction.denom(f)
402
+ # apart(f, x): partial fractions over QQ; other indeterminates are parameters
403
+ def apart(f, x = nil) = RationalFunction.apart(f, x)
404
+ # gcd(f, g), lcm(f, g) of integers or polynomials
405
+ def gcd(f, g) = RationalFunction.gcd(f, g)
406
+ def lcm(f, g) = RationalFunction.lcm(f, g)
407
+ # quo(f, g), rem(f, g), divmod(f, g): polynomial division; quo(f, g, x) divides by x with parameters
408
+ def quo(f, g, x = nil) = RationalFunction.quo(f, g, x)
409
+ def rem(f, g, x = nil) = RationalFunction.rem(f, g, x)
410
+ def divmod(f, g, x = nil) = RationalFunction.divmod(f, g, x)
411
+
412
+ # ifactor(360): prime factorization; factor(360) does the same
413
+ def ifactor(n) = NumberTheory.factor(n)
414
+ # isprime(n): Miller-Rabin, exact below 3.3e24
415
+ def isprime(n) = NumberTheory.prime?(n)
416
+ def nextprime(n) = NumberTheory.nextprime(n)
417
+ def prevprime(n) = NumberTheory.prevprime(n)
418
+ # divisors(12) => [1, 2, 3, 4, 6, 12]; totient(n) is Euler's phi
419
+ def divisors(n) = NumberTheory.divisors(n)
420
+ def totient(n) = NumberTheory.totient(n)
421
+ # invmod(3, 7): inverse modulo; chrem([2, 3], [3, 5]): Chinese remainder theorem
422
+ def invmod(a, m) = NumberTheory.invmod(a, m)
423
+ def chrem(residues, moduli) = NumberTheory.chrem(residues, moduli)
424
+
425
+ # trigonometric and logarithmic rewriting
426
+ def trigsimp(expr) = Trigonometry.trigsimp(expr)
427
+ def expand_trig(expr) = Trigonometry.expand_trig(expr)
428
+ # expand_log(log(x**2)) stays; with x > 0 assumed, or force: true, it is 2*log(x)
429
+ def expand_log(expr, force: false) = Trigonometry.expand_log(expr, force: force)
430
+ # logcombine(log(x) + log(y)) stays; with x, y > 0 assumed, or force: true, it is log(x*y)
431
+ def logcombine(expr, force: false) = Trigonometry.logcombine(expr, force: force)
432
+
433
+ # hold { 1 + 2 } keeps the block's source as an unevaluated expression;
434
+ # evaluate(expr) computes the formal integrals, derivatives, sums and limits in it.
435
+ def hold(&block) = Hold.hold(block)
436
+
437
+ # The working, not only the answer: steps { diff(x**2*sin(x), x) },
438
+ # steps(x**2 - 5*x + 6, :solve), steps(m, :rref)
439
+ def steps(*args, &block) = Steps.of(*args, &block)
440
+ def evaluate(expr) = Expression.lift(expr).evaluate
441
+ alias doit evaluate
442
+
443
+ # interval(0, 1) is [0, 1]; interval(0, 1, right_open: true) is [0, 1)
444
+ def interval(low, high, **open) = Interval.new(low, high, **open)
445
+
446
+ # A function defined case by case: piecewise(x < 0 => -x, :else => x**2)
447
+ def piecewise(*branches, **rest) = Piecewises.build(*branches, **rest)
448
+
449
+ # The points where f jumps: discontinuities(piecewise(x < 0 => 0, :else => 1))
450
+ def discontinuities(f, var = nil) = Piecewises.discontinuities(Expression.lift(f), var)
451
+
452
+ # The corners of f: points where it is continuous but the one-sided derivatives differ.
453
+ def kinks(f, var = nil) = Piecewises.kinks(Expression.lift(f), var)
454
+
455
+ # eq(x**2, 4) builds an equation; solve(eq(x**2, 4), x) solves it.
456
+ def eq(lhs, rhs) = Equation.new(lhs, rhs)
457
+ def solve(target, vars = nil, all: true, principal: false, domain: nil) = Solve.solve(target, vars, all: all, principal: principal, domain: domain)
458
+
459
+ # groebner([x**2 + y**2 - 1, x - y], [x, y]): reduced Gröbner basis; order: :lex (default), :grlex, :grevlex
460
+ def groebner(polys, vars = nil, order: :lex) = Groebner.groebner(polys, vars, order: order)
461
+ # reduce(f, basis, [x, y]): normal form of f modulo the basis; 0 exactly when f lies in the ideal
462
+ def reduce(f, basis, vars = nil, order: :lex) = Groebner.normal_form(f, basis, vars, order: order)
463
+
464
+ # rsolve(eq(u(n + 2), u(n + 1) + u(n)), u, n, init: {0 => 0, 1 => 1}): linear recurrences with constant coefficients
465
+ def rsolve(equation, u, n, init: {}) = Recurrence.rsolve(equation, u, n, init: init)
466
+
467
+ # re(z), im(z), conj(z), arg(z): real part, imaginary part, conjugate, argument (variables count as real once assumed so)
468
+ def re(z) = ComplexParts.re(z)
469
+ def im(z) = ComplexParts.im(z)
470
+ def conj(z) = ComplexParts.conj(z)
471
+ def arg(z) = ComplexParts.arg(z)
472
+
473
+ # floor(7/2r), ceil(x), round(x): rounding; mod(a, m): a modulo m. Symbolic arguments stay unevaluated.
474
+ def floor(x) = Functions.fold(Fn.new(:floor, [x]))
475
+ def ceil(x) = Functions.fold(Fn.new(:ceil, [x]))
476
+ def round(x) = Functions.fold(Fn.new(:round, [x]))
477
+ def mod(a, m) = Functions.fold(Fn.new(:mod, [a, m]))
478
+
479
+ # bernoulli(n), fibonacci(n), harmonic(n): exact values of the classical sequences
480
+ def bernoulli(n) = Functions.fold(Fn.new(:bernoulli, [n]))
481
+ def fibonacci(n) = Functions.fold(Fn.new(:fibonacci, [n]))
482
+ def harmonic(n) = Functions.fold(Fn.new(:harmonic, [n]))
483
+
484
+ # Normal(0, 1), Uniform(a, b), Exponential(l), Bernoulli(p), Binomial(n, p), Poisson(l), Geometric(p), DiscreteUniform(1, 6): distributions
485
+ def Normal(mu = 0, sigma = 1) = Distributions::Normal.new(mu, sigma)
486
+ def Uniform(a = 0, b = 1) = Distributions::Uniform.new(a, b)
487
+ def Exponential(rate = 1) = Distributions::Exponential.new(rate)
488
+ def Bernoulli(p) = Distributions::Bernoulli.new(p)
489
+ def Binomial(n, p) = Distributions::Binomial.new(n, p)
490
+ def Poisson(rate) = Distributions::Poisson.new(rate)
491
+ def Geometric(p) = Distributions::Geometric.new(p)
492
+ def DiscreteUniform(a, b) = Distributions::DiscreteUniform.new(a, b)
493
+ # StudentT(nu), ChiSquare(k), FRatio(d1, d2): the sampling distributions of the tests
494
+ def StudentT(nu) = Distributions::StudentT.new(nu)
495
+ def ChiSquare(k) = Distributions::ChiSquare.new(k)
496
+ def FRatio(d1, d2) = Distributions::FRatio.new(d1, d2)
497
+ # pdf(X, x), cdf(X, x), probability(X, x > 1): the methods as functions
498
+ def pdf(dist, x) = dist.pdf(x)
499
+ def cdf(dist, x) = dist.cdf(x)
500
+ def probability(dist, event) = dist.probability(event)
501
+
502
+ # qpochhammer(a, q, n): (a; q)_n = (1 - a)(1 - a*q)...(1 - a*q**(n - 1))
503
+ def qpochhammer(a, q, n) = QFunctions.qpochhammer(a, q, n)
504
+ # qbracket(n, q): [n]_q = 1 + q + ... + q**(n - 1), the q-analogue of n
505
+ def qbracket(n, q) = QFunctions.qbracket(n, q)
506
+ # qfactorial(n, q): [n]_q! = [1]_q*[2]_q*...*[n]_q
507
+ def qfactorial(n, q) = QFunctions.qfactorial(n, q)
508
+ # qbinomial(n, k, q): the Gaussian binomial coefficient, a polynomial in q
509
+ def qbinomial(n, k, q) = QFunctions.qbinomial(n, k, q)
510
+
511
+ # qgosper(q**k, q, k): the q-antidifference S with S(k + 1) - S(k) = f(k), or nil
512
+ def qgosper(term, q, k) = QSummation.qgosper(term, Expression.lift(k), Expression.lift(q))
513
+
514
+ # qsum(q**k, q, k: 0..n-1): a definite q-hypergeometric sum, or the sum unevaluated
515
+ def qsum(term, q, k = nil, from = nil, to = nil, **range)
516
+ k, from, to = Functions.range_arguments(k, from, to, range, "qsum", discrete: true)
517
+ k = Expression.lift(k)
518
+ from = Expression.lift(from)
519
+ to = Expression.lift(to)
520
+ QSummation.qsum(term, k, from, to, Expression.lift(q)) || Sum.new(Expression.lift(term), k, from, to)
521
+ end
522
+
523
+ # sumrecursion(binomial(n, k)**2, k, S(n)): the recurrence a definite sum obeys (Zeilberger)
524
+ def sumrecursion(term, k, s, **opts) = Zeilberger.sumrecursion(term, k, s, **opts)
525
+
526
+ # sumcertificate(binomial(n, k)**2, k, S(n)): the rational certificate that proves it
527
+ def sumcertificate(term, k, s, **opts) = Zeilberger.sumcertificate(term, k, s, **opts)
528
+
529
+ # qsumrecursion(qbinomial(n, k, q), k, q, S(n)): the recurrence a definite q-sum obeys
530
+ def qsumrecursion(term, k, q, s, **opts) = QZeilberger.qsumrecursion(term, k, q, s, **opts)
531
+
532
+ # qsumcertificate(qbinomial(n, k, q), k, q, S(n)): the certificate that proves it
533
+ def qsumcertificate(term, k, q, s, **opts) = QZeilberger.qsumcertificate(term, k, q, s, **opts)
534
+
535
+ # qsolve(eq(f(q*x), (1 - a*x)*f(x)), f, x, q): a linear q-difference equation, at x = q**n
536
+ def qsolve(equation, f, x, q, **opts) = QDifference.qsolve(equation, f, x, q, **opts)
537
+
538
+ # qhyper(eq(f(q*x), (1 - a*x)*f(x)), f, x, q): the ratios f(q*x)/f(x) of its solutions
539
+ def qhyper(equation, f, x, q) = QDifference.qhyper(equation, f, x, q)
540
+
541
+ # hyper(eq(u(n + 1), n*u(n)), u, n): the hypergeometric solutions of a recurrence
542
+ def hyper(equation, u, n) = Recurrence.hyper(equation, u, n)
543
+
544
+ # doc(:factor), doc("ZZ"), doc(:Matrix): what a name does, from the source
545
+ def doc(name) = Docs.doc(name)
546
+
547
+ # plot(sin(x)), plot(f, x: -3..3), plot([f, g], x: 0..1), plot(Normal(0, 1)): a Plot,
548
+ # shown as braille art; .show for a picture, .save("f.svg"), .to_svg, .to_png
549
+ def plot(f, var = nil, from = nil, to = nil, **opts) = Plotting.plot(f, var, from, to, **opts)
550
+ # scatter(xs, ys) or scatter(points), fit: true adds the least squares line
551
+ def scatter(xs, ys = nil, **opts) = Plotting.scatter(xs, ys, **opts)
552
+
553
+ # plot3d(sin(x*y), x: -3..3, y: -3..3), plot3d([X, Y, Z], u: 0..1, v: 0..1): a surface,
554
+ # drawn as a mesh with what lies behind it hidden; n: the mesh, view: [azimuth, elevation]
555
+ def plot3d(f, **opts) = Plotting.plot3d(f, **opts)
556
+
557
+ # parametric([cos(t), sin(t)], t: 0..2*pi): a curve given by its two components
558
+ def parametric(pair, var = nil, from = nil, to = nil, **opts) = Plotting.parametric(pair, var, from, to, **opts)
559
+
560
+ # polar(1 + cos(t), t: 0..2*pi): a curve in polar coordinates
561
+ def polar(r, var = nil, from = nil, to = nil, **opts) = Plotting.polar(r, var, from, to, **opts)
562
+ # histogram(data, bins: 8), boxplot(data) or boxplot("a" => xs, "b" => ys), barchart(frequencies(data))
563
+ def histogram(data, **opts) = Plotting.histogram(data, **opts)
564
+ def boxplot(data = nil, **opts)
565
+ options, series = Functions.split_plot_options(opts)
566
+ Plotting.boxplot(data || series, **options)
567
+ end
568
+
569
+ # barchart(frequencies(data)) or barchart(names, counts): one bar per category
570
+ def barchart(categories = nil, counts = nil, **opts)
571
+ options, series = Functions.split_plot_options(opts)
572
+ Plotting.barchart(categories || series, counts, **options)
573
+ end
574
+
575
+ # ttest(data, mu: 0), ttest(xs, ys), ttest(xs, ys, paired: true), ztest(data, sigma: 2, mu: 0):
576
+ # tests of location; alternative: :two_sided (default), :less, :greater
577
+ def ttest(data, other = nil, **opts) = Hypothesis.ttest(data, other, **opts)
578
+ def ztest(data, sigma:, mu: 0, alternative: :two_sided) = Hypothesis.ztest(data, sigma: sigma, mu: mu, alternative: alternative)
579
+ # chisquare_test(counts, expected: nil): goodness of fit; chisquare_test(rows): independence
580
+ def chisquare_test(observed, **opts) = Hypothesis.chisquare_test(observed, **opts)
581
+ # ftest(xs, ys): the ratio of two sample variances
582
+ def ftest(xs, ys, alternative: :two_sided) = Hypothesis.ftest(xs, ys, alternative: alternative)
583
+ # binomial_test(9, 10, p: 1/2r): exact, the p value stays a rational
584
+ def binomial_test(successes, trials, p: Rational(1, 2), alternative: :two_sided) = Hypothesis.binomial_test(successes, trials, p: p, alternative: alternative)
585
+ # confidence_interval(data, level: 0.95, sigma: nil, parameter: :mean|:variance|:stdev), proportion_interval(k, n)
586
+ def confidence_interval(data, **opts) = Hypothesis.confidence_interval(data, **opts)
587
+ def proportion_interval(successes, trials, level: 0.95) = Hypothesis.proportion_interval(successes, trials, level: level)
588
+
589
+ # mean(data), median, mode, variance(data, sample: true), stdev, quantile(data, p), quartiles, iqr,
590
+ # moment(data, k), skewness, kurtosis, geometric_mean, harmonic_mean, frequencies: on a list or a distribution
591
+ def mean(obj) = obj.is_a?(Distributions::Distribution) ? obj.mean : Statistics.mean(obj)
592
+ def median(obj) = obj.is_a?(Distributions::Distribution) ? obj.median : Statistics.median(obj)
593
+ def mode(data) = Statistics.mode(data)
594
+ def variance(obj, sample: true) = obj.is_a?(Distributions::Distribution) ? obj.variance : Statistics.variance(obj, sample: sample)
595
+ def stdev(obj, sample: true) = obj.is_a?(Distributions::Distribution) ? obj.stdev : Statistics.stdev(obj, sample: sample)
596
+ def quantile(obj, p) = obj.is_a?(Distributions::Distribution) ? obj.quantile(p) : Statistics.quantile(obj, p)
597
+ def quartiles(data) = Statistics.quartiles(data)
598
+ def iqr(data) = Statistics.iqr(data)
599
+ def moment(obj, k, central: true) = obj.is_a?(Distributions::Distribution) ? obj.moment(k) : Statistics.moment(obj, k, central: central)
600
+ def skewness(obj) = obj.is_a?(Distributions::Distribution) ? obj.skewness : Statistics.skewness(obj)
601
+ def kurtosis(obj) = obj.is_a?(Distributions::Distribution) ? obj.kurtosis : Statistics.kurtosis(obj)
602
+ def geometric_mean(data) = Statistics.geometric_mean(data)
603
+ def harmonic_mean(data) = Statistics.harmonic_mean(data)
604
+ def frequencies(data) = Statistics.frequencies(data)
605
+ # covariance(xs, ys), correlation(xs, ys), linreg(xs, ys, x): two data lists; linreg is the least squares line a + b*x
606
+ def covariance(xs, ys, sample: true) = Statistics.covariance(xs, ys, sample: sample)
607
+ def correlation(xs, ys) = Statistics.correlation(xs, ys)
608
+ def linreg(xs, ys, x = :x) = Statistics.linreg(xs, ys, x)
609
+
610
+ # D(y, x) is the derivative of the unknown function y; dsolve solves ODEs.
611
+ def D(expr, var, order = 1) = Derivative.new(expr, var, order)
612
+ # dsolve(eq, y, x); a system: dsolve([eq(D(x, t), y), eq(D(y, t), -x)], [x, y], t)
613
+ def dsolve(equation, y, x) = ODE.dsolve(equation, y, x)
614
+
615
+ # assume(x: ZZ, y: RR) declares variable domains; assumptions lists them.
616
+ # assume(x: ZZ) declares a domain, assume(x > 0) a sign; assumptions lists both.
617
+ # With a block - assume(x: ZZ) { solve(f, x) } - they hold for the block alone
618
+ def assume(*facts, **table, &block) = RCAS.assume(*facts, **table, &block)
619
+ def forget(*names) = RCAS.forget(*names)
620
+ def assumptions = RCAS.assumptions
621
+
622
+ # vector(QQ, 1, 2, 3), vector(1, 2, 3) or vector([1, 2, 3]) with the domain inferred.
623
+ def vector(*args)
624
+ domain = args.first.is_a?(Domain) ? args.shift : nil
625
+ args = args.first if args.size == 1 && args.first.is_a?(Array)
626
+ return domain.vector(*args) if domain
627
+ Functions.infer_domain(args) { |inferred| inferred.vector(*args) }
628
+ end
629
+
630
+ # matrix(QQ, [[1, 2], [3, 4]]) or matrix([[1, 2], [3, 4]]) with the domain inferred.
631
+ def matrix(*args)
632
+ domain = args.first.is_a?(Domain) ? args.shift : nil
633
+ rows = args.size == 1 ? args.first : args
634
+ return domain.matrix(rows) if domain
635
+ Functions.infer_domain(args.flatten) { |inferred| inferred.matrix(rows) }
636
+ end
637
+
638
+ # The domain the entries lie in; with a block, what the block builds
639
+ # over it. The block runs under the same reading of the names that
640
+ # found the domain, so that the constructor's own membership check
641
+ # agrees with it (see complex_reading).
642
+ def self.infer_domain(entries, &build)
643
+ build ||= :itself.to_proc
644
+ lifted = entries.map { |e| Expression.lift(e) }
645
+ known = lifted.map { |e| Infer.where_defined { Scalar.domain(e) } }
646
+ return build.call(known.reduce(ZZ) { |d, ed| d.join(ed) }) if known.all?
647
+ if (ring = symbolic_domain(lifted))
648
+ build.call(ring)
649
+ elsif (names = complex_reading(lifted))
650
+ RCAS.assume(**names) { build.call(complex_reading_domain(lifted)) }
651
+ else
652
+ e = lifted[known.index(nil)]
653
+ names = e.variables.reject { |v| RCAS.assumption(v) }
654
+ hint = names.empty? ? "" : "; declare #{names.map { |v| "#{v}.in(RR)" }.join(', ')} first"
655
+ raise DomainError, "can't infer a domain for #{e}#{hint}, or name one: RR.matrix(...)"
656
+ end
657
+ end
658
+
659
+ # Entries in undeclared indeterminates live in the smallest polynomial
660
+ # ring over ZZ, QQ, RR or CC that holds them all, and past that in its
661
+ # fraction field: matrix([[a, b], [c, d]]) is over ZZ[a, b, c, d], as
662
+ # matrix([[1, 2], [3, 4]]) is over ZZ. A declared name is a scalar of
663
+ # its own domain and not an indeterminate of the ring. nil when an
664
+ # entry is not a rational function (the generic eigenvalue, with its
665
+ # square root; sin(a)).
666
+ def self.symbolic_domain(entries)
667
+ free = free_names(entries)
668
+ return nil if free.empty?
669
+ rings = [ZZ, QQ, RR, CC].map { |k| PolynomialRing.new(k, free) }
670
+ (rings + rings.drop(1).map(&:fraction_field)).find do |dom|
671
+ entries.all? { |e| Infer.where_defined { dom.include?(e) } }
672
+ end
673
+ end
674
+
675
+ # Past the rational functions the undeclared names are read as what
676
+ # they are to rcas anyway, complex numbers - the matrix is then what it
677
+ # would be after a.in(CC), over CC - so that M - lambda*I can be
678
+ # written out with the eigenvalue rcas just gave. { name => CC } when
679
+ # that gives every entry a domain, nil otherwise.
680
+ def self.complex_reading(entries)
681
+ free = free_names(entries)
682
+ return nil if free.empty?
683
+ names = free.to_h { |v| [v, CC] }
684
+ names if RCAS.assume(**names) { complex_reading_domain(entries) }
685
+ end
686
+
687
+ def self.complex_reading_domain(entries)
688
+ known = entries.map { |e| Infer.where_defined { Scalar.domain(e) } }
689
+ known.reduce(ZZ) { |d, ed| d.join(ed) } if known.all?
690
+ end
691
+
692
+ def self.free_names(entries) = entries.flat_map(&:variables).uniq.reject { |v| RCAS.assumption(v) }.sort
693
+
694
+ ODD = %i[sin tan atan asin sinh sign erf Si].freeze
695
+ EVEN = %i[cos cosh abs].freeze
696
+
697
+ # Neither odd nor even, but reflected about a point: acos(-u) is
698
+ # pi - acos(u). Without it acos(-2**(1/2)/2) had no value although
699
+ # acos(2**(1/2)/2) has one, since the exact table is written for
700
+ # positive arguments and only the odd ones carry the sign across.
701
+ REFLECTED = { acos: ->(value) { (PI - value).simplify } }.freeze
702
+
703
+ # The functions rcas hands to Math for a Float argument. Asking
704
+ # `Math.respond_to?` instead looks safe and is not: `include
705
+ # RCAS::Functions` into Object - which is what README tells a library
706
+ # user to do - gives the Math module itself a `floor`, so
707
+ # Math.public_send(:floor, 2.5) lands back in Functions#floor, which
708
+ # folds, which asks again. floor, ceil, round, bernoulli, fibonacci and
709
+ # harmonic all recursed that way until the stack ran out.
710
+ MATH_NAMES = %i[sin cos tan asin acos atan sinh cosh exp log erf erfc gamma].freeze
711
+
712
+ # f(g(u)) = u, the direction that holds for every u. The other way
713
+ # round is only true on the inverse's own range.
714
+ INVERSE_PAIRS = { sin: :asin, cos: :acos, tan: :atan }.freeze
715
+
716
+ def self.imaginary_unit_value?(u)
717
+ u.is_a?(Num) && u.value.is_a?(Complex) && u.value.real.zero? && u.value.imaginary.abs == 1
718
+ end
719
+
720
+ # Math.log(-1.0) and Math.asin(2.0) raise Math::DomainError: the value
721
+ # is outside the reals, so the node stays as it is rather than the error
722
+ # reaching the user (a divergent sum used to come back as "Numerical
723
+ # argument is out of domain - log").
724
+ # asin and acos are the exception, because there the value outside the
725
+ # reals is the answer and not a symptom: cos(x) = 2 is solved at
726
+ # +-acos(2) + 2*pi*k, and a family whose members evaluate to nothing is
727
+ # correct but inert (20 Sept 2026, the tenth pass of the review).
728
+ # The branch is the one every C library takes - acos of a number past
729
+ # the interval has negative imaginary part, asin positive - written out
730
+ # rather than derived, because the general formula picks the other side
731
+ # of the cut for a real argument with no signed zero on it.
732
+ def self.math_value(fn, value)
733
+ Num.new(Math.public_send(fn.name, value))
734
+ rescue Math::DomainError
735
+ beyond = real_branch(fn.name, value)
736
+ beyond ? Num.new(beyond) : fn
737
+ end
738
+
739
+ # acos and asin of a real number outside [-1, 1]: with
740
+ # acosh(t) = log(t + sqrt(t**2 - 1)) for t >= 1,
741
+ # acos(x) = (x > 1 ? 0 : pi) - i*acosh(|x|) and asin(x) = pi/2 - acos(x).
742
+ def self.real_branch(name, value)
743
+ # log of a negative Float is its principal value log|x| + i*pi, a
744
+ # value and not an error (third review, C7)
745
+ return Complex(Math.log(-value), Math::PI) if name == :log && value.is_a?(Float) && value.negative?
746
+ return nil unless %i[asin acos].include?(name) && value.is_a?(Float) && value.abs > 1
747
+ t = value.abs
748
+ acosh = Math.log(t + Math.sqrt(t * t - 1))
749
+ acos = Complex(value.positive? ? 0.0 : Math::PI, -acosh)
750
+ name == :acos ? acos : Complex(Math::PI / 2, 0.0) - acos
751
+ end
752
+
753
+ # Constant folding for function applications; called by Simplify.
754
+ # The real n-th root once the sign of the radicand is known: a positive
755
+ # one has its ordinary root, a negative one -|x|**(1/n) for odd n and no
756
+ # real root for even n (undefined). The design decision of the fifth
757
+ # review: ** stays principal, as in MuPAD, Maple and Mathematica, and
758
+ # surd (their name) is the real root.
759
+ def self.surd_value(fn)
760
+ x, n = fn.args
761
+ # evalf floats every leaf, the index too: 3.0 is still the index 3
762
+ n = Num.new(n.value.to_i) if n.is_a?(Num) && n.value.is_a?(Float) && n.value.finite? && n.value == n.value.round
763
+ return fn unless n.is_a?(Num) && n.value.is_a?(Integer) && n.value.positive?
764
+ k = n.value
765
+ return x if k == 1
766
+ if x.is_a?(Num) && x.value.is_a?(Float)
767
+ v = x.value
768
+ return UNDEFINED if v.negative? && k.even?
769
+ return Num.new(v.negative? ? -((-v)**(1.0 / k)) : v**(1.0 / k))
770
+ end
771
+ sign = x.variables.empty? ? Decide.sign(x) : RCAS.sign_of(x)
772
+ case sign
773
+ when :positive, :nonnegative, :zero then (x**Num.new(Rational(1, k))).simplify
774
+ when :negative, :nonpositive
775
+ return fn unless k.odd? || sign == :negative
776
+ return UNDEFINED if k.even?
777
+ Neg.new(Pow.new(Neg.new(x).simplify, Num.new(Rational(1, k)))).simplify
778
+ else fn
779
+ end
780
+ end
781
+
782
+ def self.fold(fn)
783
+ if QFunctions::NAMES.include?(fn.name)
784
+ folded = QFunctions.fold(fn)
785
+ return folded if folded
786
+ end
787
+ if fn.name == :binomial && fn.args.size == 2
788
+ n, k = fn.args
789
+ return Combinatorics.binomial_value(n, k) || fn
790
+ end
791
+ return surd_value(fn) if fn.name == :surd && fn.args.size == 2
792
+ if fn.name == :mod && fn.args.size == 2
793
+ a, m = fn.args
794
+ return fn unless a.is_a?(Num) && m.is_a?(Num) && a.value.real? && m.value.real? && !m.value.zero?
795
+ return Num.new(Simplify.normalize_number(a.value % m.value))
796
+ end
797
+ return fn unless fn.args.size == 1
798
+ arg = fn.args.first
799
+
800
+ # a whole period added to the argument of sin, cos or tan drops out
801
+ if (reduced = Trigonometry.reduce_period(fn.name, arg))
802
+ return fold(Fn.new(fn.name, [reduced]))
803
+ end
804
+
805
+ # odd / even symmetry: sin(-u) = -sin(u), cos(-u) = cos(u). A negative
806
+ # number counts: cos(-1) is cos(1), which is what lets a definite
807
+ # integral of tan over a symmetric range come out as 0.
808
+ # a float argument is Math's business: pi - 1.047... is no better than
809
+ # the number, and acos is neither odd nor even, so falling through to
810
+ # either branch would answer acos(-0.5) with acos(0.5)
811
+ reflected = REFLECTED.key?(fn.name) && !(arg.is_a?(Num) && arg.value.is_a?(Float))
812
+ if ODD.include?(fn.name) || EVEN.include?(fn.name) || reflected
813
+ coeff, factors = Simplify.factorize(arg)
814
+ negative = Simplify.negative?(coeff)
815
+ if !negative && (pair = factors.find { |b, e| e == 1 && Simplify.negative_sum?(b) })
816
+ factors = factors.dup
817
+ factors.delete(pair.first)
818
+ # add, not assign: -(-2 - 0) is 2, which may already be a base
819
+ # (2**(-1/2) in (-2 - 0)/sqrt(2)), and writing 2 => 1 over it
820
+ # lost the root - Normal(0, 1).cdf(-2) came out as erf(2)
821
+ Simplify.add_factor(factors, Simplify.simplify(Neg.new(pair.first)), 1)
822
+ negative = true
823
+ end
824
+ if negative
825
+ flipped = Fn.new(fn.name, [Simplify.rebuild_product(Simplify.negative?(coeff) ? -coeff : coeff, factors)])
826
+ reflect = REFLECTED[fn.name] if reflected
827
+ next_value = fold(flipped)
828
+ return reflect.call(next_value) if reflect
829
+ return ODD.include?(fn.name) ? Simplify.negate(next_value) : next_value
830
+ end
831
+ end
832
+
833
+ # cos(acos(u)) is u for every u, and that is the direction that holds:
834
+ # acos(cos(u)) is not u outside [0, pi]. It matters because the
835
+ # inverse rarely has a value to fold to - solve(cos(x) - 2, x)
836
+ # answers with acos(2), which is a number but not one the real
837
+ # tables know (20 Sept 2026, the tenth pass of the review).
838
+ # tan(atan(i)) is the exception: atan has no value at +-i, so there
839
+ # is nothing to undo.
840
+ if (inner = INVERSE_PAIRS[fn.name]) && arg.is_a?(Fn) && arg.name == inner && arg.args.size == 1
841
+ undone = arg.args.first
842
+ return undone unless fn.name == :tan && imaginary_unit_value?(undone)
843
+ end
844
+
845
+ exact = exact_value(fn.name, arg)
846
+ return exact if exact
847
+ at_infinity = infinity_value(fn.name, arg)
848
+ return at_infinity if at_infinity
849
+
850
+ case [fn.name, arg]
851
+ in [_, Num => n] if n.value.is_a?(Float) && MATH_NAMES.include?(fn.name) then math_value(fn, n.value)
852
+ in [_, Num => n] if n.value.is_a?(Complex) && (n.value.real.is_a?(Float) || n.value.imaginary.is_a?(Float)) && CMath_lite::NAMES.include?(fn.name)
853
+ complex_value(fn.name, n.value) || fn
854
+ in [:sin, Num => n] if n.zero? then Num.new(0)
855
+ in [:cos, Num => n] if n.zero? then Num.new(1)
856
+ in [:tan, Num => n] if n.zero? then Num.new(0)
857
+ in [:atan, Num => n] if n.zero? then Num.new(0)
858
+ in [:sinh, Num => n] if n.zero? then Num.new(0)
859
+ in [:cosh, Num => n] if n.zero? then Num.new(1)
860
+ in [:exp, Num => n] if n.zero? then Num.new(1)
861
+ in [:log, Num => n] if n.one? then Num.new(0)
862
+ in [:floor, Num => n] if n.value.real? then Num.new(n.value.floor)
863
+ in [:ceil, Num => n] if n.value.real? then Num.new(n.value.ceil)
864
+ in [:round, Num => n] if n.value.real? then Num.new(n.value.round)
865
+ in [:re, Num => n] then Num.new(Simplify.normalize_number(n.value.real))
866
+ in [:im, Num => n] then Num.new(Simplify.normalize_number(n.value.imaginary))
867
+ in [:conj, Num => n] then Num.new(Simplify.normalize_number(n.value.conj))
868
+ in [:arg, Num => n] then ComplexParts.arg(n)
869
+ in [:bernoulli, Num => n] if n.value.is_a?(Integer) && n.value >= 0 then Num.new(Simplify.normalize_number(Summation.bernoulli(n.value)))
870
+ in [:fibonacci, Num => n] if n.value.is_a?(Integer) then Num.new(Combinatorics.fibonacci(n.value))
871
+ in [:harmonic, Num => n] if n.value.is_a?(Integer) && n.value >= 0 then Num.new(Simplify.normalize_number((1..n.value).sum(0r) { |k| Rational(1, k) }))
872
+ in [:erf, Num => n] if n.zero? then Num.new(0)
873
+ in [:erfc, Num => n] if n.zero? then Num.new(1)
874
+ in [:erf, Const => c] if c.name == :oo then Num.new(1)
875
+ in [:erfc, Const => c] if c.name == :oo then Num.new(0)
876
+ in [:erfc, Neg => e] if e.arg.is_a?(Const) && e.arg.name == :oo then Num.new(2)
877
+ in [:exp, Fn => inner] if inner.name == :log then inner.args.first
878
+ # exp(r*log(u)) is u**r: that is the definition of the principal power
879
+ in [:exp, Mul | Div | Neg => product] if (power = log_power(product)) then power
880
+ in [:log, Fn => inner] if inner.name == :exp && Functions.principal_log?(inner.args.first) then inner.args.first
881
+ else fn
882
+ end
883
+ end
884
+ end
885
+
886
+ # log(exp(u)) is u only on the principal strip -pi < im(u) <= pi; outside
887
+ # it the logarithm comes back reduced, so log(exp(2*pi*i)) is log(1) = 0
888
+ # and not 2*pi*i (22 Sept 2026, from a review). A real u is always inside
889
+ # it; an undeclared indeterminate is not known to be real, so the node
890
+ # stays until `assume(x: RR)` says otherwise. exp(log(u)) is u for every
891
+ # u and needs no such guard.
892
+ module Functions
893
+ # pi > PI_LOWER, an exact rational, is what lets an exact imaginary part
894
+ # be decided without ever comparing it against a Float: |im| <= 31/10
895
+ # proves |im| < pi. The strip's own edge, im = +-pi, is a rational
896
+ # multiple of pi and is decided exactly by that route instead. Deciding
897
+ # it by Float missed im = pi*(1 + 10**-20), which rounds to Math::PI and
898
+ # is outside (22 Sept 2026, from the second review). The strip is the
899
+ # principal branch's own: [DLMF, §4.2(i), eq. 4.2.5].
900
+ PI_LOWER = Rational(31, 10)
901
+
902
+ def self.principal_log?(u)
903
+ return true if ComplexParts.real_valued?(u)
904
+ imaginary = ComplexParts.im(u)
905
+ return false unless imaginary.variables.empty?
906
+ # im = r*pi: -1 < r <= 1, compared as rationals
907
+ if (r = Trig.pi_multiple(imaginary))
908
+ return r > -1 && r <= 1
909
+ end
910
+ # any other exact real im: |im| <= 31/10 < pi is inside, and nothing
911
+ # else is decided here
912
+ imaginary.is_a?(Num) && imaginary.value.real? && imaginary.value.abs <= PI_LOWER
913
+ end
914
+ end
915
+
916
+ # Exact special values: sin(pi/6), exp(i*pi), atan(1), asin(1/2), log(8)...
917
+ module Functions
918
+ # exp, log and atan at oo and -oo, the values their limits have: exp(-oo)
919
+ # is 0, log(oo) and exp(oo) are oo, atan(+-oo) is +-pi/2 (third review,
920
+ # 3.2). log(-oo) and log(0) are left alone.
921
+ def self.infinity_value(name, arg)
922
+ plus = arg == OO
923
+ minus = !plus && Limits.infinite?(arg)
924
+ return nil unless plus || minus
925
+ case name
926
+ when :exp then plus ? OO : Num.new(0)
927
+ when :log then plus ? OO : nil
928
+ when :atan then plus ? (PI / 2).simplify : (-PI / 2).simplify
929
+ end
930
+ end
931
+
932
+ # r*log(u) with a rational r, as u**r; nil for anything else.
933
+ def self.log_power(e)
934
+ coeff, factors = Simplify.factorize(e)
935
+ return nil unless factors.size == 1 && (coeff.is_a?(Integer) || coeff.is_a?(Rational))
936
+ base, exp = factors.first
937
+ return nil unless exp == 1 && base.is_a?(Fn) && base.name == :log && base.args.size == 1
938
+ Simplify.simplify(Pow.new(base.args.first, Num.new(coeff)))
939
+ end
940
+
941
+ def self.exact_value(name, arg)
942
+ case name
943
+ when :sin, :cos, :tan
944
+ if (n = Trig.integer_pi_multiple(arg))
945
+ return name == :cos ? Pow.new(Num.new(-1), n) : Num.new(0)
946
+ end
947
+ r = Trig.pi_multiple(arg) or return nil
948
+ Trig.public_send(:"#{name}_pi", r)
949
+ when :exp
950
+ r = Trig.imaginary_pi_multiple(arg) or return nil
951
+ Trig.exp_i_pi(r)
952
+ when :asin, :atan
953
+ return nil unless arg.is_a?(Num) || arg.is_a?(Pow) || arg.is_a?(Mul) || arg.is_a?(Div)
954
+ if arg.is_a?(Num) && arg.value.real? && arg.value.negative? # asin(-1/2) = -asin(1/2)
955
+ v = exact_value(name, Num.new(-arg.value)) or return nil
956
+ return Neg.new(v).simplify
957
+ end
958
+ name == :asin ? Trig.asin_exact(arg) : Trig.atan_exact(arg)
959
+ when :acos
960
+ if arg.is_a?(Num) && arg.value.real? && arg.value.negative? # acos(-v) = pi - acos(v)
961
+ v = exact_value(:acos, Num.new(-arg.value)) or return nil
962
+ return (PI - v).simplify
963
+ end
964
+ v = Trig.asin_exact(arg) or return nil
965
+ (PI / 2 - v).simplify
966
+ when :abs
967
+ if arg.is_a?(Num) && arg.value.is_a?(Complex) && [arg.value.real, arg.value.imaginary].none? { |c| c.is_a?(Float) }
968
+ # |1 + i| is sqrt(2), not 1.414...: exact in, exact out (A7)
969
+ return RCAS.sqrt(Num.new(arg.value.real**2 + arg.value.imaginary**2)).simplify
970
+ end
971
+ return Num.new(arg.value.abs) if arg.is_a?(Num)
972
+ # |b**z| = b**re(z) for b > 0: |2**i| is 1 (the fourth review, C1)
973
+ if arg.is_a?(Pow) && RCAS.sign_of(arg.base) == :positive && !ComplexParts.real_valued?(arg.exponent)
974
+ real = ComplexParts.re(arg.exponent)
975
+ return (arg.base**real).simplify unless real.each_node.any? { |n| n.is_a?(Fn) && %i[re im].include?(n.name) }
976
+ end
977
+ if arg.variables.empty? && arg.each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Complex) }
978
+ re, im = ComplexParts.parts(arg)
979
+ return RCAS.sqrt((re**2 + im**2).expand).simplify unless [re, im].any? { |part| part.each_node.any? { |n| n.is_a?(Fn) && %i[re im].include?(n.name) } }
980
+ end
981
+ return arg if RCAS.nonnegative?(arg)
982
+ return Simplify.negate(arg).simplify if %i[negative nonpositive].include?(RCAS.sign_of(arg))
983
+ d = arg.domain
984
+ d && d <= NN ? arg : nil
985
+ when *IntegralFunctions::NAMES
986
+ IntegralFunctions.value(name, arg)
987
+ when :sign
988
+ return Num.new(arg.value <=> 0) if arg.is_a?(Num) && arg.value.real?
989
+ case RCAS.sign_of(arg)
990
+ when :positive then Num.new(1)
991
+ when :negative then Num.new(-1)
992
+ end
993
+ when :factorial then arg.is_a?(Num) ? Combinatorics.factorial_value(arg.value) : nil
994
+ when :gamma then arg.is_a?(Num) ? Combinatorics.gamma_value(arg.value) : nil
995
+ when :zeta
996
+ return nil unless arg.is_a?(Num)
997
+ v = arg.value
998
+ return OO if v == 1
999
+ return Num.new(Summation.zeta_numeric(v)) if v.is_a?(Float)
1000
+ return nil unless v.is_a?(Integer) && v > 1 && v.even?
1001
+ Summation.zeta_even(v)
1002
+ when :log
1003
+ # the principal value of a negative number: log(-1) is i*pi and
1004
+ # log(-2) is log(2) + i*pi. Held back until the fifth review settled
1005
+ # that real_domain(x*log(-2)) is empty (every subexpression real),
1006
+ # and needed once solve is complete: exp(x) = -1 is i*pi + 2*pi*i*k
1007
+ if arg.is_a?(Num) && (arg.value.is_a?(Integer) || arg.value.is_a?(Rational)) && arg.value.negative?
1008
+ return (Functions.fold(Fn.new(:log, [Num.new(-arg.value)])) + I * PI).simplify
1009
+ end
1010
+ return nil unless arg.is_a?(Num) && arg.value.is_a?(Integer) && arg.value > 1
1011
+ division = NumberTheory.prime_division(arg.value, hard: false)
1012
+ return nil if division.nil?
1013
+ k = division.map(&:last).reduce(:gcd)
1014
+ return nil if k < 2
1015
+ root = division.reduce(1) { |acc, (p, e)| acc * p**(e / k) }
1016
+ Mul.new(Num.new(k), Fn.new(:log, [Num.new(root)]))
1017
+ end
1018
+ end
1019
+ end
1020
+
1021
+ # Complex-valued exp/sin/cos on floats without the deprecated CMath gem.
1022
+ # The principal branches throughout [AS64, 4.1-4.6]: log with its
1023
+ # imaginary part in (-pi, pi], the inverse functions through log and
1024
+ # sqrt of complex numbers. evalf of log, tan and the rest at a complex
1025
+ # point used to stay symbolic (third review, C7).
1026
+ module Functions
1027
+ module_function
1028
+
1029
+ # A complex Float value, or no value: atan(i) is the pole of the
1030
+ # arctangent, and its Float came out as (0 + Infinity*i) (fourth
1031
+ # review, C7).
1032
+ def complex_value(name, z)
1033
+ v = CMath_lite.public_send(name, z)
1034
+ return nil if v.nil?
1035
+ finite = v.is_a?(Complex) ? v.real.to_f.finite? && v.imaginary.to_f.finite? : v.to_f.finite?
1036
+ finite ? Num.new(v) : UNDEFINED
1037
+ end
1038
+ end
1039
+
1040
+ module CMath_lite
1041
+ NAMES = %i[exp sin cos log tan sinh cosh asin acos atan erf erfc].freeze
1042
+
1043
+ module_function
1044
+
1045
+ # erf by its power series, where that does not cancel away the digits
1046
+ # (|z| <= 3); nil beyond, and the node stays (fourth review, C7)
1047
+ def erf(z)
1048
+ return nil if z.abs > 3
1049
+ term = z
1050
+ sum = z
1051
+ square = z * z
1052
+ (1..200).each do |n|
1053
+ term *= -square / n
1054
+ piece = term / (2 * n + 1)
1055
+ sum += piece
1056
+ break if piece.abs < 1e-17 * sum.abs
1057
+ end
1058
+ sum * 2 / Math.sqrt(Math::PI)
1059
+ end
1060
+
1061
+ def erfc(z) = (value = erf(z)) && 1 - value
1062
+
1063
+ def exp(z) = Complex(Math.exp(z.real) * Math.cos(z.imaginary), Math.exp(z.real) * Math.sin(z.imaginary))
1064
+ def sin(z) = Complex(Math.sin(z.real) * Math.cosh(z.imaginary), Math.cos(z.real) * Math.sinh(z.imaginary))
1065
+ def cos(z) = Complex(Math.cos(z.real) * Math.cosh(z.imaginary), -Math.sin(z.real) * Math.sinh(z.imaginary))
1066
+ def log(z) = Complex(Math.log(z.abs), Math.atan2(z.imaginary.to_f, z.real.to_f))
1067
+ def tan(z) = sin(z) / cos(z)
1068
+ def sinh(z) = (exp(z) - exp(-z)) / 2
1069
+ def cosh(z) = (exp(z) + exp(-z)) / 2
1070
+ def sqrt(z) = exp(log(z) / 2)
1071
+ def asin(z) = Complex(0, -1) * log(Complex(0, 1) * z + sqrt(1 - z * z))
1072
+ def acos(z) = Complex(Math::PI / 2) - asin(z)
1073
+ def atan(z) = Complex(0, 0.5) * (log(1 - Complex(0, 1) * z) - log(1 + Complex(0, 1) * z))
1074
+ end
1075
+
1076
+ extend Functions
1077
+
1078
+ # u(n + 1), f(x) in a session: an undefined name applied to expressions is
1079
+ # an unknown function (the notation rsolve uses); nil for other arguments,
1080
+ # so the caller can raise NoMethodError as usual.
1081
+ def self.unknown_function(name, args)
1082
+ return nil unless args.all? { |a| a.is_a?(Expression) || a.is_a?(Numeric) || a.is_a?(Symbol) }
1083
+ hint_at_typo(name)
1084
+ Fn.new(name, args.map { |a| Expression.lift(a) })
1085
+ end
1086
+
1087
+ # sqr(2) is an unknown function called sqr, and printing it back is the
1088
+ # right answer - u(n + 1) has to work the same way. But a name one letter
1089
+ # away from a function rcas has is more likely a typo than a function of
1090
+ # the user's own, and saying so once costs nothing. Short names (u, f, y)
1091
+ # are the ones people really do use, so they are left alone.
1092
+ def self.hint_at_typo(name)
1093
+ return if name.length < 3
1094
+ @hinted ||= {}
1095
+ return if @hinted[name]
1096
+ @hinted[name] = true
1097
+ known = function_names.select { |k| k != name && k.length > 2 && one_edit_apart?(name.to_s, k.to_s) }
1098
+ .min_by { |k| [-common_prefix(name.to_s, k.to_s), k.length, k.to_s] }
1099
+ return unless known
1100
+ warn "rcas: #{name} is an unknown function (it prints back as written); did you mean #{known}?"
1101
+ end
1102
+
1103
+ # Everything a session can call: the named functions and the top-level ones.
1104
+ def self.function_names
1105
+ @function_names ||= (Functions::NAMES + Functions.public_instance_methods(false)).uniq.sort
1106
+ end
1107
+
1108
+ def self.common_prefix(a, b)
1109
+ i = 0
1110
+ i += 1 while i < a.size && i < b.size && a[i] == b[i]
1111
+ i
1112
+ end
1113
+
1114
+ # One insertion, deletion or substitution apart.
1115
+ def self.one_edit_apart?(a, b)
1116
+ return false if (a.size - b.size).abs > 1
1117
+ long, short = a.size >= b.size ? [a, b] : [b, a]
1118
+ i = 0
1119
+ i += 1 while i < short.size && long[i] == short[i]
1120
+ return true if long.size == short.size && long[(i + 1)..] == short[(i + 1)..] # substitution
1121
+ long[(i + 1)..] == short[i..] # insertion or deletion
1122
+ end
1123
+
1124
+ # Kernel's one- and two-letter printers (p, pp, and j, jj from the JSON
1125
+ # library) would otherwise capture the short names most wanted as
1126
+ # indeterminates (p for a prime!). A session's main object undefines
1127
+ # them, so the bare name reaches the auto-symbol hook like any other;
1128
+ # print with puts, print or Kernel.p(expr) instead.
1129
+ UNDEFINED_KERNEL_METHODS = %i[p pp j jj].freeze
1130
+
1131
+ def self.undefine_kernel_printers(main)
1132
+ UNDEFINED_KERNEL_METHODS.each do |name|
1133
+ main.singleton_class.undef_method(name) if main.respond_to?(name, true)
1134
+ end
1135
+ end
1136
+ end