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,1589 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# An unevaluated integral, returned for pieces no method could integrate.
|
|
5
|
+
class Integral < Expression
|
|
6
|
+
attr_reader :integrand, :var, :from, :to
|
|
7
|
+
|
|
8
|
+
def initialize(integrand, var, from = nil, to = nil)
|
|
9
|
+
@integrand = integrand
|
|
10
|
+
@var = var
|
|
11
|
+
@from = from
|
|
12
|
+
@to = to
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def definite? = !from.nil?
|
|
17
|
+
def bound_variable = definite? ? var : nil
|
|
18
|
+
def children = definite? ? [integrand, var, from, to] : [integrand, var]
|
|
19
|
+
def rebuild(integrand, var, from = nil, to = nil) = Integral.new(integrand, var, from, to)
|
|
20
|
+
|
|
21
|
+
# An indefinite integral at a point is its antiderivative there: the
|
|
22
|
+
# variable is taken first, as for a Derivative (integral(x**2, x) at
|
|
23
|
+
# x = 2 was integral(4, 2)). A definite one binds its variable and goes
|
|
24
|
+
# through the binder protocol.
|
|
25
|
+
def replace_with(table)
|
|
26
|
+
return super if definite? || table.key?(self)
|
|
27
|
+
pointwise = table.key?(var) || table.any? { |k, v| k != var && v.variables.include?(var.name) && integrand.variables.include?(k.name) }
|
|
28
|
+
return super unless pointwise
|
|
29
|
+
found = evaluate
|
|
30
|
+
raise RCAS::Unsupported, "#{self} has no antiderivative rcas can find, so it cannot be evaluated at a point" if found.each_node.any? { |n| n.is_a?(Integral) }
|
|
31
|
+
found.replace_with(table)
|
|
32
|
+
end
|
|
33
|
+
def to_sexp = [:integral, *children.map(&:to_sexp)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Indefinite integration.
|
|
37
|
+
#
|
|
38
|
+
# 1. linearity, constant factors, a table of elementary forms with a
|
|
39
|
+
# linear argument, abs/sign of a linear argument, derivative-divides
|
|
40
|
+
# substitution, integration by parts
|
|
41
|
+
# 2. rational functions exactly: Hermite reduction for the rational part,
|
|
42
|
+
# Lazard-Rioboo-Trager for the logarithmic part (log and atan terms
|
|
43
|
+
# with roots of degree <= 2 of the Rothstein-Trager resultant), and
|
|
44
|
+
# partial fractions over the real quadratic factors of a biquadratic
|
|
45
|
+
# denominator for what that leaves (1/(x**4 + 1))
|
|
46
|
+
# 3. a Risch-Norman heuristic: an ansatz that is a Laurent polynomial in x
|
|
47
|
+
# and the transcendental/algebraic atoms of the integrand, plus log
|
|
48
|
+
# terms, whose undetermined coefficients are found by linear algebra
|
|
49
|
+
# 4. rationalizing substitutions (integrate_substitutions.rb): square roots
|
|
50
|
+
# of quadratics, roots of linear forms and of ratios of them, exponentials, sin/cos
|
|
51
|
+
#
|
|
52
|
+
# Sources (keys: MANUAL.md, Sources): Hermite reduction [Her72] in Mack's
|
|
53
|
+
# linear form [Mac75], [Bro05, §2.2]; Rothstein-Trager resultant [RT76],
|
|
54
|
+
# [Bro05, §2.4]; Lazard-Rioboo-Trager [LR90], [Bro05, §2.5]; the whole
|
|
55
|
+
# rational case also [GCL92, ch. 11]; Risch-Norman [NM77], [GS89];
|
|
56
|
+
# decomposition into real quadratic factors [Har16, ch. II].
|
|
57
|
+
module Integrate
|
|
58
|
+
MAX_DEPTH = 8
|
|
59
|
+
MAX_UNKNOWNS = 400
|
|
60
|
+
MAX_SYMBOLIC_UNKNOWNS = 100
|
|
61
|
+
ATOM_FUNCTIONS = %i[exp log sin cos sinh cosh atan asin acos].freeze
|
|
62
|
+
TABLE_FUNCTIONS = %i[exp log sin cos tan sinh cosh atan].freeze
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
# The antiderivative a reader is given: at the parameter values where a
|
|
67
|
+
# denominator of the generic answer vanishes, the integrand is integrated
|
|
68
|
+
# again and the answer is a Piecewise - sin(a*x)*cos(x) at a = 1 is
|
|
69
|
+
# sin(x)**2/2, where the generic form divides by 1 - a, and x**a at
|
|
70
|
+
# a = -1 is log(x). The fifth review's decision, as MuPAD and SymPy do;
|
|
71
|
+
# `generic: true` (MuPAD's IgnoreSpecialCases) keeps the short form.
|
|
72
|
+
# Only the public entry points ask for this: the rules inside rcas want
|
|
73
|
+
# the generic antiderivative.
|
|
74
|
+
def with_special_cases(expr, var, generic: false)
|
|
75
|
+
f = Expression.lift(expr)
|
|
76
|
+
x = Expression.lift(var)
|
|
77
|
+
found = integrate(f, x)
|
|
78
|
+
return found if generic || !complete?(found)
|
|
79
|
+
parameters = (f.variables & found.variables) - [x.name]
|
|
80
|
+
return found if parameters.empty?
|
|
81
|
+
branches = special_values(found, x, parameters).filter_map do |parameter, value, denominator|
|
|
82
|
+
next family_branch(f, x, parameter, value, denominator) if value.is_a?(ImageSet)
|
|
83
|
+
integrand = f.subs(parameter => value).simplify
|
|
84
|
+
# log(a*x) at a = 0 is log(0): no integrand, so no branch
|
|
85
|
+
next nil unless defined_value?(integrand)
|
|
86
|
+
# a value in the other parameters (a = b for sin(a*x)*cos(b*x))
|
|
87
|
+
# leaves an integrand with special values of its own; it has one
|
|
88
|
+
# parameter fewer, so this ends
|
|
89
|
+
at = value.variables.empty? ? integrate(integrand, x) : with_special_cases(integrand, x)
|
|
90
|
+
complete?(at) ? [Equation.new(parameter, value), at] : nil
|
|
91
|
+
end
|
|
92
|
+
branches.empty? ? found : Piecewise.new(branches + [[Piecewise::OTHERWISE, found]])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The same for a definite integral: cos(a*x) over 0..pi is
|
|
96
|
+
# sin(pi*a)/a, which has no value at a = 0, where the integral is pi.
|
|
97
|
+
# The special values are read off the answer, and each is integrated
|
|
98
|
+
# again with the bounds taken at it; a family of them stays one branch
|
|
99
|
+
# with the integral unevaluated, since there is no one integrand to
|
|
100
|
+
# integrate.
|
|
101
|
+
def definite_with_special_cases(expr, var, from, to, generic: false)
|
|
102
|
+
f = Expression.lift(expr)
|
|
103
|
+
x = Expression.lift(var)
|
|
104
|
+
found = definite(f, x, from, to)
|
|
105
|
+
return found if generic || !complete?(found)
|
|
106
|
+
bounds = [from, to].map { |b| Expression.lift(b) }
|
|
107
|
+
parameters = ((f.variables | bounds.flat_map(&:variables)) & found.variables) - [x.name]
|
|
108
|
+
return found if parameters.empty?
|
|
109
|
+
branches = special_values(found, x, parameters).filter_map do |parameter, value, denominator|
|
|
110
|
+
if value.is_a?(ImageSet)
|
|
111
|
+
condition = Equation.new(denominator, Num.new(0))
|
|
112
|
+
next [condition, Integral.new(f, x, *bounds)]
|
|
113
|
+
end
|
|
114
|
+
integrand = f.subs(parameter => value).simplify
|
|
115
|
+
next nil unless defined_value?(integrand)
|
|
116
|
+
lo, hi = bounds.map { |b| b.subs(parameter => value).simplify }
|
|
117
|
+
at = value.variables.empty? ? definite(integrand, x, lo, hi) : definite_with_special_cases(integrand, x, lo, hi)
|
|
118
|
+
complete?(at) ? [Equation.new(parameter, value), at] : nil
|
|
119
|
+
end
|
|
120
|
+
branches.empty? ? found : Piecewise.new(branches + [[Piecewise::OTHERWISE, found]])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Infinitely many special values - sin(a) = 0 for 1/(sin(a)*x + 1),
|
|
124
|
+
# where one period gave a = 0 and a = pi and a = 2*pi divided by zero
|
|
125
|
+
# (the sixth review's preflight) - are one branch, on the condition
|
|
126
|
+
# that the denominator vanishes. Its integrand is the one at a member
|
|
127
|
+
# with k an integer; when that still depends on k, the branch keeps
|
|
128
|
+
# the integral unevaluated rather than a wrong antiderivative.
|
|
129
|
+
def family_branch(f, x, parameter, family, denominator)
|
|
130
|
+
coeff, factors = Simplify.factorize(denominator)
|
|
131
|
+
condition = Equation.new(coeff.is_a?(Numeric) ? Simplify.rebuild_product(1, factors) : denominator, Num.new(0))
|
|
132
|
+
names = family.parameters.map(&:name)
|
|
133
|
+
integrand = atom_value(f, parameter, denominator)
|
|
134
|
+
unless integrand
|
|
135
|
+
bindings = family.parameters.to_h { |p| [p.name, family.domain] }
|
|
136
|
+
integrand = RCAS.assume(**bindings) { f.subs(parameter => family.expr).simplify }
|
|
137
|
+
end
|
|
138
|
+
# x/(exp(a) - 1) has no integrand where exp(a) = 1: no branch
|
|
139
|
+
return nil unless defined_value?(integrand)
|
|
140
|
+
at = integrate(integrand, x) if (integrand.variables & names).empty?
|
|
141
|
+
at = Integral.new(f, x) unless at && complete?(at) && (at.variables & names).empty?
|
|
142
|
+
[condition, at]
|
|
143
|
+
rescue ZeroDivisionError
|
|
144
|
+
nil
|
|
145
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
146
|
+
RCAS.guard!(rescued, refused: true)
|
|
147
|
+
[condition || Equation.new(denominator, Num.new(0)), Integral.new(f, x)]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The denominator is linear in one function of the parameter, cos(a) or
|
|
151
|
+
# exp(a): where it vanishes, that function has one value, and the
|
|
152
|
+
# integrand there is f with the function replaced by it - cos(a) = 0
|
|
153
|
+
# makes 1/(cos(a)*x + 1) the integrand 1, which no member pi/2 + pi*k
|
|
154
|
+
# shows as plainly. nil for any other denominator.
|
|
155
|
+
def atom_value(f, parameter, denominator)
|
|
156
|
+
atoms = denominator.each_node.select { |n| n.is_a?(Fn) && n.variables.include?(parameter.name) }.uniq
|
|
157
|
+
return nil unless atoms.size == 1
|
|
158
|
+
atom = atoms.first
|
|
159
|
+
t = Expression.fresh_variable(:t, denominator.variables | f.variables)
|
|
160
|
+
linear = denominator.subs(atom => t).simplify
|
|
161
|
+
return nil if linear.variables.include?(parameter.name)
|
|
162
|
+
values = Solve.solve(linear, t, principal: true)
|
|
163
|
+
return nil unless values.is_a?(Array) && values.size == 1 && values.first.is_a?(Expression) && values.first.variables.empty?
|
|
164
|
+
replaced = f.subs(atom => values.first).simplify
|
|
165
|
+
replaced.variables.include?(parameter.name) ? nil : replaced
|
|
166
|
+
rescue ZeroDivisionError
|
|
167
|
+
raise
|
|
168
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
169
|
+
RCAS.guard!(rescued, refused: true)
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# [parameter, value, denominator] at which a denominator of the
|
|
174
|
+
# antiderivative that is free of x vanishes, for one parameter at a
|
|
175
|
+
# time; the value is a point or a family. A denominator in several
|
|
176
|
+
# parameters - b - a and a + b for sin(a*x)*cos(b*x) - is solved for
|
|
177
|
+
# the first of them in which it has roots free of it, and the value is
|
|
178
|
+
# then an expression in the others (a = b, a = -b).
|
|
179
|
+
def special_values(found, x, parameters)
|
|
180
|
+
denominators = []
|
|
181
|
+
found.each_node do |node|
|
|
182
|
+
case node
|
|
183
|
+
when Div then denominators << node.right
|
|
184
|
+
when Pow then denominators << node.base if node.exponent.is_a?(Num) && node.exponent.value.is_a?(Numeric) && node.exponent.value.real? && node.exponent.value.negative?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
pairs = denominators.uniq.reject { |d| d.variables.include?(x.name) }.flat_map do |d|
|
|
188
|
+
movers = (d.variables & parameters).sort
|
|
189
|
+
next [] if movers.empty?
|
|
190
|
+
movers.lazy.map { |name| values_of(d, Var.new(name), movers - [name]) }.find(&:any?) || []
|
|
191
|
+
end
|
|
192
|
+
pairs.uniq { |p, v, _| [p, v] }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# [parameter, value, d] for the roots of d in the parameter that are
|
|
196
|
+
# free of it; a family only when d has no other parameter
|
|
197
|
+
def values_of(d, parameter, others)
|
|
198
|
+
# complete: one period of sin(a) = 0 left a = 2*pi dividing by zero
|
|
199
|
+
roots = begin
|
|
200
|
+
Solve.solve(d, parameter)
|
|
201
|
+
rescue ArgumentError, NotImplementedError, RCAS::Unsupported
|
|
202
|
+
return []
|
|
203
|
+
end
|
|
204
|
+
return [] unless roots.is_a?(Array)
|
|
205
|
+
points = roots.select { |r| r.is_a?(Expression) && (r.variables - others).empty? }.map { |r| [parameter, r.simplify, d] }
|
|
206
|
+
families = others.empty? ? roots.select { |r| r.is_a?(ImageSet) }.map { |r| [parameter, r, d] } : []
|
|
207
|
+
points + families
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def integrate(expr, var)
|
|
211
|
+
x = Expression.lift(var)
|
|
212
|
+
raise ArgumentError, "integration variable must be a symbol" unless x.is_a?(Var)
|
|
213
|
+
f = Piecewises.hoist(Expression.lift(expr).simplify)
|
|
214
|
+
return Piecewises.integrate(f, x) if f.is_a?(Piecewise)
|
|
215
|
+
constant, terms = Simplify.termize(f)
|
|
216
|
+
parts = []
|
|
217
|
+
parts << Num.new(constant) * x unless constant.zero?
|
|
218
|
+
terms.each do |factors, coeff|
|
|
219
|
+
term = Simplify.rebuild_product(coeff, factors)
|
|
220
|
+
parts << (attempt(term, x, 0) || Integral.new(term, x))
|
|
221
|
+
end
|
|
222
|
+
written = f.each_node.select { |n| n.is_a?(Fn) && n.name == :atan && n.args.first.is_a?(Fn) && n.args.first.name == :tan }
|
|
223
|
+
Substitutions.unwind(parts.reduce(Num.new(0)) { |a, b| a + b }, written).simplify
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Definite integral from a to b: F(b) - F(a), with limits at infinite or
|
|
227
|
+
# singular endpoints. Stays an Integral node when no antiderivative is found.
|
|
228
|
+
def definite(expr, var, from, to)
|
|
229
|
+
x = Expression.lift(var)
|
|
230
|
+
f = Expression.lift(expr)
|
|
231
|
+
from = Expression.lift(from)
|
|
232
|
+
to = Expression.lift(to)
|
|
233
|
+
f = Piecewises.hoist(f.simplify)
|
|
234
|
+
return Piecewises.definite(f, x, from, to) if f.is_a?(Piecewise)
|
|
235
|
+
split = split_at_kinks(f, x, from, to)
|
|
236
|
+
return split if split
|
|
237
|
+
antiderivative = integrate(f, x)
|
|
238
|
+
return Integral.new(f, x, from, to) unless complete?(antiderivative)
|
|
239
|
+
points = singular_points(f, x, from, to, antiderivative)
|
|
240
|
+
return Integral.new(f, x, from, to) if points.nil? # a pole we cannot place
|
|
241
|
+
bounds = [from, *points, to]
|
|
242
|
+
value = between(antiderivative, x, bounds)
|
|
243
|
+
# A logarithm of a negative number means the antiderivative has left
|
|
244
|
+
# the reals: log|u| is one too, on every interval that avoids u = 0,
|
|
245
|
+
# and it is the one a real integral wants (the constant differs per
|
|
246
|
+
# piece, which is exactly why the pieces are evaluated separately).
|
|
247
|
+
# Only where f is real on the whole range, though: 1/sqrt(x**2 - 1)
|
|
248
|
+
# over 0..1 is -i*pi/2, and log|u| made it 0. A range on which f is
|
|
249
|
+
# not real keeps its F(b) - F(a) only when that is not real either,
|
|
250
|
+
# and otherwise stays formal below - a principal antiderivative can
|
|
251
|
+
# cross its branch cut inside the range (asin(2) for
|
|
252
|
+
# 1/sqrt(1 - x**2) over 0..2 has the wrong sign of i).
|
|
253
|
+
if real_integrand?(f) && (value.nil? || !real_valued?(value)) && real_on_range?(f, x, bounds)
|
|
254
|
+
value = between(real_logs(antiderivative), x, bounds)
|
|
255
|
+
end
|
|
256
|
+
return Integral.new(f, x, from, to) if value.nil? || (real_integrand?(f) && !real_valued?(value))
|
|
257
|
+
value
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# f is real inside each piece between the bounds: every condition of
|
|
261
|
+
# its real domain - a log's argument positive, a root's radicand not
|
|
262
|
+
# negative, asin's argument within [-1, 1] - is proved on the piece
|
|
263
|
+
# (Analysis.sign_on_interval, which is a proof and not a sample). The
|
|
264
|
+
# conditions "a denominator is not 0" are the poles, and the pieces
|
|
265
|
+
# already end there.
|
|
266
|
+
def real_on_range?(f, x, bounds)
|
|
267
|
+
conditions = Analysis.domain_conditions(f, x).reject { |c| c.op == :!= }
|
|
268
|
+
bounds.each_cons(2).all? do |p, q|
|
|
269
|
+
conditions.all? do |c|
|
|
270
|
+
sign = Analysis.sign_on_interval((c.lhs - c.rhs).simplify, x, p, q)
|
|
271
|
+
%i[> >=].include?(c.op) ? sign == :positive : sign == :negative
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
275
|
+
RCAS.guard!(rescued, refused: true)
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# abs(u) and sign(u) with a u that is not linear (the piecewise rule
|
|
280
|
+
# takes the linear ones): the range is cut at every zero of u inside
|
|
281
|
+
# it - all of them, a family counted out member by member - and on
|
|
282
|
+
# each piece abs(u) is u or -u, decided at an interior point. This is
|
|
283
|
+
# what gives the solid of revolution of cos(16*pi*x) its volume 6
|
|
284
|
+
# (third review, T1a); without it the abs was either dropped, which
|
|
285
|
+
# made the volume 0, or left to the indefinite rules, which cannot.
|
|
286
|
+
# nil when there is nothing to split or the zeros cannot be counted.
|
|
287
|
+
def split_at_kinks(f, x, from, to)
|
|
288
|
+
kinks = f.each_node.select do |n|
|
|
289
|
+
n.is_a?(Fn) && %i[abs sign].include?(n.name) && n.args.size == 1 &&
|
|
290
|
+
n.args.first.variables.include?(x.name) && !linear(n.args.first, x)
|
|
291
|
+
end.uniq
|
|
292
|
+
return nil if kinks.empty?
|
|
293
|
+
a, b = real_number(from), real_number(to)
|
|
294
|
+
return nil if a.nil? || b.nil? || a == b
|
|
295
|
+
lo, hi = [a, b].minmax
|
|
296
|
+
points = []
|
|
297
|
+
budget = MAX_BREAKS
|
|
298
|
+
kinks.each do |kink|
|
|
299
|
+
roots = begin
|
|
300
|
+
Solve.solve(kink.args.first, x, domain: RR)
|
|
301
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
302
|
+
RCAS.guard!(rescued, refused: true)
|
|
303
|
+
return nil
|
|
304
|
+
end
|
|
305
|
+
return nil unless roots.is_a?(Array)
|
|
306
|
+
roots.each do |root|
|
|
307
|
+
members = root.is_a?(ImageSet) ? root.between(lo, hi, limit: budget) : [root]
|
|
308
|
+
return nil if members.nil?
|
|
309
|
+
budget -= members.size
|
|
310
|
+
return nil if budget.negative?
|
|
311
|
+
points.concat(members.select { |m| (v = real_number(m)) && v > lo && v < hi })
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
points = points.uniq.sort_by { |p| real_number(p) }
|
|
315
|
+
ends = [from, *points, to]
|
|
316
|
+
ends = [from, *points.reverse, to] if a > b
|
|
317
|
+
pieces = ends.each_cons(2).map do |p, q|
|
|
318
|
+
middle = ((p + q) / 2).simplify
|
|
319
|
+
replaced = kinks.to_h do |kink|
|
|
320
|
+
u = kink.args.first
|
|
321
|
+
sign = Decide.sign(u.subs(x => middle).simplify)
|
|
322
|
+
return nil unless %i[positive negative].include?(sign)
|
|
323
|
+
[kink, kink.name == :abs ? (sign == :positive ? u : -u) : Num.new(sign == :positive ? 1 : -1)]
|
|
324
|
+
end
|
|
325
|
+
value = definite(f.subs(replaced).simplify, x, p, q)
|
|
326
|
+
return nil if value.each_node.any? { |n| n.is_a?(Integral) }
|
|
327
|
+
value
|
|
328
|
+
end
|
|
329
|
+
pieces.reduce(:+).simplify
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# F(b) - F(a) over each piece, added up: a one-sided limit at every
|
|
333
|
+
# interior end, since that is where the integrand blows up. The sum of
|
|
334
|
+
# +oo and -oo is `undefined`, which is the honest answer for a divergent
|
|
335
|
+
# integral, and Simplify is what says so.
|
|
336
|
+
def between(antiderivative, x, bounds)
|
|
337
|
+
pieces = bounds.each_cons(2).map do |p, q|
|
|
338
|
+
up = ascending?(p, q) # bounds the other way round approach from the other side
|
|
339
|
+
upper = endpoint(antiderivative, x, q, up ? :left : :right)
|
|
340
|
+
lower = endpoint(antiderivative, x, p, up ? :right : :left)
|
|
341
|
+
return nil if upper.nil? || lower.nil?
|
|
342
|
+
upper - lower
|
|
343
|
+
end
|
|
344
|
+
pieces.reduce(:+).simplify
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def ascending?(p, q)
|
|
348
|
+
a, b = real_number(p), real_number(q)
|
|
349
|
+
a.nil? || b.nil? ? true : a <= b
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The points strictly between the bounds where f blows up. F(b) - F(a)
|
|
353
|
+
# is the answer only on an interval where f is continuous; without this,
|
|
354
|
+
# integrate(1/x**2, x, -1, 1) is -2, a negative area under a positive
|
|
355
|
+
# integrand. Rational poles come from the denominators Analysis already
|
|
356
|
+
# collects for `discuss`, and tan(u) from the zeros of cos(u).
|
|
357
|
+
# => [] (no interior singularity), the points, or nil (cannot tell).
|
|
358
|
+
def singular_points(f, x, from, to, antiderivative = nil)
|
|
359
|
+
candidates = Analysis.denominators(f, x).flat_map { |d| [d, *vanishing_factors(d, x)] }
|
|
360
|
+
f.each_node do |node|
|
|
361
|
+
next unless node.is_a?(Fn) && node.name == :tan && node.args.first.variables.include?(x.name)
|
|
362
|
+
candidates << Fn.new(:cos, [node.args.first])
|
|
363
|
+
end
|
|
364
|
+
a, b = real_number(from), real_number(to)
|
|
365
|
+
return [] if a.nil? || b.nil?
|
|
366
|
+
lo, hi = [a, b].minmax
|
|
367
|
+
jumps = antiderivative ? jump_points(antiderivative, x, lo, hi) : []
|
|
368
|
+
return nil if jumps.nil? # breaks rcas cannot enumerate
|
|
369
|
+
return order_points(jumps, a, b) if candidates.empty?
|
|
370
|
+
|
|
371
|
+
points = []
|
|
372
|
+
unsolved = []
|
|
373
|
+
budget = MAX_BREAKS
|
|
374
|
+
candidates.uniq.each do |d|
|
|
375
|
+
roots = begin
|
|
376
|
+
Solve.solve(d, x, domain: RR)
|
|
377
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
378
|
+
RCAS.guard!(rescued, refused: true)
|
|
379
|
+
nil
|
|
380
|
+
end
|
|
381
|
+
roots = nil unless roots.nil? || roots.is_a?(Array)
|
|
382
|
+
if roots.nil?
|
|
383
|
+
unsolved << d
|
|
384
|
+
next
|
|
385
|
+
end
|
|
386
|
+
roots.each do |root|
|
|
387
|
+
# every member of a family of poles, not one period of it: 1/sin(x)
|
|
388
|
+
# over 0..10 has poles at pi, 2*pi and 3*pi
|
|
389
|
+
members = root.is_a?(ImageSet) ? root.between(lo, hi, limit: budget) : [root]
|
|
390
|
+
return nil if members.nil?
|
|
391
|
+
budget -= members.size
|
|
392
|
+
# a pole whose place depends on a parameter may be inside or not;
|
|
393
|
+
# 1/(x - a)**2 over 0..1 diverges for a = 1/2, and the generic
|
|
394
|
+
# antiderivative said -4 there. Undecided is not "outside".
|
|
395
|
+
return nil if members.any? { |m| !m.variables.empty? && possibly_inside?(m, lo, hi) }
|
|
396
|
+
points.concat(members)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
known = points.filter_map { |p| real_number(p) }
|
|
400
|
+
# A denominator whose zeros rcas cannot name may still have one here,
|
|
401
|
+
# and "no pole" would be a claim rather than an answer.
|
|
402
|
+
return nil if unsolved.any? { |d| changes_sign_between?(d, x, lo, hi, known) }
|
|
403
|
+
|
|
404
|
+
inside = points.uniq.select { |p| (v = real_number(p)) && v > lo && v < hi }
|
|
405
|
+
kinds = inside.to_h { |p| [p, singularity(f, x, p)] }
|
|
406
|
+
return nil if kinds.value?(:unknown)
|
|
407
|
+
order_points(inside.select { |p| kinds[p] == :pole } + jumps, a, b)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# A symbolic pole is outside (lo, hi) only when the assumptions say so;
|
|
411
|
+
# a pole that is not real for any value of the parameters (a sum of
|
|
412
|
+
# squares' zeros, +-i*a with a declared real) is not inside either.
|
|
413
|
+
def possibly_inside?(m, lo, hi)
|
|
414
|
+
real = begin
|
|
415
|
+
Inequalities.real?(m)
|
|
416
|
+
rescue NotImplementedError, RCAS::Unsupported
|
|
417
|
+
nil
|
|
418
|
+
end
|
|
419
|
+
return false if real == false
|
|
420
|
+
imaginary = ComplexParts.im(m)
|
|
421
|
+
return false if (sign = RCAS.sign_of(imaginary)) && %i[positive negative].include?(sign)
|
|
422
|
+
below = RCAS.sign_of((m - Num.new(Rational(lo))).simplify) if lo.finite?
|
|
423
|
+
above = RCAS.sign_of((Num.new(Rational(hi)) - m).simplify) if hi.finite?
|
|
424
|
+
return false if %i[negative nonpositive].include?(below) || %i[negative nonpositive].include?(above)
|
|
425
|
+
true
|
|
426
|
+
rescue StandardError => rescued
|
|
427
|
+
RCAS.guard!(rescued)
|
|
428
|
+
true
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def order_points(points, a, b)
|
|
432
|
+
points = points.uniq.sort_by { |p| real_number(p) }
|
|
433
|
+
a > b ? points.reverse : points
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Where the antiderivative jumps although the integrand does not: the
|
|
437
|
+
# Weierstrass substitution puts tan(x/2) into F, which breaks at every
|
|
438
|
+
# odd multiple of pi while 1/(2 + cos(x)) is perfectly smooth there.
|
|
439
|
+
# F(b) - F(a) across such a break loses a whole period - the integral
|
|
440
|
+
# of 1/(2 + cos(x)) over 0..2*pi came out as 0 - and splitting there,
|
|
441
|
+
# with the one-sided limits `between` already takes, is the answer.
|
|
442
|
+
# => the points, or nil when the breaks cannot be enumerated.
|
|
443
|
+
#
|
|
444
|
+
# atan(p/q) breaks the same way where q vanishes: Lazard-Rioboo-Trager
|
|
445
|
+
# gives (x**2 + 1)/(x**4 + 1) the antiderivative
|
|
446
|
+
# atan((x**2 - 1)/(sqrt(2)*x))/sqrt(2), which jumps by pi/sqrt(2) at
|
|
447
|
+
# x = 0 where the integrand is smooth (third review, D2).
|
|
448
|
+
def jump_points(antiderivative, x, lo, hi)
|
|
449
|
+
candidates = antiderivative.each_node.flat_map do |node|
|
|
450
|
+
next [] unless node.is_a?(Fn) && node.args.first&.variables&.include?(x.name)
|
|
451
|
+
case node.name
|
|
452
|
+
when :tan then [Fn.new(:cos, [node.args.first])]
|
|
453
|
+
when :atan then Analysis.denominators(node.args.first, x)
|
|
454
|
+
else []
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
return [] if candidates.empty?
|
|
458
|
+
|
|
459
|
+
points = []
|
|
460
|
+
budget = MAX_BREAKS
|
|
461
|
+
candidates.uniq.each do |d|
|
|
462
|
+
roots = begin
|
|
463
|
+
Solve.solve(d, x, all: true, domain: RR)
|
|
464
|
+
rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
|
|
465
|
+
RCAS.guard!(rescued, refused: true)
|
|
466
|
+
nil
|
|
467
|
+
end
|
|
468
|
+
# Unsolvable: a break only matters if it is in there, and a cos
|
|
469
|
+
# that keeps its sign between the bounds has none.
|
|
470
|
+
if roots.nil?
|
|
471
|
+
return nil if changes_sign_between?(d, x, lo, hi, [])
|
|
472
|
+
next
|
|
473
|
+
end
|
|
474
|
+
roots.each do |root|
|
|
475
|
+
# a family of breaks that cannot be counted out is not "no breaks"
|
|
476
|
+
members = root.is_a?(ImageSet) ? root.between(lo, hi, limit: budget) : [root]
|
|
477
|
+
return nil if members.nil?
|
|
478
|
+
budget -= members.size
|
|
479
|
+
points.concat(members)
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
span = (hi - lo).abs
|
|
483
|
+
points.select { |p| (v = real_number(p)) && v > lo && v < hi }
|
|
484
|
+
.uniq.select { |p| jumps?(antiderivative, x, p, span) }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# More than this many breaks in the range and the integral is held
|
|
488
|
+
# rather than split: `ImageSet#between` answers nil past it, and nil is
|
|
489
|
+
# "cannot tell", not "none" - one family of two dropping out silently
|
|
490
|
+
# left integrate(1/(2 + cos(x)), x, 0, 254*PI) wrong by a factor of two.
|
|
491
|
+
# It is a budget for the whole range, spent family by family, not a
|
|
492
|
+
# limit per family: how many families the breaks arrive in is a matter
|
|
493
|
+
# of how `solve` spells them, and the bound has to mean the same either
|
|
494
|
+
# way. It did not, and the reach halved silently when `merge_families`
|
|
495
|
+
# landed and two families of step 4*pi became one of step 2*pi (20 Sept
|
|
496
|
+
# 2026, the tenth pass of the review). The number also decides how long
|
|
497
|
+
# the slowest allowed integral takes, since a break costs about 0.048 s
|
|
498
|
+
# to split at: 128 of them is some twelve seconds, which is the most
|
|
499
|
+
# this is willing to spend before holding the integral instead.
|
|
500
|
+
MAX_BREAKS = 128
|
|
501
|
+
|
|
502
|
+
# Do the values of F either side of the point disagree? Read off two
|
|
503
|
+
# samples rather than two limits, and deliberately one-sided the safe
|
|
504
|
+
# way: splitting where F is in fact continuous costs two evaluations
|
|
505
|
+
# and nothing else, because the pieces then telescope, while missing a
|
|
506
|
+
# break costs a whole period. Limits here cost about a tenth of a
|
|
507
|
+
# second each, and there is one candidate per half period.
|
|
508
|
+
def jumps?(f, x, point, span)
|
|
509
|
+
middle = real_number(point)
|
|
510
|
+
return true if middle.nil?
|
|
511
|
+
step = [span, 1.0].max * 1e-7
|
|
512
|
+
left = real_number(f.subs(x => Num.new(middle - step)))
|
|
513
|
+
right = real_number(f.subs(x => Num.new(middle + step)))
|
|
514
|
+
return true if left.nil? || right.nil?
|
|
515
|
+
(left - right).abs > 1e-6 * [1.0, left.abs, right.abs].max
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# log(0) and tan(pi/2) are not values: an endpoint that substitutes to
|
|
519
|
+
# one of them is answered with the one-sided limit instead.
|
|
520
|
+
def defined_value?(value)
|
|
521
|
+
value.each_node.none? do |node|
|
|
522
|
+
next true if node == UNDEFINED
|
|
523
|
+
next false unless node.is_a?(Fn)
|
|
524
|
+
case node.name
|
|
525
|
+
when :log then (arg = node.args.first).is_a?(Num) && arg.value.is_a?(Numeric) && arg.value.zero?
|
|
526
|
+
when :tan then Scalar.zero?(Functions.fold(Fn.new(:cos, [node.args.first])))
|
|
527
|
+
else false
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# The factors of a denominator: a product vanishes where any of them
|
|
533
|
+
# does, and Solve can often name the zeros of x and of log(x) when it
|
|
534
|
+
# can make nothing of x*log(x).
|
|
535
|
+
def vanishing_factors(d, x)
|
|
536
|
+
_, factors = Simplify.factorize(d)
|
|
537
|
+
return [] if factors.size < 2
|
|
538
|
+
factors.filter_map do |base, exponent|
|
|
539
|
+
next nil unless base.variables.include?(x.name)
|
|
540
|
+
next nil if exponent.is_a?(Numeric) && exponent.negative?
|
|
541
|
+
base
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
SIGN_SAMPLES = 32
|
|
546
|
+
|
|
547
|
+
# A sign change of d strictly between the bounds that none of the roots
|
|
548
|
+
# already found accounts for. One-sided, like every test of this kind
|
|
549
|
+
# here: it reports a zero it can see, never the absence of one.
|
|
550
|
+
def changes_sign_between?(d, x, lo, hi, known)
|
|
551
|
+
points = (0..SIGN_SAMPLES).map { |i| lo + (hi - lo) * i / SIGN_SAMPLES.to_f }
|
|
552
|
+
values = points.map { |t| real_number(d.subs(x => Num.new(t))) }
|
|
553
|
+
points.each_cons(2).with_index.any? do |(p, q), i|
|
|
554
|
+
u = values[i]
|
|
555
|
+
v = values[i + 1]
|
|
556
|
+
next false if u.nil? || v.nil?
|
|
557
|
+
next false unless u.zero? || v.zero? || (u.negative? != v.negative?)
|
|
558
|
+
known.none? { |r| r >= p && r <= q }
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# :pole (f runs away on at least one side), :finite (a removable gap the
|
|
563
|
+
# antiderivative sees through), or :unknown, where honesty means an
|
|
564
|
+
# unevaluated Integral rather than F(b) - F(a).
|
|
565
|
+
def singularity(f, x, point)
|
|
566
|
+
sides = %i[left right].map { |dir| Limits.limit(f, x, point, dir) }
|
|
567
|
+
return :pole if sides.any? { |v| Limits.infinite?(v) }
|
|
568
|
+
sides.any? { |v| v.is_a?(Limit) } ? :unknown : :finite
|
|
569
|
+
rescue StandardError => rescued
|
|
570
|
+
RCAS.guard!(rescued)
|
|
571
|
+
:unknown
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def real_number(expr) = RCAS.real_float(expr, finite: false)
|
|
575
|
+
|
|
576
|
+
# No imaginary unit, and no logarithm of a number that is not positive.
|
|
577
|
+
def real_valued?(expr)
|
|
578
|
+
expr.each_node.none? do |node|
|
|
579
|
+
if node.is_a?(Num)
|
|
580
|
+
node.value.is_a?(Complex) && !node.value.imaginary.zero?
|
|
581
|
+
elsif node.is_a?(Fn) && node.name == :log
|
|
582
|
+
# log(log(1/2)) is not real either, and its argument is not a Num
|
|
583
|
+
arg = node.args.first
|
|
584
|
+
arg.variables.empty? && (value = real_number(arg)) && !value.positive?
|
|
585
|
+
elsif node.is_a?(Fn) && %i[asin acos].include?(node.name)
|
|
586
|
+
# asin(2) is pi/2 + i*acosh(2), and nothing in the node says i
|
|
587
|
+
arg = node.args.first
|
|
588
|
+
arg.variables.empty? && Decide.sign((Fn.new(:abs, [arg]) - 1).simplify) == :positive
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def real_integrand?(f) = real_valued?(f) && f.each_node.none? { |n| n.is_a?(Num) && n.value.is_a?(Complex) }
|
|
594
|
+
|
|
595
|
+
# log(u) => log(abs(u)), the real antiderivative of u'/u.
|
|
596
|
+
def real_logs(expr)
|
|
597
|
+
return Fn.new(:log, [RCAS.abs(real_logs(expr.args.first))]) if expr.is_a?(Fn) && expr.name == :log
|
|
598
|
+
expr.map_children { |c| real_logs(c) }
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def endpoint(antiderivative, x, point, dir)
|
|
602
|
+
unless Limits.infinite?(point)
|
|
603
|
+
value = begin
|
|
604
|
+
antiderivative.subs(x => point).simplify
|
|
605
|
+
rescue ZeroDivisionError
|
|
606
|
+
nil
|
|
607
|
+
end
|
|
608
|
+
return value if value && defined_value?(value)
|
|
609
|
+
end
|
|
610
|
+
value = Limits.limit(antiderivative, x, point, dir)
|
|
611
|
+
value.is_a?(Limit) ? nil : value
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# => antiderivative or nil
|
|
615
|
+
def attempt(f, x, depth)
|
|
616
|
+
return nil if depth > MAX_DEPTH
|
|
617
|
+
f = f.simplify
|
|
618
|
+
return f * x unless depends?(f, x)
|
|
619
|
+
# A definite integral that still moves with x is an atom whose
|
|
620
|
+
# derivative is another integral (Leibniz), so every rule that
|
|
621
|
+
# differentiates would grow one more layer for ever. rcas has no rule
|
|
622
|
+
# for the iterated integral and says so by staying formal.
|
|
623
|
+
return nil if f.each_node.any? { |n| n.is_a?(Integral) && n.definite? && depends?(n, x) }
|
|
624
|
+
|
|
625
|
+
if f.is_a?(Add) || f.is_a?(Sub)
|
|
626
|
+
constant, terms = Simplify.termize(f)
|
|
627
|
+
total = Num.new(constant) * x
|
|
628
|
+
terms.each do |factors, coeff|
|
|
629
|
+
r = attempt(Simplify.rebuild_product(coeff, factors), x, depth)
|
|
630
|
+
return nil if r.nil?
|
|
631
|
+
total += r
|
|
632
|
+
end
|
|
633
|
+
return total.simplify
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
coeff, rest = split_constant(f, x)
|
|
637
|
+
unless Scalar.one?(coeff)
|
|
638
|
+
r = attempt(rest, x, depth)
|
|
639
|
+
return r && (coeff * r).simplify
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
result = begin
|
|
643
|
+
table(f, x) || trig_product(f, x, depth) || IntegralFunctions.antiderivative(f, x) || piecewise(f, x, depth) || rational(f, x) || substitution(f, x, depth) ||
|
|
644
|
+
by_parts(f, x, depth) || Substitutions.radical(f, x, depth) || Substitutions.gaussian(f, x) || heurisch(f, x) ||
|
|
645
|
+
Substitutions.root_of_linear(f, x, depth) || Substitutions.root_of_ratio(f, x, depth) ||
|
|
646
|
+
Substitutions.exponential(f, x, depth) ||
|
|
647
|
+
Substitutions.trigonometric(f, x, depth) || shift(f, x, depth)
|
|
648
|
+
rescue ArgumentError => e
|
|
649
|
+
raise unless e.message.start_with?(NO_DERIVATIVE) # floor(x), an unknown function
|
|
650
|
+
nil
|
|
651
|
+
end
|
|
652
|
+
result&.simplify
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# sin(u)*cos(v), cos(u)*cos(v), sin(u)*sin(v) with u, v linear in x:
|
|
656
|
+
# the product-to-sum formulas [AS64, 4.3.31-33] turn them into a sum the
|
|
657
|
+
# table integrates term by term. With a parameter in the arguments
|
|
658
|
+
# (sin(a*x)*cos(x)) the heuristic below needed a linear system in a and
|
|
659
|
+
# twelve seconds; this is the rule a course teaches (third review,
|
|
660
|
+
# section 5). nil for anything else.
|
|
661
|
+
def trig_product(f, x, depth)
|
|
662
|
+
_, factors = Simplify.factorize(f)
|
|
663
|
+
trig = factors.select { |base, _| base.is_a?(Fn) && %i[sin cos].include?(base.name) && depends?(base, x) }
|
|
664
|
+
return nil unless trig.size == 2 && trig.values.all?(1) && factors.size == 2
|
|
665
|
+
(first, _), (second, _) = trig.to_a
|
|
666
|
+
u = first.args.first
|
|
667
|
+
v = second.args.first
|
|
668
|
+
return nil unless linear(u, x) && linear(v, x)
|
|
669
|
+
coeff, = Simplify.factorize(f)
|
|
670
|
+
plus = (u + v).simplify
|
|
671
|
+
minus = (u - v).simplify
|
|
672
|
+
sum =
|
|
673
|
+
case [first.name, second.name]
|
|
674
|
+
when %i[sin cos] then Fn.new(:sin, [plus]) + Fn.new(:sin, [minus])
|
|
675
|
+
when %i[cos sin] then Fn.new(:sin, [plus]) - Fn.new(:sin, [minus])
|
|
676
|
+
when %i[cos cos] then Fn.new(:cos, [minus]) + Fn.new(:cos, [plus])
|
|
677
|
+
when %i[sin sin] then Fn.new(:cos, [minus]) - Fn.new(:cos, [plus])
|
|
678
|
+
end
|
|
679
|
+
r = attempt((Num.new(coeff) * sum / 2).simplify, x, depth + 1)
|
|
680
|
+
r && complete?(r) ? r : nil
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Cancel with each radical held as an atom, so that for example
|
|
684
|
+
# x*(1 + x/sqrt(1 + x**2))/(sqrt(1 + x**2) + x) collapses to x/sqrt(1 + x**2).
|
|
685
|
+
def atom_cancel(g, x)
|
|
686
|
+
roots = g.each_node.select do |n|
|
|
687
|
+
n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) &&
|
|
688
|
+
!n.exponent.value.integer? && depends?(n.base, x)
|
|
689
|
+
end.uniq
|
|
690
|
+
return g if roots.empty?
|
|
691
|
+
forward = roots.each_with_index.to_h { |r, i| [r, Var.new(:"_rad#{i}")] }
|
|
692
|
+
forward.invert.then { |back| g.subs(forward).cancel.subs(back) }
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Differentiating is how the rules look for substitutions, so an integrand
|
|
696
|
+
# with no derivative rule stays an unevaluated integral instead of raising.
|
|
697
|
+
NO_DERIVATIVE = "don't know the derivative"
|
|
698
|
+
|
|
699
|
+
def depends?(expr, x) = expr.variables.include?(x.name)
|
|
700
|
+
def complete?(expr) = expr.each_node.none? { |n| n.is_a?(Integral) }
|
|
701
|
+
|
|
702
|
+
# [constant factor, x-dependent factor]
|
|
703
|
+
def split_constant(f, x)
|
|
704
|
+
coeff, factors = Simplify.factorize(f)
|
|
705
|
+
const = factors.reject { |b, e| depends?(b, x) || (e.is_a?(Expression) && depends?(e, x)) }
|
|
706
|
+
return [Num.new(1), f] if const.empty? && coeff == 1
|
|
707
|
+
[Simplify.rebuild_product(coeff, const), Simplify.rebuild_product(1, factors.reject { |b, _| const.key?(b) })]
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# a*x + b => [a, b], else nil
|
|
711
|
+
def linear(u, x)
|
|
712
|
+
a = begin
|
|
713
|
+
u.diff(x)
|
|
714
|
+
rescue ArgumentError
|
|
715
|
+
return nil
|
|
716
|
+
end
|
|
717
|
+
return nil if depends?(a, x) || Scalar.zero?(a)
|
|
718
|
+
b = (u - a * x).simplify
|
|
719
|
+
return nil if depends?(b, x)
|
|
720
|
+
[a, b]
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# ---- layer 1: the table -------------------------------------------------
|
|
724
|
+
|
|
725
|
+
# f is a single factor base**exp (constants already split off).
|
|
726
|
+
def table(f, x)
|
|
727
|
+
coeff, factors = Simplify.factorize(f)
|
|
728
|
+
return nil unless coeff == 1 && factors.size == 1
|
|
729
|
+
base, exp = factors.first
|
|
730
|
+
if base == Simplify.exp_base
|
|
731
|
+
base = Fn.new(:exp, [Expression.lift(exp)])
|
|
732
|
+
exp = 1
|
|
733
|
+
end
|
|
734
|
+
exp_e = Expression.lift(exp)
|
|
735
|
+
|
|
736
|
+
if depends?(exp_e, x)
|
|
737
|
+
return nil if depends?(base, x)
|
|
738
|
+
ab = linear(exp_e, x) or return nil
|
|
739
|
+
return Simplify.power_node(base, exp) / (ab.first * Fn.new(:log, [base]))
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
if base == x
|
|
743
|
+
return exp == -1 ? Fn.new(:log, [x]) : x**(exp_e + 1) / (exp_e + 1)
|
|
744
|
+
elsif (ab = linear(base, x))
|
|
745
|
+
a = ab.first
|
|
746
|
+
return exp == -1 ? Fn.new(:log, [base]) / a : base**(exp_e + 1) / ((exp_e + 1) * a)
|
|
747
|
+
elsif base.is_a?(Fn) && base.args.size == 1 && TABLE_FUNCTIONS.include?(base.name) && (ab = linear(base.args.first, x))
|
|
748
|
+
u = base.args.first
|
|
749
|
+
a = ab.first
|
|
750
|
+
if exp == 1
|
|
751
|
+
case base.name
|
|
752
|
+
when :exp then base / a
|
|
753
|
+
when :sin then -Fn.new(:cos, [u]) / a
|
|
754
|
+
when :cos then Fn.new(:sin, [u]) / a
|
|
755
|
+
when :tan then -Fn.new(:log, [Fn.new(:cos, [u])]) / a
|
|
756
|
+
when :log then (u * base - u) / a
|
|
757
|
+
when :atan then (u * base - Fn.new(:log, [1 + u**2]) / 2) / a
|
|
758
|
+
when :sinh then Fn.new(:cosh, [u]) / a
|
|
759
|
+
when :cosh then Fn.new(:sinh, [u]) / a
|
|
760
|
+
end
|
|
761
|
+
elsif exp == -1
|
|
762
|
+
case base.name
|
|
763
|
+
when :cos then Fn.new(:log, [(1 + Fn.new(:sin, [u])) / base]) / a
|
|
764
|
+
when :sin then Fn.new(:log, [(1 - Fn.new(:cos, [u])) / base]) / a
|
|
765
|
+
when :cosh then Fn.new(:atan, [Fn.new(:sinh, [u])]) / a
|
|
766
|
+
when :sinh then Fn.new(:log, [(Fn.new(:cosh, [u]) - 1) / base]) / a
|
|
767
|
+
end
|
|
768
|
+
elsif exp == -2
|
|
769
|
+
case base.name
|
|
770
|
+
when :cos then Fn.new(:tan, [u]) / a
|
|
771
|
+
when :sin then -Fn.new(:cos, [u]) / (Fn.new(:sin, [u]) * a)
|
|
772
|
+
when :cosh then Fn.new(:sinh, [u]) / (Fn.new(:cosh, [u]) * a)
|
|
773
|
+
when :sinh then -Fn.new(:cosh, [u]) / (Fn.new(:sinh, [u]) * a)
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# ---- layer 1: derivative-divides ------------------------------------------
|
|
780
|
+
|
|
781
|
+
def substitution(f, x, depth)
|
|
782
|
+
candidates = f.each_node.select { |n| !n.equal?(f) && !n.is_a?(Var) && !n.is_a?(Num) && depends?(n, x) }.uniq
|
|
783
|
+
candidates = candidates.sort_by { |n| -n.each_node.count }.first(12)
|
|
784
|
+
t = Var.new(:"_u#{depth}")
|
|
785
|
+
candidates.each do |u|
|
|
786
|
+
du = u.diff(x)
|
|
787
|
+
next if Scalar.zero?(du)
|
|
788
|
+
g = (f / du).simplify.subs(u => t).simplify
|
|
789
|
+
next if depends?(g, x)
|
|
790
|
+
r = attempt(g, t, depth + 1)
|
|
791
|
+
return r.subs(t => u).simplify if r && complete?(r)
|
|
792
|
+
end
|
|
793
|
+
nil
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# ---- layer 1: integration by parts ----------------------------------------
|
|
797
|
+
|
|
798
|
+
# Functions that get simpler when differentiated, so u in u*dv.
|
|
799
|
+
BY_PARTS = %i[log atan asin acos erf erfc].freeze
|
|
800
|
+
|
|
801
|
+
def by_parts(f, x, depth)
|
|
802
|
+
_, factors = Simplify.factorize(f)
|
|
803
|
+
parts = factors.map { |b, e| Simplify.power_node(b, e) }
|
|
804
|
+
u = parts.find { |g| g.is_a?(Fn) && BY_PARTS.include?(g.name) } ||
|
|
805
|
+
parts.find { |g| g.is_a?(Pow) && g.base.is_a?(Fn) && BY_PARTS.include?(g.base.name) && g.exponent.is_a?(Num) && g.exponent.integer? && g.exponent.value.positive? }
|
|
806
|
+
if u.nil?
|
|
807
|
+
u = parts.find { |g| polynomial_in?(g, x) }
|
|
808
|
+
rest = parts - [u]
|
|
809
|
+
return nil if u.nil? || rest.size != 1
|
|
810
|
+
return nil unless (g = rest.first) && ((g.is_a?(Fn) && %i[exp sin cos sinh cosh].include?(g.name)) || (g.is_a?(Pow) && !depends?(g.base, x)))
|
|
811
|
+
end
|
|
812
|
+
dv = Simplify.product_node(parts - [u])
|
|
813
|
+
v = attempt(dv, x, depth + 1)
|
|
814
|
+
return nil unless v && complete?(v) && !harder?(v, dv)
|
|
815
|
+
rest = integrate_forms((u.diff(x) * v).simplify, x, depth + 1)
|
|
816
|
+
return nil unless rest
|
|
817
|
+
(u * v - rest).simplify
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# v brings in a function dv did not have, so integrating u' * v would be a
|
|
821
|
+
# step backwards (x**2*exp(-x**2): v = erf, and u'*v is the original problem).
|
|
822
|
+
SPECIAL = %i[erf erfc Ei Si Ci li].freeze
|
|
823
|
+
|
|
824
|
+
def harder?(v, dv)
|
|
825
|
+
names = ->(e) { e.each_node.filter_map { |n| n.name if n.is_a?(Fn) } }
|
|
826
|
+
(names.call(v) & SPECIAL).any? && (names.call(dv) & SPECIAL).empty?
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# u' * v often needs a normal form before it can be integrated: a common
|
|
830
|
+
# denominator for u = atan(1/x), a rationalized one for u = log(x + sqrt(x**2 + 1)).
|
|
831
|
+
def integrate_forms(g, x, depth)
|
|
832
|
+
seen = []
|
|
833
|
+
[-> { g }, -> { g.cancel }, -> { atom_cancel(g, x) }, -> { g.rationalize.cancel }].each do |form|
|
|
834
|
+
h = form.call.simplify
|
|
835
|
+
next if seen.include?(h)
|
|
836
|
+
seen << h
|
|
837
|
+
r = attempt(h, x, depth)
|
|
838
|
+
return r if r && complete?(r)
|
|
839
|
+
rescue ArgumentError, DomainError, ZeroDivisionError
|
|
840
|
+
next
|
|
841
|
+
end
|
|
842
|
+
nil
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# ---- layer 1: absolute values and signs -------------------------------------
|
|
846
|
+
|
|
847
|
+
# |u| is u*sign(u), and sign(u) is constant on each side of the root of u,
|
|
848
|
+
# so it can be treated as a constant factor: with g = A(x) + B(x)*sign(u)
|
|
849
|
+
# the integral is INT A + sign(u)*(F - F(x0)), F the integral of B and x0
|
|
850
|
+
# the root of u. The constant makes the antiderivative continuous there,
|
|
851
|
+
# which is what a definite integral across the root needs. Only a linear u
|
|
852
|
+
# (one sign change, at a point we can name) is handled. [Zor15, ch. 6]
|
|
853
|
+
def piecewise(f, x, depth)
|
|
854
|
+
nodes = f.each_node.select { |n| n.is_a?(Fn) && %i[abs sign].include?(n.name) && depends?(n.args.first, x) }.uniq
|
|
855
|
+
return nil if nodes.empty?
|
|
856
|
+
u = nodes.first.args.first
|
|
857
|
+
return nil unless nodes.all? { |n| n.args.first == u }
|
|
858
|
+
a, b = linear(u, x)
|
|
859
|
+
return nil if a.nil?
|
|
860
|
+
|
|
861
|
+
sgn = Var.new(:"_sg#{depth}")
|
|
862
|
+
g = f.subs(nodes.to_h { |n| [n, n.name == :abs ? (u * sgn) : sgn] })
|
|
863
|
+
plus = g.subs(sgn => Num.new(1)).simplify
|
|
864
|
+
minus = g.subs(sgn => Num.new(-1)).simplify
|
|
865
|
+
even = ((plus + minus) / 2).simplify # sign(u)**2 == 1
|
|
866
|
+
odd = ((plus - minus) / 2).simplify
|
|
867
|
+
|
|
868
|
+
total = Num.new(0)
|
|
869
|
+
unless Scalar.zero?(even)
|
|
870
|
+
r = attempt(even, x, depth + 1)
|
|
871
|
+
return nil unless r && complete?(r)
|
|
872
|
+
total += r
|
|
873
|
+
end
|
|
874
|
+
return total.simplify if Scalar.zero?(odd)
|
|
875
|
+
|
|
876
|
+
r = attempt(odd, x, depth + 1)
|
|
877
|
+
return nil unless r && complete?(r)
|
|
878
|
+
root = (-b / a).simplify
|
|
879
|
+
value = begin
|
|
880
|
+
r.subs(x => root).simplify
|
|
881
|
+
rescue StandardError => rescued
|
|
882
|
+
RCAS.guard!(rescued)
|
|
883
|
+
return nil
|
|
884
|
+
end
|
|
885
|
+
return nil if value.each_node.any? { |n| n.is_a?(Num) && !n.value.finite? } || depends?(value, x)
|
|
886
|
+
(total + Fn.new(:sign, [u]) * (r - value)).simplify
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
# Substitute x = (v - b)/a for a linear sub-expression a*x + b, so that
|
|
890
|
+
# e.g. x*exp(x)/(x + 1)**2 becomes a problem in v = x + 1 alone.
|
|
891
|
+
def shift(f, x, depth)
|
|
892
|
+
candidates = f.each_node.select { |n| (n.is_a?(Add) || n.is_a?(Sub)) && (ab = linear(n, x)) && !Scalar.zero?(ab.last) }.uniq
|
|
893
|
+
candidates.first(3).each do |u|
|
|
894
|
+
a, b = linear(u, x)
|
|
895
|
+
v = Var.new(:"_v#{depth}")
|
|
896
|
+
g = f.subs(x => (v - b) / a).simplify
|
|
897
|
+
r = attempt(g, v, depth + 1)
|
|
898
|
+
return (r.subs(v => u) / a).simplify if r && complete?(r)
|
|
899
|
+
end
|
|
900
|
+
nil
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def polynomial_in?(g, x)
|
|
904
|
+
_, table = Expand.table(g)
|
|
905
|
+
table.each_key.all? do |factors|
|
|
906
|
+
factors.all? do |b, e|
|
|
907
|
+
next false if e.is_a?(Expression) && depends?(e, x)
|
|
908
|
+
depends?(b, x) ? (b == x && e.is_a?(Integer) && e >= 0) : true
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
rescue ArgumentError
|
|
912
|
+
false
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# ---- layer 2: rational functions --------------------------------------------
|
|
916
|
+
|
|
917
|
+
def rational(f, x)
|
|
918
|
+
pair = as_rational(f, x) or return nil
|
|
919
|
+
rational_integrate(*pair, x)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# f as num/den in QQ[x], or nil when f is not a rational function of x
|
|
923
|
+
# with rational coefficients.
|
|
924
|
+
def as_rational(f, x)
|
|
925
|
+
ring = QQ[x.name]
|
|
926
|
+
constant, table = Expand.table(f)
|
|
927
|
+
return nil unless exact?(constant)
|
|
928
|
+
num = ring.call(constant)
|
|
929
|
+
den = ring.one
|
|
930
|
+
table.each do |factors, coeff|
|
|
931
|
+
return nil unless exact?(coeff)
|
|
932
|
+
n = ring.call(coeff)
|
|
933
|
+
d = ring.one
|
|
934
|
+
factors.each do |base, exp|
|
|
935
|
+
return nil unless exp.is_a?(Integer)
|
|
936
|
+
poly = begin
|
|
937
|
+
ring.call(base)
|
|
938
|
+
rescue DomainError
|
|
939
|
+
return nil
|
|
940
|
+
end
|
|
941
|
+
exp.positive? ? n *= poly**exp : d *= poly**(-exp)
|
|
942
|
+
end
|
|
943
|
+
num = num * d + n * den
|
|
944
|
+
den *= d
|
|
945
|
+
end
|
|
946
|
+
g = num.gcd(den)
|
|
947
|
+
num = num.exact_div(g)
|
|
948
|
+
den = den.exact_div(g)
|
|
949
|
+
lc = den.leading_coefficient
|
|
950
|
+
[num * Scalar.div(Num.new(1), lc), den.monic]
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def exact?(v) = v.is_a?(Integer) || v.is_a?(Rational)
|
|
954
|
+
|
|
955
|
+
def rational_integrate(num, den, x)
|
|
956
|
+
q, r = num.divmod(den)
|
|
957
|
+
result = q.integrate.to_expr
|
|
958
|
+
return result if r.zero?
|
|
959
|
+
|
|
960
|
+
g, a, dstar = hermite(r, den)
|
|
961
|
+
result += g
|
|
962
|
+
return result.simplify if a.zero?
|
|
963
|
+
|
|
964
|
+
q2, a = a.divmod(dstar)
|
|
965
|
+
result += q2.integrate.to_expr
|
|
966
|
+
return result.simplify if a.zero?
|
|
967
|
+
|
|
968
|
+
common = a.gcd(dstar)
|
|
969
|
+
a = a.exact_div(common)
|
|
970
|
+
dstar = dstar.exact_div(common)
|
|
971
|
+
logs = log_part(a, dstar, x) || real_log_part(a, dstar, x)
|
|
972
|
+
result += logs || Integral.new((a.to_expr / dstar.to_expr).simplify, x)
|
|
973
|
+
result.simplify
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# ---- layer 2: real quadratic factors ----------------------------------------
|
|
977
|
+
|
|
978
|
+
# What Lazard-Rioboo-Trager leaves when a root of the Rothstein-Trager
|
|
979
|
+
# resultant has degree higher than two: split a/d into partial fractions
|
|
980
|
+
# over the irreducible factors of d and integrate each one. Factors of
|
|
981
|
+
# degree at most two go back to the resultant method; a biquadratic
|
|
982
|
+
# x**4 + a*x**2 + b is split into the real quadratics the textbook uses,
|
|
983
|
+
# (x**2 + s*x + t)(x**2 - s*x + t) with t = sqrt(b) and s = sqrt(2*t - a).
|
|
984
|
+
# That is the classical decomposition into real factors [Har16, ch. II],
|
|
985
|
+
# and it is what makes 1/(x**4 + 1) come out in logs and arc tangents.
|
|
986
|
+
def real_log_part(a, d, x)
|
|
987
|
+
pieces = partial_fractions(a, d) or return nil
|
|
988
|
+
total = Num.new(0)
|
|
989
|
+
pieces.each do |n, f|
|
|
990
|
+
part = f.degree <= 2 ? log_part(n, f, x) : biquadratic_logs(n, f, x)
|
|
991
|
+
return nil if part.nil?
|
|
992
|
+
total += part
|
|
993
|
+
end
|
|
994
|
+
total.simplify
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
# a/d with d squarefree => [[numerator, irreducible factor], ...], from
|
|
998
|
+
# n_i = a * (d/f_i)**(-1) mod f_i (the factors are pairwise coprime).
|
|
999
|
+
def partial_fractions(a, d)
|
|
1000
|
+
factors = d.factor.factors.map { |f, _| f }
|
|
1001
|
+
return nil if factors.size < 2 && d.degree < 3
|
|
1002
|
+
factors.map do |f|
|
|
1003
|
+
rest = d.exact_div(f)
|
|
1004
|
+
g, s, = rest.xgcd(f)
|
|
1005
|
+
return nil unless g.constant?
|
|
1006
|
+
[((a * s) % f) * Scalar.div(Num.new(1), g.leading_coefficient), f]
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# n/(x**4 + a*x**2 + b), the quartic irreducible over QQ: with the real
|
|
1011
|
+
# split above, n/(q+ * q-) = (A*x + B)/q+ + (C*x + D)/q- and each piece is
|
|
1012
|
+
# a logarithm plus an arc tangent.
|
|
1013
|
+
def biquadratic_logs(n, f, x)
|
|
1014
|
+
cs = Solve.polynomial_coefficients(f.to_expr, x)
|
|
1015
|
+
return nil unless cs && cs.size == 5 && Scalar.zero?(cs[1]) && Scalar.zero?(cs[3]) && Scalar.one?(cs[4])
|
|
1016
|
+
b = cs[0]
|
|
1017
|
+
a = cs[2]
|
|
1018
|
+
ns = Solve.polynomial_coefficients(n.to_expr, x) or return nil
|
|
1019
|
+
n0, n1, n2, n3 = Array.new(4) { |i| ns[i] || Num.new(0) }
|
|
1020
|
+
disc = (a**2 - 4 * b).simplify
|
|
1021
|
+
return even_split(n0, n1, n2, n3, a, disc, x) if positive?(disc)
|
|
1022
|
+
t = RCAS.sqrt(b)
|
|
1023
|
+
s2 = (2 * t - a).simplify
|
|
1024
|
+
w2 = (2 * t + a).simplify
|
|
1025
|
+
return nil unless positive?(s2) && positive?(w2)
|
|
1026
|
+
sq = RCAS.sqrt(s2)
|
|
1027
|
+
w = RCAS.sqrt(w2)
|
|
1028
|
+
|
|
1029
|
+
k = ((n1 - t * n3) / sq).simplify # D - B
|
|
1030
|
+
m = (n0 / t).simplify # B + D
|
|
1031
|
+
j = ((n2 - m) / sq).simplify # C - A
|
|
1032
|
+
bb = ((m - k) / 2).simplify
|
|
1033
|
+
dd = ((m + k) / 2).simplify
|
|
1034
|
+
aa = ((n3 - j) / 2).simplify
|
|
1035
|
+
cc = ((n3 + j) / 2).simplify
|
|
1036
|
+
|
|
1037
|
+
qplus = (x**2 + sq * x + t).simplify
|
|
1038
|
+
qminus = (x**2 - sq * x + t).simplify
|
|
1039
|
+
(aa / 2 * Fn.new(:log, [qplus]) + (2 * bb - aa * sq) / w * Fn.new(:atan, [((2 * x + sq) / w).simplify]) +
|
|
1040
|
+
cc / 2 * Fn.new(:log, [qminus]) + (2 * dd + cc * sq) / w * Fn.new(:atan, [((2 * x - sq) / w).simplify])).simplify
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
# The other real split of x**4 + a*x**2 + b: when a**2 - 4*b is positive the
|
|
1044
|
+
# quartic is (x**2 + p)*(x**2 + q) with p, q = (a +- sqrt(a**2 - 4*b))/2,
|
|
1045
|
+
# both irrational (a rational pair would have factored over QQ already).
|
|
1046
|
+
def even_split(n0, n1, n2, n3, a, disc, x)
|
|
1047
|
+
r = RCAS.sqrt(disc)
|
|
1048
|
+
p = ((a + r) / 2).simplify
|
|
1049
|
+
q = ((a - r) / 2).simplify
|
|
1050
|
+
gap = (q - p).simplify
|
|
1051
|
+
return nil if Scalar.zero?(gap)
|
|
1052
|
+
aa = ((n1 - p * n3) / gap).simplify
|
|
1053
|
+
bb = ((n0 - p * n2) / gap).simplify
|
|
1054
|
+
first = quadratic_log(aa, bb, p, x) or return nil
|
|
1055
|
+
second = quadratic_log((n3 - aa).simplify, (n2 - bb).simplify, q, x) or return nil
|
|
1056
|
+
(first + second).simplify
|
|
1057
|
+
end
|
|
1058
|
+
|
|
1059
|
+
# INT (A*x + B)/(x**2 + c) dx, an arc tangent for c > 0 and a logarithm
|
|
1060
|
+
# for c < 0 (where x**2 + c has the two real roots +-sqrt(-c)).
|
|
1061
|
+
def quadratic_log(aa, bb, c, x)
|
|
1062
|
+
log = (aa / 2 * Fn.new(:log, [(x**2 + c).simplify])).simplify
|
|
1063
|
+
return log if Scalar.zero?(bb)
|
|
1064
|
+
if positive?(c)
|
|
1065
|
+
root = RCAS.sqrt(c)
|
|
1066
|
+
(log + bb / root * Fn.new(:atan, [(x / root).simplify])).simplify
|
|
1067
|
+
elsif positive?((-c).simplify)
|
|
1068
|
+
m = RCAS.sqrt((-c).simplify)
|
|
1069
|
+
(log + bb / (2 * m) * (Fn.new(:log, [(x - m).simplify]) - Fn.new(:log, [(x + m).simplify]))).simplify
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# A constant expression that is definitely positive (radicals included).
|
|
1074
|
+
# Decided, not read off a Float against 1e-12 (Decide): the branch of
|
|
1075
|
+
# the real quadratic factors depends on this sign.
|
|
1076
|
+
def positive?(e) = Decide.sign(Expression.lift(e).simplify) == :positive
|
|
1077
|
+
|
|
1078
|
+
# Mack's linear Hermite reduction: a/d = g' + a2/d* with d* squarefree.
|
|
1079
|
+
def hermite(a, d)
|
|
1080
|
+
g = Num.new(0)
|
|
1081
|
+
dminus = d.gcd(d.derivative)
|
|
1082
|
+
dstar = d.exact_div(dminus)
|
|
1083
|
+
while dminus.degree.positive?
|
|
1084
|
+
dminus2 = dminus.gcd(dminus.derivative)
|
|
1085
|
+
dminusstar = dminus.exact_div(dminus2)
|
|
1086
|
+
lhs = -(dstar * dminus.derivative).exact_div(dminus)
|
|
1087
|
+
gcd, s, = lhs.xgcd(dminusstar)
|
|
1088
|
+
raise "Hermite reduction: unexpected common factor" unless gcd.constant?
|
|
1089
|
+
b = (a * s) % dminusstar
|
|
1090
|
+
c = (a - b * lhs).exact_div(dminusstar)
|
|
1091
|
+
a = c - b.derivative * dstar.exact_div(dminusstar)
|
|
1092
|
+
g += b.to_expr / dminus.to_expr
|
|
1093
|
+
dminus = dminus2
|
|
1094
|
+
end
|
|
1095
|
+
[g, a, dstar]
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
# Logarithmic part of a/d (d squarefree, deg a < deg d, gcd(a, d) = 1):
|
|
1099
|
+
# sum over the roots c of the Rothstein-Trager resultant of
|
|
1100
|
+
# c * log(gcd(a - c*d', d)). Roots of degree 1 give plain logs, complex
|
|
1101
|
+
# conjugate pairs give log + atan; roots of higher degree return nil.
|
|
1102
|
+
def log_part(a, d, x)
|
|
1103
|
+
t = Var.new(:_t)
|
|
1104
|
+
ring2 = QQ[x.name, :_t]
|
|
1105
|
+
tring = QQ[:_t]
|
|
1106
|
+
dp = ring2.call(d.to_expr)
|
|
1107
|
+
ap = ring2.call(a.to_expr) - ring2.call(t) * ring2.call(d.derivative.to_expr)
|
|
1108
|
+
|
|
1109
|
+
resultant = sylvester_resultant(dp, ap, x, tring)
|
|
1110
|
+
return nil if resultant.degree(:_t) < 1
|
|
1111
|
+
|
|
1112
|
+
dprime = d.derivative
|
|
1113
|
+
total = Num.new(0)
|
|
1114
|
+
resultant.factor.factors.map(&:first).each do |ri|
|
|
1115
|
+
if ri.degree == 1
|
|
1116
|
+
c = Rational(-ri.coeff(0).value, ri.coeff(1).value)
|
|
1117
|
+
v = (a - dprime * c).gcd(d)
|
|
1118
|
+
next if v.constant?
|
|
1119
|
+
total += Num.new(c) * Fn.new(:log, [pretty(v)])
|
|
1120
|
+
else
|
|
1121
|
+
v = NumberFieldGcd.new(ri, tring).gcd(a, dprime, d)
|
|
1122
|
+
next if v.size <= 1
|
|
1123
|
+
return nil unless ri.degree == 2
|
|
1124
|
+
xring = QQ[x.name]
|
|
1125
|
+
p0 = xring.zero
|
|
1126
|
+
p1 = xring.zero
|
|
1127
|
+
v.each_with_index do |e, k|
|
|
1128
|
+
mono = xring.call(Var.new(x.name)**k)
|
|
1129
|
+
p0 += mono * e.coeff(0).value
|
|
1130
|
+
p1 += mono * e.coeff(1).value
|
|
1131
|
+
end
|
|
1132
|
+
total += quadratic_logs(ri, p0, p1)
|
|
1133
|
+
end
|
|
1134
|
+
end
|
|
1135
|
+
total
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
# gcd(a - θ*d', d) in K[x] for K = QQ[t]/(ri), θ the class of t.
|
|
1139
|
+
# Polynomials over K are arrays of K elements (QQ[t] polynomials reduced
|
|
1140
|
+
# modulo ri), index = degree in x.
|
|
1141
|
+
# res_x(p, q) for p, q in QQ[x, t], as an element of QQ[t], by
|
|
1142
|
+
# fraction-free (Bareiss) elimination on the Sylvester matrix.
|
|
1143
|
+
def sylvester_resultant(p, q, x, tring)
|
|
1144
|
+
m = p.degree(x.name)
|
|
1145
|
+
n = q.degree(x.name)
|
|
1146
|
+
pc = (0..m).map { |k| tring.call(p.coefficient_in(x.name, k).to_expr) }
|
|
1147
|
+
qc = (0..n).map { |k| tring.call(q.coefficient_in(x.name, k).to_expr) }
|
|
1148
|
+
size = m + n
|
|
1149
|
+
rows = []
|
|
1150
|
+
n.times { |i| rows << Array.new(size) { |j| (j - i).between?(0, m) ? pc[m - (j - i)] : tring.zero } }
|
|
1151
|
+
m.times { |i| rows << Array.new(size) { |j| (j - i).between?(0, n) ? qc[n - (j - i)] : tring.zero } }
|
|
1152
|
+
bareiss(rows, tring)
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def bareiss(rows, ring)
|
|
1156
|
+
n = rows.size
|
|
1157
|
+
m = rows.map(&:dup)
|
|
1158
|
+
prev = ring.one
|
|
1159
|
+
sign = 1
|
|
1160
|
+
(0...n - 1).each do |k|
|
|
1161
|
+
if m[k][k].zero?
|
|
1162
|
+
swap = (k + 1...n).find { |i| !m[i][k].zero? }
|
|
1163
|
+
return ring.zero unless swap
|
|
1164
|
+
m[k], m[swap] = m[swap], m[k]
|
|
1165
|
+
sign = -sign
|
|
1166
|
+
end
|
|
1167
|
+
(k + 1...n).each do |i|
|
|
1168
|
+
(k + 1...n).each do |j|
|
|
1169
|
+
m[i][j] = (m[i][j] * m[k][k] - m[i][k] * m[k][j]).exact_div(prev)
|
|
1170
|
+
end
|
|
1171
|
+
end
|
|
1172
|
+
prev = m[k][k]
|
|
1173
|
+
end
|
|
1174
|
+
m[n - 1][n - 1] * sign
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
class NumberFieldGcd
|
|
1178
|
+
def initialize(ri, tring)
|
|
1179
|
+
@ri = ri
|
|
1180
|
+
@tring = tring
|
|
1181
|
+
@t = tring.call(Var.new(:_t))
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
# => monic gcd as an array of K elements
|
|
1185
|
+
def gcd(a, dprime, d)
|
|
1186
|
+
f = (0..[a.degree, dprime.degree].max).map do |k|
|
|
1187
|
+
(@tring.call(a.coeff(k).value) - @t * @tring.call(dprime.coeff(k).value)) % @ri
|
|
1188
|
+
end
|
|
1189
|
+
g = (0..d.degree).map { |k| @tring.call(d.coeff(k).value) }
|
|
1190
|
+
f = trim(f)
|
|
1191
|
+
g = trim(g)
|
|
1192
|
+
f, g = g, rem(f, g) until g.empty?
|
|
1193
|
+
monic(f)
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
private
|
|
1197
|
+
|
|
1198
|
+
def trim(p)
|
|
1199
|
+
p = p.dup
|
|
1200
|
+
p.pop while !p.empty? && p.last.zero?
|
|
1201
|
+
p
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
def inv(e)
|
|
1205
|
+
g, s, = e.xgcd(@ri)
|
|
1206
|
+
raise DomainError, "#{e} is not invertible mod #{@ri}" unless g.constant? && !g.zero?
|
|
1207
|
+
(s * Scalar.div(Num.new(1), g.constant_term)) % @ri
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
def monic(p)
|
|
1211
|
+
return p if p.empty?
|
|
1212
|
+
i = inv(p.last)
|
|
1213
|
+
p.map { |e| (e * i) % @ri }
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
def rem(f, g)
|
|
1217
|
+
r = f.dup
|
|
1218
|
+
i = inv(g.last)
|
|
1219
|
+
while r.size >= g.size && !r.empty?
|
|
1220
|
+
c = (r.last * i) % @ri
|
|
1221
|
+
shift = r.size - g.size
|
|
1222
|
+
g.each_with_index { |ge, k| r[shift + k] -= c * ge }
|
|
1223
|
+
r = trim(r.map { |e| e % @ri })
|
|
1224
|
+
end
|
|
1225
|
+
r
|
|
1226
|
+
end
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
# Sum over the two roots c of A t^2 + B t + C of c*log(p0 + c*p1), written
|
|
1230
|
+
# with real logs and atan when the roots are complex.
|
|
1231
|
+
def quadratic_logs(ri, p0, p1)
|
|
1232
|
+
qa, qb, qc = ri.coeff(2).value, ri.coeff(1).value, ri.coeff(0).value
|
|
1233
|
+
a0 = Rational(-qb, 2 * qa)
|
|
1234
|
+
disc = Rational(qb * qb - 4 * qa * qc)
|
|
1235
|
+
p = p0 + p1 * a0
|
|
1236
|
+
scale = [p, p1].map { |q| q.terms.values.map { |c| c.value.is_a?(Rational) ? c.value.denominator : 1 }.reduce(1, :lcm) }.reduce(1, :lcm)
|
|
1237
|
+
p *= scale
|
|
1238
|
+
p1 *= scale
|
|
1239
|
+
common = Polynomial.rational_gcd(p.content, p1.content)
|
|
1240
|
+
unless common.zero? || common == 1
|
|
1241
|
+
p *= Rational(1, common)
|
|
1242
|
+
p1 *= Rational(1, common)
|
|
1243
|
+
end
|
|
1244
|
+
if disc.negative?
|
|
1245
|
+
b = RCAS.sqrt(Num.new(-disc)) / (2 * qa)
|
|
1246
|
+
b2 = -disc / (4 * qa * qa)
|
|
1247
|
+
modulus = pretty(p * p + p1 * p1 * b2)
|
|
1248
|
+
# atan(u) = -atan(1/u) + const: pick the orientation whose argument is a polynomial
|
|
1249
|
+
arctan =
|
|
1250
|
+
if p.degree > p1.degree
|
|
1251
|
+
2 * b * Fn.new(:atan, [p.to_expr / (b * p1.to_expr)])
|
|
1252
|
+
else
|
|
1253
|
+
-2 * b * Fn.new(:atan, [b * p1.to_expr / p.to_expr])
|
|
1254
|
+
end
|
|
1255
|
+
Num.new(a0) * Fn.new(:log, [modulus]) + arctan
|
|
1256
|
+
else
|
|
1257
|
+
beta = RCAS.sqrt(Num.new(disc)) / (2 * qa)
|
|
1258
|
+
(Num.new(a0) + beta) * Fn.new(:log, [p.to_expr + beta * p1.to_expr]) +
|
|
1259
|
+
(Num.new(a0) - beta) * Fn.new(:log, [p.to_expr - beta * p1.to_expr])
|
|
1260
|
+
end
|
|
1261
|
+
end
|
|
1262
|
+
|
|
1263
|
+
# Integer primitive version of a QQ[x] polynomial (a constant factor
|
|
1264
|
+
# inside a log only shifts the integration constant).
|
|
1265
|
+
def pretty(poly)
|
|
1266
|
+
poly.clear_denominators.to_ring(ZZ[*poly.ring.vars]).primitive_part.to_expr
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
# ---- layer 3: Risch-Norman heuristic ---------------------------------------
|
|
1270
|
+
|
|
1271
|
+
def heurisch(f, x)
|
|
1272
|
+
Heurisch.new(f, x).run
|
|
1273
|
+
rescue DomainError, ZeroDivisionError, NotImplementedError, RCAS::Unsupported
|
|
1274
|
+
nil
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
class Heurisch
|
|
1278
|
+
def initialize(f, x)
|
|
1279
|
+
@x = x
|
|
1280
|
+
@f = rewrite_tan(f).simplify
|
|
1281
|
+
@atoms = [] # Expressions; index i+1 in exponent vectors (0 is x)
|
|
1282
|
+
@roots = {} # [base, q] => root atom index
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
def run
|
|
1286
|
+
collect(@f)
|
|
1287
|
+
return nil if @atoms.size > 8
|
|
1288
|
+
closure
|
|
1289
|
+
ftab = laurent(@f) or return nil
|
|
1290
|
+
@derivatives = @atoms.map { |a| atom_derivative(a) or return nil }
|
|
1291
|
+
@trig_pairs = trig_pairs
|
|
1292
|
+
ftab = normalize(ftab)
|
|
1293
|
+
|
|
1294
|
+
monomials = ansatz(ftab)
|
|
1295
|
+
return nil if monomials.empty? || monomials.size > MAX_UNKNOWNS
|
|
1296
|
+
logs = log_candidates(ftab)
|
|
1297
|
+
unknown_tables = monomials.map { |m| normalize(monomial_derivative(m)) } +
|
|
1298
|
+
logs.map { |l| normalize(log_derivative(l)) }
|
|
1299
|
+
|
|
1300
|
+
keys = (unknown_tables.flat_map(&:keys) + ftab.keys).uniq
|
|
1301
|
+
rows = keys.map { |k| unknown_tables.map { |t| t[k] || Num.new(0) } }
|
|
1302
|
+
rhs = keys.map { |k| ftab[k] || Num.new(0) }
|
|
1303
|
+
# with symbolic constants in the entries (pi in the surface of
|
|
1304
|
+
# revolution of sin(2*pi*x)) every step of the elimination cancels,
|
|
1305
|
+
# and a system of hundreds of unknowns ran for minutes (fourth
|
|
1306
|
+
# review); such an ansatz is refused, and the integral stays formal
|
|
1307
|
+
symbolic = (rows.flatten + rhs).any? { |e| !Expression.lift(e).is_a?(Num) }
|
|
1308
|
+
return nil if symbolic && (unknown_tables.size > MAX_SYMBOLIC_UNKNOWNS || rows.size > 4 * MAX_SYMBOLIC_UNKNOWNS)
|
|
1309
|
+
solution = parametric_solve(rows, rhs) || begin
|
|
1310
|
+
matrix = MatrixSpace.new(QQ, rows.size, unknown_tables.size).unchecked(rows)
|
|
1311
|
+
matrix.solve(rhs)
|
|
1312
|
+
rescue DomainError
|
|
1313
|
+
return nil
|
|
1314
|
+
end
|
|
1315
|
+
return nil if solution == :inconsistent
|
|
1316
|
+
|
|
1317
|
+
result = Num.new(0)
|
|
1318
|
+
monomials.each_with_index do |m, i|
|
|
1319
|
+
c = solution[i]
|
|
1320
|
+
next if Scalar.zero?(c)
|
|
1321
|
+
result += c * monomial_expr(m)
|
|
1322
|
+
end
|
|
1323
|
+
logs.each_with_index do |l, j|
|
|
1324
|
+
c = solution[monomials.size + j]
|
|
1325
|
+
next if Scalar.zero?(c)
|
|
1326
|
+
result += c * Fn.new(:log, [l])
|
|
1327
|
+
end
|
|
1328
|
+
result.simplify
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
private
|
|
1332
|
+
|
|
1333
|
+
# With a parameter in the entries (sin(a*x)*cos(x)) the elimination
|
|
1334
|
+
# has to cancel as it goes, or the entries swell: Elimination.rref
|
|
1335
|
+
# ran for minutes on this one (third review, section 5). The
|
|
1336
|
+
# cancelling solver of the q-side does it; nil hands a numeric system
|
|
1337
|
+
# back to the matrix.
|
|
1338
|
+
def parametric_solve(rows, rhs)
|
|
1339
|
+
return nil if (rows.flatten + rhs).all? { |e| Expression.lift(e).is_a?(Num) }
|
|
1340
|
+
unknowns = rows.first.each_index.map { |i| Var.new(:"_heurisch#{i}") }
|
|
1341
|
+
conditions = rows.zip(rhs).map do |row, b|
|
|
1342
|
+
row.each_with_index.reduce(Simplify.negate(Expression.lift(b))) { |acc, (c, i)| acc + Expression.lift(c) * unknowns[i] }.simplify
|
|
1343
|
+
end
|
|
1344
|
+
found = QSummation.linear_solve(conditions, unknowns)
|
|
1345
|
+
return :inconsistent if found.nil?
|
|
1346
|
+
free = unknowns.to_h { |u| [u, Num.new(0)] }
|
|
1347
|
+
unknowns.map { |u| Expression.lift(found[u] || Num.new(0)).subs(free).simplify }
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def x = @x
|
|
1351
|
+
def depends?(e) = Integrate.depends?(e, x)
|
|
1352
|
+
def zeros = Array.new(@atoms.size + 1, 0)
|
|
1353
|
+
|
|
1354
|
+
def rewrite_tan(e)
|
|
1355
|
+
e = e.map_children { |c| rewrite_tan(c) }
|
|
1356
|
+
e.is_a?(Fn) && e.name == :tan ? Fn.new(:sin, e.args) / Fn.new(:cos, e.args) : e
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
# Register every transcendental / algebraic / polynomial atom of e.
|
|
1360
|
+
def collect(e)
|
|
1361
|
+
_, table = Expand.table(e)
|
|
1362
|
+
table.each_key do |factors|
|
|
1363
|
+
factors.each do |base, exp|
|
|
1364
|
+
if exp.is_a?(Expression) && depends?(exp)
|
|
1365
|
+
register(Simplify.power_node(base, exp))
|
|
1366
|
+
collect(exp)
|
|
1367
|
+
elsif exp.is_a?(Rational) && depends?(base)
|
|
1368
|
+
register_root(base, exp.denominator)
|
|
1369
|
+
collect(base) unless base == x
|
|
1370
|
+
elsif depends?(base) && base != x
|
|
1371
|
+
register(base)
|
|
1372
|
+
collect(base) if base.is_a?(Add) || base.is_a?(Sub) || base.is_a?(Neg)
|
|
1373
|
+
collect(base.args.first) if base.is_a?(Fn)
|
|
1374
|
+
end
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
def register(atom)
|
|
1380
|
+
return if atom == x || @atoms.include?(atom)
|
|
1381
|
+
raise DomainError, "unsupported atom #{atom}" if atom.is_a?(Fn) && !ATOM_FUNCTIONS.include?(atom.name)
|
|
1382
|
+
@atoms << atom
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
def register_root(base, q)
|
|
1386
|
+
register(base) unless base == x
|
|
1387
|
+
key = [base, q]
|
|
1388
|
+
return @roots[key] if @roots.key?(key)
|
|
1389
|
+
atom = Pow.new(base, Num.new(Rational(1, q)))
|
|
1390
|
+
@atoms << atom
|
|
1391
|
+
@roots[key] = @atoms.size - 1
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
# Atoms appearing in derivatives of atoms are atoms too.
|
|
1395
|
+
def closure
|
|
1396
|
+
10.times do
|
|
1397
|
+
before = @atoms.size
|
|
1398
|
+
@atoms.dup.each do |a|
|
|
1399
|
+
next if @roots.value?(@atoms.index(a))
|
|
1400
|
+
collect(a.diff(x).simplify)
|
|
1401
|
+
end
|
|
1402
|
+
break if @atoms.size == before
|
|
1403
|
+
end
|
|
1404
|
+
raise DomainError, "too many atoms" if @atoms.size > 12
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
# Expression => { exponent_vector => coefficient } or nil
|
|
1408
|
+
def laurent(e)
|
|
1409
|
+
constant, table = Expand.table(e.simplify)
|
|
1410
|
+
out = {}
|
|
1411
|
+
add(out, zeros, Num.new(constant)) unless constant.zero?
|
|
1412
|
+
table.each do |factors, coeff|
|
|
1413
|
+
exps = zeros
|
|
1414
|
+
c = Num.new(coeff)
|
|
1415
|
+
factors.each do |base, exp|
|
|
1416
|
+
if exp.is_a?(Expression) && depends?(exp)
|
|
1417
|
+
i = @atoms.index(Simplify.power_node(base, exp)) or return nil
|
|
1418
|
+
exps[i + 1] += 1
|
|
1419
|
+
elsif exp.is_a?(Rational) && depends?(base)
|
|
1420
|
+
i = @roots[[base, exp.denominator]] or return nil
|
|
1421
|
+
exps[i + 1] += exp.numerator
|
|
1422
|
+
elsif base == x
|
|
1423
|
+
return nil unless exp.is_a?(Integer)
|
|
1424
|
+
exps[0] += exp
|
|
1425
|
+
elsif depends?(base)
|
|
1426
|
+
return nil unless exp.is_a?(Integer)
|
|
1427
|
+
i = @atoms.index(base) or return nil
|
|
1428
|
+
exps[i + 1] += exp
|
|
1429
|
+
else
|
|
1430
|
+
c *= Simplify.power_node(base, exp)
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
add(out, exps, c.simplify)
|
|
1434
|
+
end
|
|
1435
|
+
out
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
def atom_derivative(a)
|
|
1439
|
+
if (root = @roots.key(@atoms.index(a)))
|
|
1440
|
+
base, q = root
|
|
1441
|
+
inner = base == x ? { zeros => Num.new(1) } : laurent(base.diff(x).simplify)
|
|
1442
|
+
return nil unless inner
|
|
1443
|
+
exps = zeros
|
|
1444
|
+
exps[@atoms.index(a) + 1] = 1 - q
|
|
1445
|
+
scale(multiply(inner, { exps => Num.new(1) }), Num.new(Rational(1, q)))
|
|
1446
|
+
else
|
|
1447
|
+
laurent(a.diff(x).simplify)
|
|
1448
|
+
end
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
# ---- tables ---------------------------------------------------------
|
|
1452
|
+
|
|
1453
|
+
def add(table, key, coeff)
|
|
1454
|
+
v = Scalar.add(table[key] || Num.new(0), coeff)
|
|
1455
|
+
Scalar.zero?(v) ? table.delete(key) : table[key] = v
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
def merge(a, b)
|
|
1459
|
+
out = a.dup
|
|
1460
|
+
b.each { |k, c| add(out, k, c) }
|
|
1461
|
+
out
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
def scale(table, c)
|
|
1465
|
+
table.transform_values { |v| Scalar.mul(v, c) }
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
def multiply(a, b)
|
|
1469
|
+
out = {}
|
|
1470
|
+
a.each do |ka, ca|
|
|
1471
|
+
b.each { |kb, cb| add(out, ka.zip(kb).map(&:sum), Scalar.mul(ca, cb)) }
|
|
1472
|
+
end
|
|
1473
|
+
out
|
|
1474
|
+
end
|
|
1475
|
+
|
|
1476
|
+
def monomial_derivative(exps)
|
|
1477
|
+
out = {}
|
|
1478
|
+
exps.each_with_index do |e, i|
|
|
1479
|
+
next if e.zero?
|
|
1480
|
+
lowered = exps.dup
|
|
1481
|
+
lowered[i] -= 1
|
|
1482
|
+
d = i.zero? ? { zeros => Num.new(1) } : @derivatives[i - 1]
|
|
1483
|
+
out = merge(out, scale(multiply(d, { lowered => Num.new(1) }), Num.new(e)))
|
|
1484
|
+
end
|
|
1485
|
+
out
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
def log_derivative(l)
|
|
1489
|
+
i = @atoms.index(l)
|
|
1490
|
+
inverse = zeros
|
|
1491
|
+
if i.nil?
|
|
1492
|
+
inverse[0] = -1
|
|
1493
|
+
multiply({ zeros => Num.new(1) }, { inverse => Num.new(1) })
|
|
1494
|
+
else
|
|
1495
|
+
inverse[i + 1] = -1
|
|
1496
|
+
multiply(@derivatives[i], { inverse => Num.new(1) })
|
|
1497
|
+
end
|
|
1498
|
+
end
|
|
1499
|
+
|
|
1500
|
+
def monomial_expr(exps)
|
|
1501
|
+
factors = {}
|
|
1502
|
+
factors[x] = exps[0] unless exps[0].zero?
|
|
1503
|
+
exps.drop(1).each_with_index { |e, i| factors[@atoms[i]] = e unless e.zero? }
|
|
1504
|
+
Simplify.rebuild_product(1, factors)
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1507
|
+
# ---- trig relations ---------------------------------------------------
|
|
1508
|
+
|
|
1509
|
+
# [[sin_index, cos_index, sign]] with cos^2 = 1 + sign*sin^2
|
|
1510
|
+
def trig_pairs
|
|
1511
|
+
pairs = []
|
|
1512
|
+
@atoms.each_with_index do |a, i|
|
|
1513
|
+
next unless a.is_a?(Fn) && %i[sin sinh].include?(a.name)
|
|
1514
|
+
partner = a.name == :sin ? :cos : :cosh
|
|
1515
|
+
j = @atoms.index { |b| b.is_a?(Fn) && b.name == partner && b.args == a.args }
|
|
1516
|
+
pairs << [i + 1, j + 1, a.name == :sin ? -1 : 1] if j
|
|
1517
|
+
end
|
|
1518
|
+
pairs
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
# Reduce cos^k (k >= 2) via cos^2 = 1 - sin^2 (cosh^2 = 1 + sinh^2), and
|
|
1522
|
+
# sin^2 via sin^2 = 1 - cos^2 where cos has a negative exponent.
|
|
1523
|
+
def normalize(table)
|
|
1524
|
+
@trig_pairs.each do |si, ci, sign|
|
|
1525
|
+
loop do
|
|
1526
|
+
key = table.keys.find { |k| k[ci] >= 2 }
|
|
1527
|
+
break unless key
|
|
1528
|
+
c = table.delete(key)
|
|
1529
|
+
k1 = key.dup
|
|
1530
|
+
k1[ci] -= 2
|
|
1531
|
+
add(table, k1, c)
|
|
1532
|
+
k2 = k1.dup
|
|
1533
|
+
k2[si] += 2
|
|
1534
|
+
add(table, k2, Scalar.mul(c, Num.new(sign)))
|
|
1535
|
+
end
|
|
1536
|
+
loop do
|
|
1537
|
+
key = table.keys.find { |k| k[ci].negative? && k[si] >= 2 }
|
|
1538
|
+
break unless key
|
|
1539
|
+
c = table.delete(key)
|
|
1540
|
+
k1 = key.dup
|
|
1541
|
+
k1[si] -= 2
|
|
1542
|
+
add(table, k1, Scalar.mul(c, Num.new(sign)))
|
|
1543
|
+
k2 = k1.dup
|
|
1544
|
+
k2[ci] += 2
|
|
1545
|
+
add(table, k2, Scalar.mul(c, Num.new(-sign)))
|
|
1546
|
+
end
|
|
1547
|
+
end
|
|
1548
|
+
table
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
# ---- the ansatz -------------------------------------------------------
|
|
1552
|
+
|
|
1553
|
+
def ansatz(ftab)
|
|
1554
|
+
keys = ftab.keys
|
|
1555
|
+
ranges = (0..@atoms.size).map do |i|
|
|
1556
|
+
values = keys.map { |k| k[i] }
|
|
1557
|
+
lo, hi = values.min, values.max
|
|
1558
|
+
if i.zero?
|
|
1559
|
+
[[lo, 0].min, hi + 1]
|
|
1560
|
+
else
|
|
1561
|
+
a = @atoms[i - 1]
|
|
1562
|
+
if @roots.value?(i - 1) then [[lo, 0].min, hi + 2]
|
|
1563
|
+
elsif a.is_a?(Fn) && a.name == :exp then [lo, hi]
|
|
1564
|
+
elsif a.is_a?(Pow) then [lo, hi]
|
|
1565
|
+
elsif a.is_a?(Fn) && %i[sin cos sinh cosh].include?(a.name)
|
|
1566
|
+
m = @trig_pairs.select { |p| p[0] == i || p[1] == i }.flat_map { |si, ci, _| keys.map { |k| k[si] + k[ci] } }.max || hi
|
|
1567
|
+
[[lo, 0].min, [m, hi].max + 1]
|
|
1568
|
+
else [[lo + (lo.negative? ? 1 : 0), 0].min, hi + 1]
|
|
1569
|
+
end
|
|
1570
|
+
end
|
|
1571
|
+
end
|
|
1572
|
+
count = ranges.reduce(1) { |acc, (lo, hi)| acc * (hi - lo + 1) }
|
|
1573
|
+
return [] if count > MAX_UNKNOWNS
|
|
1574
|
+
ranges.map { |lo, hi| (lo..hi).to_a }.reduce([[]]) { |acc, r| acc.product(r).map(&:flatten) }
|
|
1575
|
+
.reject { |m| m.all?(&:zero?) }
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
def log_candidates(ftab)
|
|
1579
|
+
cands = []
|
|
1580
|
+
cands << x if ftab.keys.any? { |k| k[0].negative? }
|
|
1581
|
+
@atoms.each do |a|
|
|
1582
|
+
next if @roots.value?(@atoms.index(a))
|
|
1583
|
+
cands << a if a.is_a?(Add) || a.is_a?(Sub) || (a.is_a?(Fn) && %i[sin cos sinh cosh].include?(a.name))
|
|
1584
|
+
end
|
|
1585
|
+
cands
|
|
1586
|
+
end
|
|
1587
|
+
end
|
|
1588
|
+
end
|
|
1589
|
+
end
|