rcas 0.2.0

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