rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -0,0 +1,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
|