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
data/lib/rcas/solve.rb ADDED
@@ -0,0 +1,2002 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # A solution of several unknowns, {x => 2, y => 1}: a Hash keyed by the
5
+ # indeterminates, which prints and compares as one. In a session a bare
6
+ # name is a Symbol, so sol[x] asks with :x while the key is Var(:x) - it
7
+ # answered nil (found on 23 Sept 2026, writing the linear optimization
8
+ # section). A Symbol is looked up as the indeterminate it names.
9
+ class Assignment < Hash
10
+ def [](key) = super(Assignment.key_for(key))
11
+ def fetch(key, *rest, &block) = super(Assignment.key_for(key), *rest, &block)
12
+ def key?(key) = super(Assignment.key_for(key))
13
+ alias has_key? key?
14
+ alias include? key?
15
+ alias member? key?
16
+ def values_at(*keys) = super(*keys.map { |k| Assignment.key_for(k) })
17
+ def dig(key, *rest) = super(Assignment.key_for(key), *rest)
18
+ def self.key_for(key) = key.is_a?(Symbol) ? Var.new(key) : key
19
+ end
20
+
21
+ # lhs = rhs. Build with eq(a, b) or a.eq(b); solve with solve(equation, x).
22
+ class Equation
23
+ attr_reader :lhs, :rhs
24
+
25
+ def initialize(lhs, rhs)
26
+ @lhs = Expression.lift(lhs)
27
+ @rhs = Expression.lift(rhs)
28
+ freeze
29
+ end
30
+
31
+ def to_expr = Sub.new(lhs, rhs)
32
+ def simplify = Equation.new(lhs.simplify, rhs.simplify)
33
+ def expand = Equation.new(lhs.expand, rhs.expand)
34
+ def subs(*args) = Equation.new(lhs.subs(*args), rhs.subs(*args))
35
+ def swap = Equation.new(rhs, lhs)
36
+ def variables = (lhs.variables | rhs.variables).sort
37
+ def solve(var = nil) = Solve.solve(self, var)
38
+
39
+ # Does the equation hold for these values? Exactly for exact values; a
40
+ # Float difference holds when it is within its own rounding error (an
41
+ # absolute 1e-9 said 1e-10 = 0 holds: fourth review, 2.4).
42
+ def holds?(**bindings)
43
+ difference = Expression.lift(lhs - rhs).subs(bindings.to_h { |k, v| [k, Expression.lift(v)] })
44
+ value = difference.simplify
45
+ return false unless value.is_a?(Num) || value.variables.empty?
46
+ return Decide.zero?(value) == true unless difference.each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Float) }
47
+ found, error = Decide.float_with_error(difference)
48
+ !found.nil? && found.abs <= Decide::ERROR_MARGIN * error
49
+ end
50
+
51
+ %i[+ - * /].each do |op|
52
+ define_method(op) do |other|
53
+ other.is_a?(Equation) ? Equation.new(lhs.public_send(op, other.lhs), rhs.public_send(op, other.rhs)) : Equation.new(lhs.public_send(op, other), rhs.public_send(op, other))
54
+ end
55
+ end
56
+ def -@ = Equation.new(-lhs, -rhs)
57
+
58
+ # hold { integral(x**2, x) == x }.doit is x**3/3 = x: both sides
59
+ # evaluated, the statement kept (third review, C9)
60
+ def evaluate = Equation.new(lhs.evaluate, rhs.evaluate)
61
+ alias doit evaluate
62
+ alias unhold evaluate
63
+
64
+ def ==(other) = other.is_a?(Equation) && other.lhs == lhs && other.rhs == rhs
65
+ alias eql? ==
66
+ def hash = [Equation, lhs, rhs].hash
67
+
68
+ def to_s = "#{lhs} = #{rhs}"
69
+ alias inspect to_s
70
+
71
+ # to_latex: latex.rb's (the one with wrap:), which replaced this one
72
+ end
73
+
74
+ # {2*pi*k | k in ZZ}: a solution that is a family rather than a number.
75
+ # An equation with infinitely many solutions has no honest answer as a
76
+ # list of numbers - sin(x) = 0 is solved by every multiple of pi, not by
77
+ # 0 and pi - and this is the object MuPAD answers such an equation with.
78
+ # The parameter carries its domain with it, so the answer says what k is
79
+ # instead of leaving the reader (and `simplify`) to guess.
80
+ class ImageSet
81
+ attr_reader :expr, :parameters, :domain
82
+
83
+ def initialize(expr, parameters, domain = ZZ)
84
+ @expr = Expression.lift(expr)
85
+ @parameters = Array(parameters).map { |p| p.is_a?(Var) ? p : Var.new(p) }
86
+ @domain = domain
87
+ freeze
88
+ end
89
+
90
+ # The member at k = i, with one value per parameter: at(0), at(1), ...
91
+ def at(*values)
92
+ bindings = parameters.each_with_index.to_h { |p, i| [p.name, Expression.lift(values[i])] }
93
+ expr.subs(bindings).simplify
94
+ end
95
+
96
+ # No member on the real line: the step is real and a member is not, so
97
+ # every member has the same non-zero imaginary part ({pi - acos(2) +
98
+ # 2*pi*k}, the zeros of 2 + cos(x)). Only an affine family is measured.
99
+ def nonreal?
100
+ pair = affine or return false
101
+ base, step = pair
102
+ Inequalities.real?(step) && !Inequalities.real?(base)
103
+ rescue NotImplementedError, RCAS::Unsupported
104
+ false
105
+ rescue StandardError => rescued
106
+ RCAS.guard!(rescued)
107
+ false
108
+ end
109
+
110
+ # [base, step] when the family is base + step*k, nil otherwise.
111
+ def affine
112
+ return nil unless parameters.size == 1
113
+ coefficients = Solve.polynomial_coefficients(expr, parameters.first)
114
+ return nil unless coefficients && coefficients.size <= 2 && coefficients.none? { |c| c.variables.include?(parameters.first.name) }
115
+ [coefficients[0].simplify, (coefficients[1] || Num.new(0)).simplify]
116
+ end
117
+
118
+ # The members between two numbers, when they can be counted out; [] for
119
+ # a family that never meets the real line; nil when they cannot be
120
+ # counted - too many, infinitely many, or a family whose shape is not
121
+ # understood. Counting base + k*step from at(0) and at(1) was right
122
+ # only for an arithmetic family: sqrt(pi*k), the kinks of |sin(x**2)|,
123
+ # lost four of seven, and 1/(2*pi*k) divided by zero (fourth review).
124
+ # Only the indices of the family's own domain count (k >= 0 over NN).
125
+ def between(lo, hi, limit: 1024)
126
+ return nil unless parameters.size == 1
127
+ return [] if nonreal?
128
+ indices = affine ? affine_indices(lo, hi) : crossing_indices(lo, hi)
129
+ return nil if indices.nil?
130
+ first, last = indices
131
+ first = [first, 0].max if domain == NN
132
+ return [] if first > last
133
+ return nil if last - first > limit
134
+ (first..last).filter_map do |i|
135
+ member = begin
136
+ at(i)
137
+ rescue ZeroDivisionError
138
+ next
139
+ end
140
+ member if (v = numeric(member)) && v >= lo && v <= hi
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def affine_indices(lo, hi)
147
+ base, step = affine.map { |c| numeric(c) }
148
+ return nil if base.nil? || step.nil? || step.abs < 1e-12
149
+ [((lo - base) / step).floor, ((hi - base) / step).ceil].minmax
150
+ end
151
+
152
+ # expr(k) is continuous between the edges of its real domain, so it
153
+ # meets [lo, hi] on the pieces cut out by the solutions of expr = lo and
154
+ # expr = hi and those edges; one sample decides each piece. nil when a
155
+ # solution cannot be named, or a piece that reaches to infinity stays
156
+ # inside (1/(2*pi*k) crowds towards 0: infinitely many members).
157
+ def crossing_indices(lo, hi)
158
+ k = parameters.first
159
+ cuts = [lo, hi].flat_map do |bound|
160
+ roots = Solve.solve(expr - Num.new(bound.to_r), k, domain: RR)
161
+ return nil unless roots.is_a?(Array) && roots.all? { |r| r.is_a?(Expression) && r.variables.empty? }
162
+ roots.filter_map { |r| numeric(r) }
163
+ end
164
+ edges = Analysis.real_domain(expr, k).intervals.flat_map { |i| [i.low, i.high] }
165
+ .reject { |e| Limits.infinite?(e) }.map { |e| numeric(e) or return nil }
166
+ points = (cuts + edges).uniq.sort
167
+ inside = ->(t) { (v = value_at(k, t)) && v >= lo && v <= hi }
168
+ found = []
169
+ ([-Float::INFINITY] + points).zip(points + [Float::INFINITY]).each do |a, b|
170
+ t = if a.infinite? && b.infinite? then 0.0
171
+ elsif a.infinite? then b - 1
172
+ elsif b.infinite? then a + 1
173
+ else (a + b) / 2
174
+ end
175
+ next unless inside.call(t)
176
+ return nil if a.infinite? || b.infinite?
177
+ found << [a.floor, b.ceil]
178
+ end
179
+ points.each { |t| found << [t.floor, t.ceil] if inside.call(t) }
180
+ return [1, 0] if found.empty?
181
+ [found.map(&:first).min, found.map(&:last).max]
182
+ rescue NotImplementedError, RCAS::Unsupported, ArgumentError, ZeroDivisionError
183
+ nil
184
+ end
185
+
186
+ def value_at(k, t)
187
+ numeric(expr.subs(k.name => Num.new(t)))
188
+ rescue ZeroDivisionError
189
+ nil
190
+ end
191
+
192
+ public
193
+
194
+ def variables = expr.variables - parameters.map(&:name)
195
+ def to_expr = expr
196
+
197
+ # The image of the set under a function of its member: the parameter
198
+ # keeps its domain while the block runs, and the result is simplified
199
+ # there too, so cos of {2*pi*k | k in ZZ} folds to 1 without anyone
200
+ # declaring k first - that fold needs the integer. A result that no
201
+ # longer mentions the parameter is not a family any more and comes back
202
+ # as the value itself.
203
+ def map
204
+ inside = RCAS.assume(**parameters.to_h { |p| [p.name, domain] }) do
205
+ found = yield(expr)
206
+ found.is_a?(Expression) ? found.simplify : found
207
+ end
208
+ lifted = Expression.lift(inside)
209
+ (lifted.variables & parameters.map(&:name)).empty? ? lifted : ImageSet.new(lifted, parameters, domain)
210
+ end
211
+
212
+ def ==(other)
213
+ other.is_a?(ImageSet) && other.expr == expr && other.parameters == parameters && other.domain == domain
214
+ end
215
+ alias eql? ==
216
+ def hash = [ImageSet, expr, parameters, domain].hash
217
+
218
+ def to_s = "{#{expr} | #{parameters.join(', ')} in #{domain}}"
219
+ alias inspect to_s
220
+
221
+ def to_latex(wrap: nil)
222
+ inside = parameters.map { |p| LaTeX.of(p) }.join(', ')
223
+ "\\left\\{ #{LaTeX.of(expr)} \\mid #{inside} \\in #{LaTeX.of(domain)} \\right\\}"
224
+ end
225
+
226
+ private
227
+
228
+ def numeric(value) = RCAS.real_float(value, finite: false)
229
+ end
230
+
231
+ # Equation solving.
232
+ #
233
+ # solve(x**2 - 3*x + 2, x) # => [1, 2]
234
+ # solve(eq(x**2, -1), x) # => [-i, i]
235
+ # solve(exp(2*x) - 3*exp(x) + 2, x) # => [0, log(2)]
236
+ # solve([eq(x + y, 3), eq(x - y, 1)], [x, y]) # => [{x => 2, y => 1}]
237
+ #
238
+ # solve(abs(x) - 1, x) # => [1, -1]
239
+ #
240
+ # Polynomials are solved exactly through factorization over QQ, the
241
+ # quadratic formula, k-th roots for binomials and the quadratic formula
242
+ # with symbolic coefficients; irreducible factors of degree >= 3 with
243
+ # numeric coefficients get numeric roots. Transcendental equations are
244
+ # reduced to polynomials in one atom (exp(x), sin(x), sqrt(x), ...) and
245
+ # inverted. An equation with abs or sign is split into its cases and each
246
+ # candidate substituted back. Systems: linear in the unknowns, or two
247
+ # polynomial equations in two unknowns through resultants.
248
+ #
249
+ # Sources (keys: MANUAL.md, Sources): elimination by resultants [GCL92,
250
+ # ch. 9], [CLO15, §3.6]; numeric roots by the Durand-Kerner (Weierstrass)
251
+ # simultaneous iteration [Ker66], started at powers of 0.4 + 0.9i.
252
+ module Solve
253
+ MAX_DEPTH = 6
254
+ # abs and sign split the line into cases; 2**n branches, so a small n.
255
+ CASES = %i[abs sign].freeze
256
+ MAX_CASES = 3
257
+
258
+ module_function
259
+
260
+ # Every solution, which for a trigonometric equation means a family per
261
+ # period: the answer is an ImageSet, {2*pi*k | k in ZZ}, rather than a
262
+ # selection from it. `principal: true` (or the older `all: false`) asks
263
+ # for the solutions in one period instead, which is what the analysis
264
+ # inside rcas wants and what a table of exact values shows.
265
+ # The unknown's declared domain (assume(x: ZZ), or domain: here) keeps
266
+ # out the solutions that demonstrably do not lie in it.
267
+ def solve(target, vars = nil, all: true, principal: false, domain: nil)
268
+ principal ||= !all
269
+ if target.equal?(true) || target.equal?(false)
270
+ raise ArgumentError, "solve: `==` compares structurally in Ruby and this one is already #{target}; " \
271
+ "write solve(eq(lhs, rhs), x) or solve(hold { lhs == rhs }, x)"
272
+ end
273
+ return Inequalities.solve(target, vars) if target.is_a?(Inequality) || (target.is_a?(Array) && target.any? { |t| t.is_a?(Inequality) })
274
+ return system(target, vars, domain: domain) if target.is_a?(Array)
275
+ return piecewise(target, vars) if piecewise?(target)
276
+ original = to_zero(target)
277
+ f = original.simplify
278
+ x = variable(f, vars)
279
+ # The poles are read off the equation as it was written: simplify
280
+ # cancels (x - 1)/(x - 1) to 1, and x = 1 is still no solution of
281
+ # (x - 1)/(x - 1) = 1, where the left side has no value.
282
+ poles = Analysis.denominators(original, x)
283
+ # 0 = 0 holds for every value of x. That is an answer, and a set is
284
+ # what says it; raising made a true statement look like a failure.
285
+ # Every value *of x*: with x declared an integer, sin(pi*x) vanishes
286
+ # on ZZ and nowhere else, so the reals would be an overstatement.
287
+ return everywhere(x, domain, poles, original) if Scalar.zero?(f) || rational_identity?(f, x)
288
+ found =
289
+ begin
290
+ univariate(f, x, 0, all: !principal)
291
+ rescue Whole => e
292
+ # squaring gave an identity, and the answer is a set (S18)
293
+ return e.set
294
+ rescue NotImplementedError, RCAS::Unsupported
295
+ constant = trig_constant(f)
296
+ raise if constant.nil?
297
+ return Scalar.zero?(constant) ? everywhere(x, domain, poles, original) : []
298
+ end
299
+ roots = dedupe(found).map { |root| family(root, x, f) }
300
+ ordered(dedupe(absorbed(dedupe(restrict(off_poles(merge_families(roots), poles, x), x, domain))).map { |r| normal_family(r) }))
301
+ end
302
+
303
+ # One spelling per set (the fifth review, 3.2): base + step*k with, over
304
+ # ZZ, a positive real step and the base reduced into [0, step) - the
305
+ # {pi*(1 + 2*k)/2} that family_off_poles built is {pi/2 + pi*k}, which is
306
+ # what solve(cos(x)) says. Over NN the direction is part of the set and
307
+ # only the spelling changes. Anything else is left as it is.
308
+ def normal_family(set)
309
+ return set unless set.is_a?(ImageSet)
310
+ pair = set.affine or return set
311
+ base, step = pair
312
+ k = set.parameters.first
313
+ if set.domain == ZZ && step.variables.empty? && base.variables.empty?
314
+ sign = Decide.sign(step)
315
+ return set unless %i[positive negative].include?(sign)
316
+ step = Neg.new(step).simplify if sign == :negative
317
+ ratio = (base / step).simplify
318
+ if ratio.is_a?(Num) && (ratio.value.is_a?(Integer) || ratio.value.is_a?(Rational))
319
+ base = (base - step * Num.new(ratio.value.floor)).simplify
320
+ end
321
+ end
322
+ ImageSet.new((base + step * k).simplify, set.parameters, set.domain)
323
+ rescue StandardError => rescued
324
+ RCAS.guard!(rescued)
325
+ set
326
+ end
327
+
328
+ # A point that is a member of a family is said by the family: x*sin(x)
329
+ # listed 0 and {pi*k | k in ZZ} (fourth review, 2.5).
330
+ def absorbed(list)
331
+ families = list.select { |s| s.is_a?(ImageSet) && s.affine }
332
+ sets = list.select { |s| s.is_a?(NumberSet) }
333
+ return list if families.empty? && sets.empty?
334
+ list.reject do |point|
335
+ next false unless point.is_a?(Expression) && point.variables.empty?
336
+ next true if sets.any? { |set| point.is_a?(Num) && set.include?(point.value) }
337
+ families.any? do |set|
338
+ base, step = set.affine
339
+ next false if Scalar.zero?(step)
340
+ index = ((point - base) / step).simplify
341
+ index.is_a?(Num) && index.value.is_a?(Integer) && set.domain.include?(index.value)
342
+ end
343
+ end
344
+ end
345
+
346
+ # x/(x + 1) + 1/(x + 1) - 1 is zero as a rational function, which the
347
+ # normal form of simplify does not show: its cleared numerator is the
348
+ # zero polynomial, and the root finder answered that with [].
349
+ def rational_identity?(f, x)
350
+ return false unless f.each_node.any? { |n| n.is_a?(Div) || (n.is_a?(Pow) && n.exponent.is_a?(Num) && Simplify.negative?(n.exponent.value)) }
351
+ Scalar.zero?(f.cancel)
352
+ rescue StandardError => rescued
353
+ RCAS.guard!(rescued)
354
+ false
355
+ end
356
+
357
+ # Every value of x is a solution: the declared domain when there is one,
358
+ # the reals otherwise - less the poles of the equation as written, so
359
+ # (x**2 - 1)/(x - 1) = x + 1 holds everywhere but at 1, and less the
360
+ # points where it has no value at all (log(x)/log(x) = 1 is false at
361
+ # 0: its real domain says so). Poles that come in a family cannot be
362
+ # taken out of a finite union of intervals, and the reals would claim
363
+ # them: that is refused. So is a declared ZZ, QQ or CC with a pole in
364
+ # it, since rcas has no set "the integers but 1" to answer with (the
365
+ # declared set itself was the answer, pole and all: fourth review).
366
+ def everywhere(x, domain, poles = [], original = nil)
367
+ declared = domain || RCAS.assumption(x.name)
368
+ points = poles.flat_map do |d|
369
+ found = Solve.solve(d, x)
370
+ raise RCAS::Unsupported, "every #{x} where #{d} != 0 is a solution; that set is not a finite union of intervals" unless found.is_a?(Array)
371
+ found
372
+ end
373
+ if points.any? { |p| p.is_a?(ImageSet) }
374
+ raise RCAS::Unsupported, "every #{x} off the zeros of #{poles.join(', ')} is a solution; that set is not a finite union of intervals"
375
+ end
376
+ if declared && declared != RR
377
+ inside = points.reject { |p| Infer.excluded?(p, declared) }
378
+ return declared if inside.empty?
379
+ raise RCAS::Unsupported, "every #{x} in #{declared} except #{inside.map(&:to_s).join(', ')} is a solution; rcas has no set to write that with"
380
+ end
381
+ real = points.select { |p| Analysis.numeric(p) }
382
+ set = real.empty? ? RealSet.reals : RealSet.reals - RealSet.new(real.map { |p| Interval.point(p) })
383
+ return set if original.nil?
384
+ defined = begin
385
+ Analysis.real_domain(original, x)
386
+ rescue NotImplementedError, RCAS::Unsupported, ArgumentError
387
+ nil
388
+ end
389
+ defined.is_a?(RealSet) ? set - (RealSet.reals - defined) : set
390
+ end
391
+
392
+ # Roots and families of the simplified equation that are poles of the
393
+ # equation as written. A point is dropped when a denominator is shown
394
+ # to vanish there. A family is checked member by member through its
395
+ # index: the members where a denominator vanishes form a sub-progression
396
+ # (or a single index, or nothing), and what is left is written as
397
+ # families again - sin(2*x)/sin(x) keeps {pi/2 + pi*k} and loses
398
+ # {pi*k}, sin(x)/x keeps {pi*k} for k >= 1 and k <= -1.
399
+ def off_poles(roots, poles, x)
400
+ return roots if poles.empty?
401
+ roots.flat_map do |root|
402
+ if root.is_a?(ImageSet)
403
+ family_off_poles(root, poles, x)
404
+ elsif root.is_a?(Expression) && root.variables.empty?
405
+ pole_at?(poles, x, root) ? [] : [root]
406
+ else
407
+ [root]
408
+ end
409
+ end
410
+ end
411
+
412
+ def pole_at?(poles, x, point)
413
+ poles.any? do |d|
414
+ value = begin
415
+ d.subs(x => point).simplify
416
+ rescue ZeroDivisionError
417
+ next true
418
+ end
419
+ Decide.zero?(value) == true
420
+ end
421
+ end
422
+
423
+ # [a + b*k | k in ZZ] minus its poles, as a list of families.
424
+ def family_off_poles(set, poles, x)
425
+ return [set] unless set.parameters.size == 1 && set.domain == ZZ
426
+ k = set.parameters.first
427
+ step = begin
428
+ Coefficients.coeff(set.expr, k, 1)
429
+ rescue StandardError => rescued
430
+ RCAS.guard!(rescued)
431
+ return [set]
432
+ end
433
+ offset = (set.expr - step * k).simplify
434
+ return [set] if offset.variables.include?(k.name) || Scalar.zero?(step)
435
+ # indices to drop: residues r mod n (from a family of poles) and
436
+ # single indices (from isolated poles)
437
+ progressions = []
438
+ singles = []
439
+ poles.each do |d|
440
+ zeros = begin
441
+ Solve.solve(d, x)
442
+ rescue NotImplementedError, RCAS::Unsupported, ArgumentError
443
+ next
444
+ end
445
+ next unless zeros.is_a?(Array)
446
+ zeros.each do |z|
447
+ if z.is_a?(ImageSet)
448
+ progressions.concat(progression_hit(z, offset, step))
449
+ elsif z.is_a?(Expression) && z.variables.empty?
450
+ index = ((z - offset) / step).simplify
451
+ singles << index.value if index.is_a?(Num) && index.value.is_a?(Integer)
452
+ end
453
+ end
454
+ end
455
+ return [set] if progressions.empty? && singles.empty?
456
+ modulus = progressions.map(&:last).reduce(1, :lcm)
457
+ kept = (0...modulus).reject { |r| progressions.any? { |res, n| r % n == res } }
458
+ return [] if kept.empty?
459
+ j = Var.new(k.name)
460
+ singles = singles.uniq
461
+ kept.flat_map do |r|
462
+ # the indices t of this class, member offset + step*(r + modulus*t),
463
+ # that are poles: all of them go, however many fall in one class
464
+ # (sin(x)/(x**2 - pi**2) kept -pi after taking out pi: fourth review)
465
+ holes = singles.select { |i| i % modulus == r }.map { |i| (i - r) / modulus }.sort
466
+ at = ->(t) { (offset + step * (r + modulus * t)).simplify }
467
+ next [ImageSet.new(at.call(j), [j], ZZ)] if holes.empty?
468
+ between = (holes.first + 1...holes.last).reject { |t| holes.include?(t) }
469
+ next [set] if between.size > 1000
470
+ [ImageSet.new(at.call(holes.first - 1 - j), [j], NN), *between.map { |t| at.call(t) },
471
+ ImageSet.new(at.call(holes.last + 1 + j), [j], NN)]
472
+ end
473
+ end
474
+
475
+ # The residues r mod n of the indices k at which a + b*k is a member of
476
+ # the family z, as [[r, n], ...], when that is decidable: the steps and
477
+ # the offsets have a rational ratio. k gives the member of z with index
478
+ # m = (a - c + b*k)/e, which is an integer or not with period q in k,
479
+ # q the denominator of b/e - so trying k = 0...q finds every residue.
480
+ def progression_hit(z, offset, step)
481
+ return [] unless z.parameters.size == 1 && z.domain == ZZ
482
+ m = z.parameters.first
483
+ zstep = begin
484
+ Coefficients.coeff(z.expr, m, 1)
485
+ rescue StandardError => rescued
486
+ RCAS.guard!(rescued)
487
+ return []
488
+ end
489
+ zoffset = (z.expr - zstep * m).simplify
490
+ return [] if zoffset.variables.include?(m.name)
491
+ ratio = (step / zstep).simplify
492
+ shift = ((offset - zoffset) / zstep).simplify
493
+ rational = ->(v) { v.is_a?(Num) && (v.value.is_a?(Integer) || v.value.is_a?(Rational)) }
494
+ return [] unless rational.call(ratio) && rational.call(shift)
495
+ ratio = Rational(ratio.value)
496
+ shift = Rational(shift.value)
497
+ q = ratio.denominator
498
+ (0...q).select { |k| (shift + ratio * k).denominator == 1 }.map { |r| [r, q] }
499
+ end
500
+
501
+ # An identity is an identity however it is written, and no rule in the
502
+ # chain sees the Pythagorean one: sin(x)**2 + cos(x)**2 - 1 is zero, and
503
+ # `Scalar.zero?` cannot say so because the identity only shows after
504
+ # trigsimp. The other side of the same reading is just as much an
505
+ # answer - a trigonometric expression that reduces to a constant which
506
+ # is not zero has no solutions at all, so sin(x)**2 + cos(x)**2 + 1 = 0
507
+ # is [] rather than "can't solve" (20 Sept 2026, the ninth pass of the
508
+ # review). It is asked only once every rule has failed, because trigsimp
509
+ # costs milliseconds and `discuss` calls solve for every row it fills.
510
+ def trig_constant(f)
511
+ return nil unless f.each_node.any? { |n| n.is_a?(Fn) && Trigonometry::SQUARES.key?(n.name) }
512
+ reduced = Trigonometry.trigsimp(f).simplify
513
+ reduced.variables.empty? ? reduced : nil
514
+ rescue StandardError => rescued
515
+ RCAS.guard!(rescued)
516
+ nil
517
+ end
518
+
519
+ # Families of one period that say the same thing twice, or that together
520
+ # make a finer one: sin(x)**2 = 1 is solved at pi/2, -pi/2 and 3*pi/2
521
+ # over a period of 2*pi, of which the last two are the same set and all
522
+ # three together are {pi/2 + pi*k | k in ZZ} - which is what a student
523
+ # writes. The offsets are compared as fractions of the step, so it works
524
+ # the same for {2*k} and {1 + 2*k}, whose union is the integers.
525
+ def merge_families(roots)
526
+ entries = roots.map { |root| family_shape(root) || [:plain, root] }
527
+ groups = entries.select { |e| e.first == :family }.group_by { |e| e[1] }
528
+ done = {}
529
+ entries.flat_map do |entry|
530
+ next [entry.last] if entry.first == :plain
531
+ next [] if done[entry[1]]
532
+ done[entry[1]] = true
533
+ merged_family(groups[entry[1]], entry[1])
534
+ end
535
+ end
536
+
537
+ # [:family, step, offset as a fraction of the step, the set], or nil for
538
+ # anything this cannot measure - a set whose offset is not a rational
539
+ # multiple of its step has no residue to compare.
540
+ def family_shape(root)
541
+ return nil unless root.is_a?(ImageSet) && root.parameters.size == 1
542
+ k = root.parameters.first
543
+ step = begin
544
+ Coefficients.coeff(root.expr, k, 1)
545
+ rescue StandardError => rescued
546
+ RCAS.guard!(rescued)
547
+ nil
548
+ end
549
+ return nil if step.nil? || Scalar.zero?(step)
550
+ offset = (root.expr - step * k).simplify
551
+ return nil if offset.variables.include?(k.name)
552
+ residue = (offset / step).simplify
553
+ return nil unless residue.is_a?(Num) && (residue.value.is_a?(Integer) || residue.value.is_a?(Rational))
554
+ [:family, step, Rational(residue.value) % 1, root]
555
+ end
556
+
557
+ def merged_family(group, step)
558
+ residues = group.map { |e| e[2] }.uniq.sort
559
+ set = group.first.last
560
+ count = residues.size
561
+ if count > 1 && residues.each_cons(2).all? { |a, b| b - a == Rational(1, count) }
562
+ [rebuilt_family(set, residues.first * step, step / count)]
563
+ else
564
+ residues.map { |residue| rebuilt_family(set, residue * step, step) }
565
+ end
566
+ end
567
+
568
+ # {k | k in ZZ} is the domain itself, and says so.
569
+ def rebuilt_family(set, offset, step)
570
+ k = set.parameters.first
571
+ expr = (Expression.lift(offset) + step * k).simplify
572
+ expr == k ? set.domain : ImageSet.new(expr, [k], set.domain)
573
+ end
574
+
575
+ # A root carrying a parameter the equation did not have is one period's
576
+ # worth of solutions repeated for ever: say so as a set, and name the
577
+ # parameter's domain, so that `simplify` can check the answer and the
578
+ # reader does not have to assume what k is.
579
+ def family(root, x, f)
580
+ return root unless root.is_a?(Expression)
581
+ parameters = root.variables - f.variables - [x.name]
582
+ parameters.empty? ? root : ImageSet.new(root, parameters.sort.map { |name| Var.new(name) })
583
+ end
584
+
585
+ # Real roots ascending, then the rest in the order they were found. The
586
+ # order a method happens to produce is not an answer about the roots.
587
+ def ordered(roots)
588
+ keys = roots.each_with_index.to_h do |root, i|
589
+ value = begin
590
+ root.is_a?(Expression) ? root.evalf : nil # a family or a set has no value
591
+ rescue StandardError => rescued
592
+ RCAS.guard!(rescued)
593
+ nil
594
+ end
595
+ value = value.value if value.is_a?(Num)
596
+ [root, value.is_a?(Numeric) && value.real? ? [0, value.to_f, i] : [1, 0.0, i]]
597
+ end
598
+ roots.sort_by { |root| keys[root] }
599
+ end
600
+
601
+ # Drop the solutions that contradict what the unknown was declared to
602
+ # be - a number set, a sign, or both. What cannot be decided stays
603
+ # (Infer.excluded?), so an answer is never lost to a guess.
604
+ def restrict(roots, x, domain)
605
+ wanted = domain || RCAS.assumption(x.name)
606
+ sign = RCAS.signs[x.name]
607
+ return roots if wanted.nil? && sign.nil?
608
+ roots.flat_map do |root|
609
+ # {k | k in ZZ} came back as ZZ itself: the smaller of the two sets
610
+ # (third review, S17)
611
+ next [wanted && !root.subset?(wanted) ? wanted : root] if root.is_a?(NumberSet)
612
+ next restrict_family(root, wanted, sign) if root.is_a?(ImageSet)
613
+ (wanted && Infer.excluded?(root, wanted)) || (sign && wrong_sign?(root, sign)) ? [] : [root]
614
+ end
615
+ end
616
+
617
+ # Which members of a family lie in a declared domain. For the families
618
+ # trigonometry produces this is decidable rather than guessed: a + b*k
619
+ # with a and b rational multiples of pi is rational only where the pi
620
+ # part cancels, because pi is transcendental, and that happens for at
621
+ # most one k. So {2*pi*k | k in ZZ} meets ZZ in 0 alone, and
622
+ # {pi + 2*pi*k | k in ZZ} not at all. A family of any other shape stays
623
+ # whole: "some member might qualify" is the honest answer there.
624
+ def restrict_family(set, wanted, sign, reindexed: false)
625
+ # a family off the real line has no member in RR: asin(2) + 2*pi*k
626
+ # under domain: RR (third review, S19)
627
+ return [] if wanted && wanted <= RR && set.nonreal?
628
+ if wanted && wanted <= RR && (members = real_members(set))
629
+ return members.reject { |m| Infer.excluded?(m, wanted) || (sign && wrong_sign?(m, sign)) }
630
+ end
631
+ if wanted && wanted <= RR && !reindexed && set.affine.nil? && !real_family?(set)
632
+ # {(4*log(2) + 8*pi*i*k)**(1/2)/2} was kept whole under domain: RR,
633
+ # and discuss drew asymptotes at its complex members (the sixth
634
+ # review's preflight): its real members, or a refusal
635
+ members = power_real_members(set) or
636
+ raise RCAS::Unsupported, "can't tell which members of #{set} are real"
637
+ return members.flat_map do |m|
638
+ next restrict_family(m, wanted, sign, reindexed: true) if m.is_a?(ImageSet)
639
+ Infer.excluded?(m, wanted) || (sign && wrong_sign?(m, sign)) ? [] : [m]
640
+ end
641
+ end
642
+ return [set] unless wanted && wanted <= QQ && set.parameters.size == 1
643
+ k = set.parameters.first
644
+ slope = begin
645
+ Coefficients.coeff(set.expr, k, 1)
646
+ rescue StandardError => rescued
647
+ RCAS.guard!(rescued)
648
+ nil
649
+ end
650
+ return [set] if slope.nil?
651
+ rest = (set.expr - slope * k).simplify
652
+ return [set] if rest.variables.include?(k.name)
653
+ if rational?(slope) && rational?(rest)
654
+ return [set] if wanted == QQ
655
+ return rational_family(set, rest.value.to_r, slope.value.to_r, wanted, sign)
656
+ end
657
+ step = Trig.pi_multiple(slope)
658
+ offset = Scalar.zero?(rest) ? Rational(0) : Trig.pi_multiple(rest)
659
+ return [set] if step.nil? || offset.nil? || step.zero?
660
+ turns = -offset / step
661
+ return [] unless turns.denominator == 1 && set.domain.include?(turns.numerator)
662
+ member = set.at(turns.numerator)
663
+ (wanted && Infer.excluded?(member, wanted)) || (sign && wrong_sign?(member, sign)) ? [] : [member]
664
+ end
665
+
666
+ def rational?(e) = e.is_a?(Num) && (e.value.is_a?(Integer) || e.value.is_a?(Rational))
667
+
668
+ # The real members of a family whose step is not real: base + step*k is
669
+ # real where im(base) + im(step)*k = 0, at one k or none - log(2) of
670
+ # {log(2) + 2*pi*i*k}, nothing of {i*pi + 2*pi*i*k}. This is what
671
+ # domain: RR makes of the complete answer over CC. nil for a family of
672
+ # another shape, or when a part is not decided.
673
+ def real_members(set)
674
+ pair = set.affine or return nil
675
+ base, step = pair
676
+ im_step = ComplexParts.im(step).simplify
677
+ return nil unless im_step.variables.empty? && Decide.zero?(im_step) == false
678
+ im_base = ComplexParts.im(base).simplify
679
+ return nil unless im_base.variables.empty?
680
+ index = (Neg.new(im_base) / im_step).simplify
681
+ return nil unless index.is_a?(Num) || Decide.zero?(ComplexParts.im(index)) == true
682
+ k = index.is_a?(Num) ? index.value : nil
683
+ return [] unless k.is_a?(Integer) || (k.is_a?(Rational) && k.denominator == 1)
684
+ return [] unless set.domain.include?(k.to_i)
685
+ member = set.at(k.to_i)
686
+ real = ComplexParts.re(member).simplify
687
+ # the real part of 2*log(2)/(i*pi + log(2)) + 2*i*pi/(i*pi + log(2))
688
+ # is 2, written over pi**2 + log(2)**2 twice
689
+ cancelled = cancel_constants(real)
690
+ [cancelled.each_node.count < real.each_node.count ? cancelled : real]
691
+ rescue StandardError => rescued
692
+ RCAS.guard!(rescued)
693
+ nil
694
+ end
695
+
696
+ # Every member real: sqrt(pi*k) over NN, the kinks of |sin(x**2)|.
697
+ def real_family?(set)
698
+ bindings = set.parameters.to_h { |p| [p.name, set.domain] }
699
+ domain = RCAS.assume(**bindings) { Infer.domain(set.expr) }
700
+ !domain.nil? && domain <= RR
701
+ rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
702
+ RCAS.guard!(rescued, refused: true)
703
+ false
704
+ end
705
+
706
+ # c*A(k)**e with c real, A affine in k and e = +-1 or +-1/q: w**(1/q)
707
+ # real makes w = (w**(1/q))**q real, and a real w has a real principal
708
+ # root exactly when it is not negative; 1/w is real exactly with w.
709
+ # (Not so for other exponents: w**(3/2) is real at arg(w) = 2*pi/3.)
710
+ # A is real at one k or none (real_members). nil for any other shape.
711
+ def power_real_members(set)
712
+ return nil unless set.parameters.size == 1
713
+ k = set.parameters.first
714
+ coeff, factors = Simplify.factorize(set.expr)
715
+ constant, moving = factors.partition { |b, _| !b.variables.include?(k.name) }
716
+ return nil unless moving.size == 1 && coeff.is_a?(Numeric) && coeff.real?
717
+ scale = Simplify.rebuild_product(coeff, constant.to_h)
718
+ return nil unless ComplexParts.real_valued?(scale)
719
+ base, exp = moving.first
720
+ exp = exp.value if exp.is_a?(Num)
721
+ exp = exp.to_r if exp.is_a?(Integer)
722
+ return nil unless exp.is_a?(Rational) && exp.numerator.abs == 1
723
+ power = ->(b) { (scale * Simplify.power_node(b, exp.denominator == 1 ? exp.to_i : exp)).simplify }
724
+ inner = ImageSet.new(base, set.parameters, set.domain)
725
+ if (pair = inner.affine) && real_step?(pair)
726
+ # 1/(2*pi*k): a real power of a real family is real throughout
727
+ return exp.denominator == 1 ? [set] : non_negative_part(set, inner, k)
728
+ end
729
+ points = real_members(inner) or return nil
730
+ points.filter_map do |b|
731
+ next power.call(b) if exp.denominator == 1
732
+ case Decide.sign(b)
733
+ when :positive, :zero then power.call(b)
734
+ when :negative then nil
735
+ else return nil
736
+ end
737
+ end
738
+ end
739
+
740
+ def real_step?(pair)
741
+ pair.all? { |p| (im = ComplexParts.im(p).simplify).variables.empty? && Decide.zero?(im) == true }
742
+ end
743
+
744
+ # A real affine A(k) = b + s*k under a root: the members are real
745
+ # exactly where A(k) >= 0, which is k >= -b/s for s > 0 - the
746
+ # family again, counted from there over NN: {+-sqrt(pi*k) | k in NN}
747
+ # are the zeros of sin(x**2) on the real line.
748
+ def non_negative_part(set, inner, k)
749
+ b, step = inner.affine
750
+ ratio = (Neg.new(b) / step).simplify
751
+ direction = Decide.sign(step)
752
+ return nil unless rational?(ratio) && %i[positive negative].include?(direction)
753
+ r = ratio.value.to_r
754
+ if direction == :positive
755
+ start = r.ceil
756
+ start = [start, 0].max if set.domain == NN
757
+ return [ImageSet.new(set.expr.subs(k.name => Num.new(start) + k).simplify, [k], NN)]
758
+ end
759
+ last = r.floor
760
+ return (0..last).map { |i| set.at(i) } if set.domain == NN
761
+ [ImageSet.new(set.expr.subs(k.name => Num.new(last) - k).simplify, [k], NN)]
762
+ end
763
+
764
+ # cancel works in a polynomial ring, and pi and log(2) are no
765
+ # indeterminates of one: they stand in as fresh names while it runs.
766
+ def cancel_constants(e)
767
+ atoms = e.each_node.select { |n| n.is_a?(Const) || (n.is_a?(Fn) && n.variables.empty?) }.uniq
768
+ atoms = atoms.reject { |a| atoms.any? { |b| !b.equal?(a) && b != a && b.each_node.any? { |n| n == a } } }
769
+ return e if atoms.empty?
770
+ taken = e.variables
771
+ names = atoms.each_with_index.map { |_, i| Var.new(:"c#{i}") }
772
+ raise ArgumentError, "names taken" if names.any? { |v| taken.include?(v.name) }
773
+ forth = atoms.zip(names).to_h
774
+ e.subs(forth).cancel.subs(forth.invert).simplify
775
+ end
776
+
777
+ # a + b*k with a, b rational is an integer exactly on a residue class of
778
+ # k: b*k + a in ZZ is s*p*k = -q*r (mod q*s) for b = p/q, a = r/s, a
779
+ # linear congruence. So {k/2 | k in ZZ} meets ZZ in every integer, and
780
+ # {1/2 + k | k in ZZ} nowhere (the family was kept whole, offering 1/2
781
+ # as an integer: fourth review). Over NN the members also start at 0.
782
+ def rational_family(set, a, b, wanted, sign)
783
+ return [set] unless set.domain == ZZ && wanted <= ZZ
784
+ p, q = b.numerator, b.denominator
785
+ r, s = a.numerator, a.denominator
786
+ modulus = q * s
787
+ coefficient = (s * p) % modulus
788
+ target = (-q * r) % modulus
789
+ g = coefficient.gcd(modulus)
790
+ return [] unless (target % g).zero?
791
+ period = modulus / g
792
+ k0 = period > 1 ? (target / g) * NumberTheory.invmod(coefficient / g, period) % period : 0
793
+ base = a + b * k0
794
+ step = (b * period).abs # the same progression either way round
795
+ j = Var.new(:k)
796
+ lower = { positive: 1, nonnegative: 0 }[sign] || (wanted == NN ? 0 : nil)
797
+ upper = { negative: -1, nonpositive: 0 }[sign]
798
+ return [] if lower && upper
799
+ if lower
800
+ start = base + step * ((lower - base) / step).ceil
801
+ return [NN] if start.zero? && step == 1
802
+ return [ImageSet.new((Num.new(start) + Num.new(step) * j).simplify, [j], NN)]
803
+ end
804
+ if upper
805
+ start = base + step * ((upper - base) / step).floor
806
+ return [ImageSet.new((Num.new(start) - Num.new(step) * j).simplify, [j], NN)]
807
+ end
808
+ return [ZZ] if base.denominator == 1 && step == 1
809
+ [ImageSet.new((Num.new(base) + Num.new(step) * j).simplify, [j], ZZ)]
810
+ end
811
+
812
+ # What each declared sign allows a root to be.
813
+ ALLOWED_SIGNS = { positive: %i[positive], nonnegative: %i[positive zero],
814
+ negative: %i[negative], nonpositive: %i[negative zero] }.freeze
815
+
816
+ def wrong_sign?(root, sign)
817
+ found = root_sign(root)
818
+ !found.nil? && !ALLOWED_SIGNS.fetch(sign, %i[positive negative zero]).include?(found)
819
+ end
820
+
821
+ # The sign of a constant root, or nil when it is not decided: a root
822
+ # with a parameter in it, or one that is not real.
823
+ def root_sign(root)
824
+ return nil unless root.variables.empty?
825
+ return :zero if Scalar.zero?(root)
826
+ value = Analysis.numeric(root)
827
+ return nil if value.nil?
828
+ value.positive? ? :positive : :negative
829
+ end
830
+
831
+ # An integer parameter for the periodic solutions, avoiding the names in use.
832
+ def period_parameter(f, x)
833
+ taken = f.variables | [x.name]
834
+ name = %i[k n m j].find { |candidate| !taken.include?(candidate) }
835
+ name ||= (1..).lazy.map { |i| :"k#{i}" }.find { |candidate| !taken.include?(candidate) }
836
+ Var.new(name)
837
+ end
838
+
839
+ def to_zero(target) = target.is_a?(Equation) ? Sub.new(target.lhs, target.rhs) : Expression.lift(target)
840
+
841
+ def piecewise?(target)
842
+ return true if target.is_a?(Expression) && target.each_node.any? { |n| n.is_a?(Piecewise) }
843
+ target.is_a?(Equation) && (target.lhs.each_node.any? { |n| n.is_a?(Piecewise) } || target.rhs.each_node.any? { |n| n.is_a?(Piecewise) })
844
+ end
845
+
846
+ # Every branch is solved on its own piece (piecewise.rb).
847
+ def piecewise(target, vars)
848
+ return Piecewises.solve(Piecewises.hoist(target), Num.new(0), vars) unless target.is_a?(Equation)
849
+ pw = Piecewises.hoist(Sub.new(target.lhs, target.rhs).simplify)
850
+ return Piecewises.solve(pw, Num.new(0), vars) if pw.is_a?(Piecewise)
851
+ Solve.solve(pw, vars)
852
+ end
853
+
854
+ def variable(f, vars)
855
+ return Expression.lift(vars) if vars
856
+ free = f.variables
857
+ raise ArgumentError, "solve: which variable? #{f} has #{free.size} variables" unless free.size == 1
858
+ Var.new(free.first)
859
+ end
860
+
861
+ def depends?(e, x) = e.variables.include?(x.name)
862
+
863
+ def dedupe(list)
864
+ out = []
865
+ list.each { |s| out << s unless out.any? { |o| o == s } }
866
+ out
867
+ end
868
+
869
+ # ---- one equation, one unknown ----------------------------------------------
870
+
871
+ def univariate(f, x, depth, all: false)
872
+ return [] if depth > MAX_DEPTH
873
+ f = f.simplify
874
+ raise ArgumentError, "every value of #{x} is a solution" if Scalar.zero?(f)
875
+ return [] unless depends?(f, x)
876
+
877
+ cases = case_nodes(f, x)
878
+ return case_split(f, x, cases, depth, all: all) unless cases.empty?
879
+
880
+ num, den = numerator_denominator(f, x)
881
+ coeffs = polynomial_coefficients(num, x)
882
+ # a zero numerator is an identity off the poles, not "no roots"
883
+ raise ArgumentError, "every value of #{x} is a solution" if coeffs&.all? { |c| Scalar.zero?(c) }
884
+ roots = coeffs ? polynomial_roots(coeffs) : transcendental(num, x, depth, all: all)
885
+ roots = roots.map(&:simplify).reject { |r| Scalar.zero?(den.subs(x => r).simplify) }
886
+ verify(f, x, roots)
887
+ end
888
+
889
+ # The distinct abs(u) and sign(u) in f whose u depends on x: the places
890
+ # where f is one expression to the left of a point and another to the right.
891
+ def case_nodes(f, x)
892
+ f.each_node.select { |n| n.is_a?(Fn) && CASES.include?(n.name) && n.args.size == 1 && depends?(n.args.first, x) }.uniq
893
+ end
894
+
895
+ # |u| is u where u >= 0 and -u where u <= 0, and sign(u) is 1, -1 and 0
896
+ # in the same three places, so an equation with n of them is 2**n
897
+ # equations without any, together with the points where a sign vanishes.
898
+ # Every candidate goes back through verify, which drops the roots of a
899
+ # branch that do not lie in it, so the answer is the case split a
900
+ # student writes - and |x| - 1 = 0 no longer comes back empty.
901
+ # A branch that vanishes as a whole - |x| - x is zero on all of x >= 0 -
902
+ # contributes the set where its cases hold, and the answer is then a
903
+ # set: the points of the other branches with those sets (the answer was
904
+ # an ArgumentError naming the set: fourth review, S18).
905
+ def case_split(f, x, nodes, depth, all: false)
906
+ raise RCAS::Unsupported, "can't solve #{f} = 0 for #{x}: too many cases" if nodes.size > MAX_CASES
907
+ sets = []
908
+ roots = [1, -1].repeated_permutation(nodes.size).flat_map { |signs| branch_roots(f, x, nodes, signs, depth, sets, all: all) }
909
+ roots += nodes.select { |n| n.name == :sign }.flat_map { |n| univariate(n.args.first, x, depth + 1, all: all) }
910
+ points = verify(f, x, dedupe(roots.map(&:simplify)))
911
+ return points if sets.empty?
912
+ real = points.select { |p| p.is_a?(Expression) && Analysis.numeric(p) }
913
+ raise Whole, sets.reduce(RealSet.new(real.map { |p| Interval.point(p) })) { |acc, set| acc | set }
914
+ end
915
+
916
+ def branch_roots(f, x, nodes, signs, depth, sets = [], all: false)
917
+ branch = nodes.zip(signs).map { |node, sign| [node, branch_value(node, sign)] }.to_h
918
+ univariate(f.subs(branch), x, depth + 1, all: all)
919
+ rescue ArgumentError => e
920
+ raise unless e.message.start_with?("every value")
921
+ conditions = branch_conditions(nodes, signs)
922
+ set = begin
923
+ Inequalities.solve(conditions, x)
924
+ rescue NotImplementedError, RCAS::Unsupported
925
+ nil
926
+ end
927
+ raise ArgumentError, "every #{x} with #{conditions.join(' and ')} solves #{f} = 0" unless set.is_a?(RealSet)
928
+ sets << set
929
+ []
930
+ end
931
+
932
+ def branch_value(node, sign)
933
+ return Num.new(sign) if node.name == :sign
934
+ sign.positive? ? node.args.first : Neg.new(node.args.first)
935
+ end
936
+
937
+ def branch_conditions(nodes, signs)
938
+ nodes.zip(signs).map do |node, sign|
939
+ strict = node.name == :sign
940
+ op = if sign.positive? then strict ? :> : :>=
941
+ else strict ? :< : :<=
942
+ end
943
+ Inequality.new(node.args.first, op, 0)
944
+ end
945
+ end
946
+
947
+ # f = num / den with den the product of the x-dependent denominators.
948
+ def numerator_denominator(f, x)
949
+ _, table = Expand.table(f)
950
+ den_factors = {}
951
+ table.each_key do |factors|
952
+ factors.each do |base, exp|
953
+ next unless exp.is_a?(Integer) && exp.negative? && depends?(base, x)
954
+ den_factors[base] = [den_factors[base] || 0, -exp].max
955
+ end
956
+ end
957
+ # no denominator left once expanded: (x**2 - x)/x is x - 1, and the
958
+ # unexpanded quotient has no value at 0 (fourth review, S17)
959
+ return [f.expand, Num.new(1)] if den_factors.empty?
960
+ constant, table = Expand.table(f)
961
+ cleared = {}
962
+ cleared[den_factors.dup] = constant unless constant.zero?
963
+ table.each do |factors, coeff|
964
+ merged = factors.dup
965
+ den_factors.each do |base, k|
966
+ e = Simplify.add_exponents(merged[base] || 0, k)
967
+ e.is_a?(Numeric) && e.zero? ? merged.delete(base) : merged[base] = e
968
+ end
969
+ cleared[merged] = (cleared[merged] || 0) + coeff
970
+ end
971
+ [Simplify.rebuild_sum(0, cleared).expand, Simplify.rebuild_product(1, den_factors)]
972
+ end
973
+
974
+ # Coefficients of f as a polynomial in x (index = degree), or nil.
975
+ def polynomial_coefficients(f, x)
976
+ constant, table = Expand.table(f)
977
+ coeffs = Hash.new { Num.new(0) }
978
+ coeffs[0] = Num.new(constant)
979
+ table.each do |factors, coeff|
980
+ degree = 0
981
+ c = Num.new(coeff)
982
+ factors.each do |base, exp|
983
+ if base == x
984
+ return nil unless exp.is_a?(Integer) && exp >= 0
985
+ degree += exp
986
+ elsif depends?(base, x) || (exp.is_a?(Expression) && depends?(exp, x))
987
+ return nil
988
+ else
989
+ c *= Simplify.power_node(base, exp)
990
+ end
991
+ end
992
+ coeffs[degree] = coeffs[degree] + c
993
+ end
994
+ list = (0..coeffs.keys.max).map { |k| coeffs[k].simplify }
995
+ list.pop while list.size > 1 && Scalar.zero?(list.last)
996
+ list
997
+ end
998
+
999
+ # Roots (with multiplicity) of the polynomial with these coefficients.
1000
+ def polynomial_roots(coeffs)
1001
+ coeffs = coeffs.dup
1002
+ coeffs.pop while coeffs.size > 1 && Scalar.zero?(coeffs.last)
1003
+ n = coeffs.size - 1
1004
+ return [] if n < 1
1005
+ return exact_roots(coeffs) if coeffs.all? { |c| c.is_a?(Num) && (c.value.is_a?(Integer) || c.value.is_a?(Rational)) }
1006
+ return float_roots(coeffs) if n > 2 && coeffs.all? { |c| c.is_a?(Num) && c.value.is_a?(Numeric) && c.value.real? }
1007
+
1008
+ factored = symbolic_roots_by_factoring(coeffs)
1009
+ return factored if factored
1010
+
1011
+ case n
1012
+ when 1 then [(-coeffs[0] / coeffs[1]).cancel]
1013
+ when 2 then quadratic(coeffs[2], coeffs[1], coeffs[0])
1014
+ else raise RCAS::Unsupported, "can't solve a degree #{n} polynomial with symbolic coefficients exactly"
1015
+ end
1016
+ end
1017
+
1018
+ # (x - a)*(x - 1) = 0 has the roots a and 1: factor over QQ[x, params]
1019
+ # first and read linear factors off; quadratic factors use the formula.
1020
+ def symbolic_roots_by_factoring(coeffs)
1021
+ x = Var.new(:_x)
1022
+ expr = coeffs.each_with_index.reduce(Num.new(0)) { |acc, (c, k)| acc + c * x**k }
1023
+ vars = expr.variables
1024
+ return nil unless vars.include?(:_x)
1025
+ return nil if coeffs.size > 3 && irreducible_image?(coeffs)
1026
+ poly = Polynomial.from_expr(QQ[*vars], expr)
1027
+ factors = poly.factor.factors.map(&:first).reject { |g| g.degree(:_x).zero? }
1028
+ return nil if factors.size <= 1 && factors.first&.degree(:_x).to_i >= 2
1029
+ factors.flat_map do |g|
1030
+ cs = (0..g.degree(:_x)).map { |k| g.coefficient_in(:_x, k).to_expr }
1031
+ case cs.size - 1
1032
+ when 1 then [(-cs[0] / cs[1]).cancel]
1033
+ when 2 then quadratic(cs[2], cs[1], cs[0])
1034
+ else raise RCAS::Unsupported, "can't solve a degree #{cs.size - 1} factor with symbolic coefficients exactly"
1035
+ end
1036
+ end
1037
+ rescue DomainError
1038
+ nil
1039
+ end
1040
+
1041
+ # A factor of f in x, of any degree, survives setting the parameters to
1042
+ # integers where the leading coefficient does not vanish, so an
1043
+ # irreducible image of the full degree proves f has no factor to find:
1044
+ # a quartic with three parameters sat in Kronecker's substitution for
1045
+ # half a minute before the same refusal (third review, section 5).
1046
+ def irreducible_image?(coeffs)
1047
+ params = coeffs.flat_map(&:variables).uniq
1048
+ return false if params.empty?
1049
+ random = Random.new(20260923)
1050
+ 2.times do
1051
+ point = params.to_h { |v| [Var.new(v), Num.new(random.rand(2..97))] }
1052
+ values = coeffs.map { |c| c.subs(point).simplify }
1053
+ next unless values.all? { |v| v.is_a?(Num) && (v.value.is_a?(Integer) || v.value.is_a?(Rational)) }
1054
+ next if values.last.value.zero?
1055
+ image = Polynomial.new(QQ[:_x], values.each_with_index.reject { |v, _| v.value.zero? }.to_h { |v, k| [[k], v] })
1056
+ found = image.factor.factors
1057
+ return true if found.size == 1 && found.first.last == 1 && found.first.first.degree == coeffs.size - 1
1058
+ end
1059
+ false
1060
+ rescue StandardError => rescued
1061
+ RCAS.guard!(rescued)
1062
+ false
1063
+ end
1064
+
1065
+ def quadratic(a, b, c)
1066
+ disc = (b**2 - 4 * a * c).expand
1067
+ return [(-b / (2 * a)).simplify] * 2 if Scalar.zero?(disc)
1068
+ root = square_root(disc)
1069
+ [((-b - root) / (2 * a)).simplify, ((-b + root) / (2 * a)).simplify]
1070
+ end
1071
+
1072
+ # sqrt(d) with perfect squares taken out: sqrt((a - 1)**2) => a - 1,
1073
+ # sqrt(4*a) => 2*sqrt(a), sqrt(a**2 + a**4) => a*sqrt(1 + a**2). For
1074
+ # any complex values c*sqrt(w) is one of +-sqrt(c**2*w), so this is
1075
+ # right for the +- pair of the quadratic formula, the one caller, and
1076
+ # for nothing else.
1077
+ def square_root(d)
1078
+ return RCAS.sqrt(d) if d.variables.empty?
1079
+ fact = d.to_poly.factor
1080
+ return RCAS.sqrt(d) if fact.factors.all? { |_, m| m == 1 }
1081
+ outside = Num.new(1)
1082
+ inside = Num.new(fact.unit.value)
1083
+ fact.factors.each do |g, m|
1084
+ outside *= g.to_expr**(m / 2) if m >= 2
1085
+ inside *= g.to_expr if m.odd?
1086
+ end
1087
+ (outside * RCAS.sqrt(inside.simplify)).simplify
1088
+ rescue DomainError, NotImplementedError, RCAS::Unsupported
1089
+ RCAS.sqrt(d)
1090
+ end
1091
+
1092
+ def exact_roots(coeffs)
1093
+ ring = QQ[:_x]
1094
+ poly = Polynomial.new(ring, coeffs.each_with_index.to_h { |c, k| [[k], c] })
1095
+ poly.factor.factors.flat_map do |g, m|
1096
+ roots =
1097
+ if g.degree == 1
1098
+ [Num.new(Simplify.normalize_number(Rational(-g.coeff(0).value, g.coeff(1).value)))]
1099
+ elsif g.degree == 2
1100
+ quadratic(g.coeff(2), g.coeff(1), g.coeff(0))
1101
+ elsif g.terms.size == 2 && g.coeff(0) != 0
1102
+ binomial_roots(g)
1103
+ elsif g.degree == 4 && g.terms.keys.all? { |e| e[0].even? }
1104
+ biquadratic_roots(g)
1105
+ else
1106
+ (0...g.degree).map { |i| RootOf.new(g.primitive_part, i) }
1107
+ end
1108
+ roots * m
1109
+ end
1110
+ end
1111
+
1112
+ # a x^4 + b x^2 + c = 0: x = +-sqrt(r) for the two roots r of a r^2 + b r + c
1113
+ def biquadratic_roots(g)
1114
+ quadratic(g.coeff(4), g.coeff(2), g.coeff(0)).flat_map do |r|
1115
+ root = RCAS.sqrt(r)
1116
+ [Simplify.negate(root).simplify, root]
1117
+ end
1118
+ end
1119
+
1120
+ # a x^k + b = 0 => |b/a|^(1/k) times the k-th roots of +-1
1121
+ def binomial_roots(g)
1122
+ k = g.degree
1123
+ c = Rational(-g.coeff(0).value, g.coeff(k).value)
1124
+ radius = Pow.new(Num.new(c.abs), Num.new(Rational(1, k)))
1125
+ (0...k).map do |j|
1126
+ angle = c.positive? ? Rational(2 * j, k) : Rational(2 * j + 1, k)
1127
+ (radius * Fn.new(:exp, [I * PI * angle])).simplify
1128
+ end
1129
+ end
1130
+
1131
+ # Durand-Kerner iteration for an irreducible numeric polynomial.
1132
+ def numeric_roots(g)
1133
+ n = g.degree
1134
+ lc = g.coeff(n).value.to_f
1135
+ a = (0..n).map { |k| g.coeff(k).value.to_f / lc }
1136
+ roots = (0...n).map { |k| Complex(0.4, 0.9)**k }
1137
+ value = ->(z) { a.each_with_index.reduce(0) { |acc, (c, k)| acc + c * z**k } }
1138
+ 500.times do
1139
+ moved = 0.0
1140
+ roots = roots.each_with_index.map do |z, i|
1141
+ denom = roots.each_with_index.reduce(1) { |acc, (w, j)| i == j ? acc : acc * (z - w) }
1142
+ nz = z - value.call(z) / denom
1143
+ moved += (nz - z).abs
1144
+ nz
1145
+ end
1146
+ break if moved < 1e-14
1147
+ end
1148
+ # Which roots are real is an exact question: Sturm's theorem counts
1149
+ # them, and that many roots nearest the real axis are the real ones
1150
+ # (an |Im| < 1e-9 test and a rounding to twelve digits decided it
1151
+ # before: fourth review, 2.4). Conjugate pairs are made exact pairs.
1152
+ real = sturm_count(g)
1153
+ nearest = roots.each_with_index.sort_by { |z, _| z.imaginary.abs }.first(real).map(&:last)
1154
+ roots.each_with_index.map do |z, i|
1155
+ Num.new(nearest.include?(i) ? z.real : z)
1156
+ end.sort_by { |r| r.value.is_a?(Complex) ? [r.value.real, r.value.imaginary] : [r.value, 0] }
1157
+ end
1158
+
1159
+ # Float coefficients (a Float matrix's characteristic polynomial, the
1160
+ # fifth review's L10): Durand-Kerner in Floats. A Float polynomial has
1161
+ # no exact real-root count, so a root is real when no other root is its
1162
+ # conjugate - the roots of a real polynomial come in conjugate pairs,
1163
+ # and one left without a partner can only be real.
1164
+ def float_roots(coeffs)
1165
+ lead = coeffs.last.value.to_f
1166
+ a = coeffs.map { |c| c.value.to_f / lead }
1167
+ zero = a.index { |c| !c.zero? } # x**m divides: those roots are exactly 0
1168
+ a = a.drop(zero)
1169
+ roots = durand_kerner(a) + [0.0] * zero
1170
+ unit = [roots.map(&:abs).max, Float::MIN].max
1171
+ k = scaled_size(a, unit)
1172
+ roots = polished_clusters(roots, a, unit, k)
1173
+ noise = 10 * Float::EPSILON * k * unit
1174
+ paired = []
1175
+ roots.each_with_index do |z, i|
1176
+ next if paired.include?(i)
1177
+ partner = roots.each_index.find { |j| j != i && !paired.include?(j) && (roots[j] - z.conj).abs <= 2 * noise && z.imaginary.abs > noise }
1178
+ paired.push(i, partner) if partner
1179
+ end
1180
+ roots.each_with_index.map do |z, i|
1181
+ # a real part at the level of rounding is none: the rotation's +-i
1182
+ # came out as -2e-17 + i
1183
+ z = Complex(z.real.abs <= noise ? 0.0 : z.real, z.imaginary) if paired.include?(i)
1184
+ Num.new(paired.include?(i) ? z : z.real)
1185
+ end.sort_by { |r| r.value.is_a?(Complex) ? [1, r.value.real, r.value.imaginary] : [0, r.value, 0] }
1186
+ end
1187
+
1188
+ def durand_kerner(a)
1189
+ n = a.size - 1
1190
+ return [] if n < 1
1191
+ roots = (0...n).map { |k| Complex(0.4, 0.9)**k }
1192
+ value = ->(z) { a.each_with_index.reduce(0) { |acc, (c, k)| acc + c * z**k } }
1193
+ 500.times do
1194
+ moved = 0.0
1195
+ roots = roots.each_with_index.map do |z, i|
1196
+ denom = roots.each_with_index.reduce(1) { |acc, (w, j)| i == j ? acc : acc * (z - w) }
1197
+ step = denom.zero? ? 0 : value.call(z) / denom
1198
+ moved += step.abs
1199
+ z - step
1200
+ end
1201
+ break if moved < 1e-15
1202
+ end
1203
+ roots
1204
+ end
1205
+
1206
+ # The coefficients of the monic polynomial with its roots scaled into
1207
+ # the unit disc: how large they are is how much rounding a root sees.
1208
+ def scaled_size(a, unit)
1209
+ n = a.size - 1
1210
+ [a.each_with_index.map { |c, i| c.abs / unit**(n - i) }.max, 1.0].max
1211
+ end
1212
+
1213
+ # A root of multiplicity m is found only to about (eps*k)**(1/m) of the
1214
+ # scale, as m roots scattered round it: a triple eigenvalue 2 came
1215
+ # back 2.5e-5 to 4.6e-5 apart. So m roots that lie within ten times
1216
+ # that of each other are one root of multiplicity m, and Newton's
1217
+ # method for it, z - m*p(z)/p'(z), polishes their mean; two roots
1218
+ # 1e-6 apart are further apart than a double root can scatter, and
1219
+ # stay two. Largest clusters first. The radius is relative to the
1220
+ # roots, not to 1: eigenvalues 1e-8, 3e-8 and 5e-8 were one triple
1221
+ # root (the sixth review's preflight).
1222
+ def polished_clusters(roots, a, unit, k)
1223
+ value = ->(z) { a.each_with_index.reduce(0) { |acc, (c, i)| acc + c * z**i } }
1224
+ slope = ->(z) { a.each_with_index.reduce(0) { |acc, (c, i)| i.zero? ? acc : acc + i * c * z**(i - 1) } }
1225
+ left = roots.dup
1226
+ found = []
1227
+ roots.size.downto(2) do |m|
1228
+ radius = 10 * (Float::EPSILON * k)**(1.0 / m) * unit
1229
+ loop do
1230
+ cluster = left.lazy.map { |z| left.sort_by { |w| (w - z).abs }.first(m) }
1231
+ .find { |group| group.size == m && group.combination(2).all? { |p, q| (p - q).abs <= radius } }
1232
+ break unless cluster
1233
+ cluster.each { |z| left.delete_at(left.index(z)) }
1234
+ z = cluster.sum / m
1235
+ 30.times do
1236
+ d = slope.call(z)
1237
+ break if d.zero?
1238
+ step = m * value.call(z) / d
1239
+ break if step.abs <= 1e-16 * unit
1240
+ z -= step
1241
+ end
1242
+ found.concat([z] * m)
1243
+ end
1244
+ end
1245
+ found + left
1246
+ end
1247
+
1248
+ # The number of distinct real roots of a polynomial over QQ: the sign
1249
+ # changes of its Sturm sequence at -oo less those at oo.
1250
+ def sturm_count(g)
1251
+ g = g.to_ring(QQ[*g.ring.vars]) unless g.ring.base == QQ # the remainders need a field
1252
+ sequence = [g, g.derivative]
1253
+ until sequence.last.zero? || sequence.last.degree.zero?
1254
+ remainder = sequence[-2] % sequence[-1]
1255
+ break if remainder.zero?
1256
+ sequence << -remainder
1257
+ end
1258
+ at = lambda do |sign|
1259
+ values = sequence.map do |q|
1260
+ lead = q.leading_coefficient.value
1261
+ lead = -lead if sign.negative? && q.degree.odd?
1262
+ lead <=> 0
1263
+ end.reject(&:zero?)
1264
+ values.each_cons(2).count { |a, b| a != b }
1265
+ end
1266
+ at.call(-1) - at.call(1)
1267
+ end
1268
+
1269
+ # ---- transcendental equations -------------------------------------------------
1270
+
1271
+ def transcendental(f, x, depth, all: false)
1272
+ atoms = f.each_node.select { |n| depends?(n, x) && transcendental_atom?(n) }.uniq
1273
+ atoms = atoms.sort_by { |n| -n.each_node.count }
1274
+ common = common_exponential(atoms, x) || common_power(atoms, x)
1275
+ atoms.unshift(common) if common && !atoms.include?(common)
1276
+ t = Var.new(:"_s#{depth}")
1277
+
1278
+ atoms.each do |u|
1279
+ g = replace_atom(f, u, x, t)
1280
+ next if g.nil? || depends?(g, x)
1281
+ inverted = begin
1282
+ univariate(g, t, depth + 1).flat_map { |v| invert(u, v, x, depth, all: all) }
1283
+ rescue NotImplementedError, RCAS::Unsupported, ArgumentError
1284
+ next # another atom, or one of the rules below, may do better
1285
+ end
1286
+ return inverted
1287
+ end
1288
+
1289
+ # x**(p/q): substitute t = x**(1/q)
1290
+ q = root_denominator(f, x)
1291
+ if q > 1 && !depends?(g = replace_root(f, x, q, t), x)
1292
+ # only when t = x**(1/q) replaces every x: sqrt(x)*log(x) keeps an
1293
+ # x inside the log, and "solving for t" there treated it as a
1294
+ # constant (and sqrt(x) = sqrt(2 - x) answered 2 - x)
1295
+ values = univariate(g, t, depth + 1)
1296
+ return values.map { |v| (v**q).simplify }
1297
+ end
1298
+
1299
+ product = product_equation(f, x, depth, all: all)
1300
+ return product if product
1301
+
1302
+ homogeneous = homogeneous_trig(f, x, depth, all: all)
1303
+ return homogeneous if homogeneous
1304
+
1305
+ radicals = radical_equation(f, x, depth, all: all)
1306
+ return radicals if radicals
1307
+
1308
+ logs = logarithmic_equation(f, x, depth)
1309
+ return logs if logs
1310
+
1311
+ raise RCAS::Unsupported, "can't solve #{f} = 0 for #{x}; nsolve(#{f}, #{x}: a..b) finds a root numerically"
1312
+ end
1313
+
1314
+ # A product vanishes where one of its factors does, so a product no
1315
+ # rule can take whole is still three easy equations when it is written
1316
+ # as one: (x + 1)*(x - 2)*sin(x). Every factor has to be solvable, or
1317
+ # the answer would be missing roots without saying so; a factor in the
1318
+ # denominator is not one of them (`numerator_denominator` has already
1319
+ # taken those away, and its zeros are poles rather than roots).
1320
+ def product_equation(f, x, depth, all: false)
1321
+ _, factors = Simplify.factorize(f)
1322
+ pieces = factors.filter_map do |base, exponent|
1323
+ next nil if exponent.is_a?(Numeric) && Simplify.negative?(exponent)
1324
+ # a positive power vanishes exactly where its base does, and a
1325
+ # symbolic exponent is the factor itself: exp(u) is stored as
1326
+ # EXP**u, whose base knows nothing about x
1327
+ piece = exponent.is_a?(Numeric) && exponent.positive? ? base : Simplify.power_node(base, exponent)
1328
+ depends?(piece, x) ? piece : nil
1329
+ end
1330
+ if pieces.size < 2
1331
+ # a product the normal form has already multiplied out:
1332
+ # (x - 2)*log(x)/x arrives as -2*log(x) + x*log(x)
1333
+ common, rest = Simplify.common_factor(f)
1334
+ pieces = [common, rest].compact.select { |piece| depends?(piece, x) }
1335
+ return nil if pieces.size < 2
1336
+ end
1337
+ # A root of one factor is a root of the product only where the rest of
1338
+ # the product is defined: log(x)*(x**2 - 4) does not vanish at -2.
1339
+ defined_roots(f, x, pieces.flat_map { |piece| univariate(piece, x, depth + 1, all: all) })
1340
+ rescue NotImplementedError, RCAS::Unsupported
1341
+ nil # one factor rcas cannot solve: the product is no easier
1342
+ end
1343
+
1344
+ # An equation in which every term has the same total degree in sin(u)
1345
+ # and cos(u) is a polynomial in tan(u): divide through by cos(u)**n.
1346
+ # sin(x) + cos(x) = 0 is tan(x) = -1, which rcas solves, and there was
1347
+ # no rule that saw it. Where the division loses the zeros of cos(u) -
1348
+ # which it does only when no term is a pure power of sin(u) - they are
1349
+ # put back, and a term short of the top degree is raised to it by
1350
+ # `homogenize` first.
1351
+ def homogeneous_trig(f, x, depth, all: false)
1352
+ arguments = f.each_node.filter_map do |node|
1353
+ node.args.first if node.is_a?(Fn) && %i[sin cos].include?(node.name) && depends?(node, x)
1354
+ end.uniq
1355
+ return nil unless arguments.size == 1
1356
+ u = arguments.first
1357
+ sine = Fn.new(:sin, [u])
1358
+ cosine = Fn.new(:cos, [u])
1359
+ f = homogenize(f, sine, cosine)
1360
+ return nil if f.nil?
1361
+ constant, table = Expand.table(f)
1362
+ return nil unless constant.zero? && table.size > 1
1363
+
1364
+ t = Var.new(:"_t#{depth}")
1365
+ degrees = []
1366
+ polynomial = Num.new(0)
1367
+ table.each do |factors, coefficient|
1368
+ sines = factors[sine] || 0
1369
+ cosines = factors[cosine] || 0
1370
+ rest = factors.reject { |base, _| base == sine || base == cosine }
1371
+ return nil unless [sines, cosines].all? { |e| e.is_a?(Integer) && !e.negative? }
1372
+ return nil if rest.keys.any? { |base| depends?(base, x) }
1373
+ degrees << sines + cosines
1374
+ polynomial += Simplify.rebuild_product(coefficient, rest) * t**sines
1375
+ end
1376
+ return nil unless degrees.uniq.size == 1 && degrees.first.positive?
1377
+
1378
+ roots = univariate(polynomial.simplify, t, depth + 1)
1379
+ answers = roots.flat_map { |value| invert(Fn.new(:tan, [u]), value, x, depth, all: all) }
1380
+ # cos(u) = 0 solves it too when there is no pure sin(u)**n term
1381
+ answers += univariate(cosine, x, depth + 1, all: all) if Coefficients.coeff(polynomial.simplify, t, degrees.first).nil? ||
1382
+ Scalar.zero?(Coefficients.coeff(polynomial.simplify, t, degrees.first))
1383
+ answers
1384
+ rescue NotImplementedError, RCAS::Unsupported
1385
+ nil
1386
+ end
1387
+
1388
+ # sin(x)*cos(x) = 1/2 is not homogeneous as written and becomes so when
1389
+ # the 1/2 is read as (sin(x)**2 + cos(x)**2)/2 - the classical trick,
1390
+ # and the reason the identity is taught before the equation is set. A
1391
+ # term short of the top degree by an even number is raised to it that
1392
+ # way; an odd gap (sin(x) = 1/2) has no such reading and the rule
1393
+ # declines, which leaves the equation to the atom substitution that
1394
+ # already answers it.
1395
+ def homogenize(f, sine, cosine)
1396
+ constant, table = Expand.table(f)
1397
+ entries = table.map do |factors, coefficient|
1398
+ powers = [factors[sine] || 0, factors[cosine] || 0]
1399
+ return nil unless powers.all? { |e| e.is_a?(Integer) && !e.negative? }
1400
+ [powers.sum, Simplify.rebuild_product(coefficient, factors)]
1401
+ end
1402
+ entries << [0, Expression.lift(constant)] unless constant.zero?
1403
+ return nil if entries.empty?
1404
+ top = entries.map(&:first).max
1405
+ return f if entries.all? { |degree, _| degree == top }
1406
+ return nil unless top.positive? && entries.all? { |degree, _| ((top - degree) % 2).zero? }
1407
+ pythagoras = Simplify.power_node(sine, 2) + Simplify.power_node(cosine, 2)
1408
+ raised = entries.map { |degree, term| term * pythagoras**((top - degree) / 2) }
1409
+ Expand.expand(raised.inject(:+)).simplify
1410
+ end
1411
+
1412
+ # sqrt(u) = v: the radical on one side, both sides to the q-th power,
1413
+ # and the answers kept only where the original equation is defined.
1414
+ # Raising to a power invents roots, and `verify` drops those.
1415
+ #
1416
+ # Two radicals go one to each side, sqrt(x) = sqrt(2 - x), and the
1417
+ # squared equation has one radical fewer; what is left is squared again
1418
+ # one level down.
1419
+ # The answer of an equation that is a whole set rather than points.
1420
+ class Whole < StandardError
1421
+ attr_reader :set
1422
+
1423
+ def initialize(set)
1424
+ @set = set
1425
+ super("the solutions form the set #{set}")
1426
+ end
1427
+ end
1428
+
1429
+ def radical_equation(f, x, depth, all: false)
1430
+ constant, terms = Simplify.termize(f)
1431
+ with, without = terms.partition { |factors, _| root_index(factors, x) }
1432
+ return nil unless [1, 2].include?(with.size)
1433
+ q = with.map { |factors, _| root_index(factors, x) }.max
1434
+ return nil if q.nil? || q > 3
1435
+ side = Simplify.rebuild_sum(0, with.first(1).to_h)
1436
+ others = with.drop(1) + without
1437
+ rest = Simplify.rebuild_sum(-constant, others.to_h.transform_values { |c| -c })
1438
+ g = (Expand.expand(Simplify.power_node(side, q)) - Expand.expand(Simplify.power_node(rest, q))).simplify
1439
+ if Scalar.zero?(g)
1440
+ # sqrt(x**2) = -x squares to x**2 = x**2, which says nothing: an even
1441
+ # root is the non-negative one, so the equation holds exactly where
1442
+ # the other side is not negative - that set, or a refusal below the
1443
+ # top (an "every value" verdict about the square was wrong: S18)
1444
+ raise RCAS::Unsupported, "squaring #{f} = 0 gives an identity" unless q.even? && depth.zero? && with.size == 1 && side_is_root?(side, x)
1445
+ raise Whole, Inequalities.solve(Inequality.new(rest, :>=, 0), x)
1446
+ end
1447
+ if root_denominator(g, x) > 1 || g.each_node.any? { |n| root_index_of(n, x) }
1448
+ # still a radical: fine if there are fewer radical terms than before
1449
+ remaining = Simplify.termize(g).last.count { |factors, _| root_index(factors, x) }
1450
+ return nil unless remaining < with.size
1451
+ end
1452
+ # all: goes down too - sqrt(exp(x)) = exp(x) squares to a family,
1453
+ # and one period of it was [0] - and the families squaring brings
1454
+ # are checked like the points
1455
+ found = univariate(g, x, depth + 1, all: all)
1456
+ families, points = found.partition { |r| !family_parameters(r, x, f).empty? }
1457
+ defined_roots(f, x, verify(f, x, points) + verified_families(f, x, families))
1458
+ rescue NotImplementedError, RCAS::Unsupported
1459
+ nil
1460
+ end
1461
+
1462
+ # Squaring can add a family as easily as a point: sqrt(exp(x)) =
1463
+ # -exp(x) squares to the same equation as sqrt(exp(x)) = exp(x). A
1464
+ # family is kept when its members at six indices all satisfy f,
1465
+ # decided exactly, dropped when none does, and refused when some do
1466
+ # and some do not - which members those are is not written here.
1467
+ VERIFIED_INDICES = [0, 1, -1, 2, -2, 3].freeze
1468
+
1469
+ # Inside univariate a family is still a root in a name the equation
1470
+ # does not have (`family` makes the ImageSet at the top), and that name
1471
+ # runs over the integers.
1472
+ def family_parameters(root, x, f) = root.is_a?(Expression) ? root.variables - f.variables - [x.name] : []
1473
+
1474
+ def verified_families(f, x, families)
1475
+ families.select do |family|
1476
+ names = family_parameters(family, x, f)
1477
+ verdicts = VERIFIED_INDICES.map do |k|
1478
+ member = family.subs(names.to_h { |name| [name, Num.new(k)] }).simplify
1479
+ residual = f.subs(x => member).simplify
1480
+ Integrate.defined_value?(residual) ? Decide.zero?(residual) : false
1481
+ rescue ZeroDivisionError
1482
+ false
1483
+ end
1484
+ next true if verdicts.all?(true) || verdicts.include?(nil)
1485
+ next false if verdicts.none?(true)
1486
+ raise RCAS::Unsupported, "squaring #{f} = 0 gives the family #{family}, of which only some members solve it"
1487
+ end
1488
+ end
1489
+
1490
+ # log(u) + log(v) = c: one logarithm instead of two, which the atom
1491
+ # substitution can then invert.
1492
+ def logarithmic_equation(f, x, depth)
1493
+ logs = f.each_node.count { |n| n.is_a?(Fn) && n.name == :log && depends?(n, x) }
1494
+ return nil unless logs > 1
1495
+ # every candidate is verified against the equation, so the logarithms
1496
+ # may be combined as if their arguments were positive
1497
+ combined = Trigonometry.logcombine(f, force: true).simplify
1498
+ return nil if combined == f
1499
+ defined_roots(f, x, univariate(combined, x, depth + 1))
1500
+ rescue NotImplementedError, RCAS::Unsupported
1501
+ nil
1502
+ end
1503
+
1504
+ # A single radical with coefficient 1: sqrt(u) itself, so that
1505
+ # sqrt(u) = rest holds exactly where rest >= 0 once the squares agree.
1506
+ def side_is_root?(side, x)
1507
+ coeff, factors = Simplify.factorize(side)
1508
+ coeff == 1 && factors.size == 1 && root_index_of(Simplify.power_node(*factors.first), x)
1509
+ end
1510
+
1511
+ # The denominator of a fractional exponent on a base that involves x.
1512
+ def root_index(factors, x)
1513
+ factors.filter_map { |base, exp| root_index_of(Simplify.power_node(base, exp), x) }.max
1514
+ end
1515
+
1516
+ def root_index_of(node, x)
1517
+ return nil unless node.is_a?(Pow) && node.exponent.is_a?(Num)
1518
+ value = node.exponent.value
1519
+ return nil unless value.is_a?(Rational) && value.denominator > 1 && depends?(node.base, x)
1520
+ value.denominator
1521
+ end
1522
+
1523
+ # A root of the squared equation is a root of this one only where this
1524
+ # one is defined: a logarithm needs a positive argument and an even root
1525
+ # a non-negative one. That is the check a student is told to make.
1526
+ def defined_roots(f, x, roots)
1527
+ conditions = Analysis.domain_conditions(f, x)
1528
+ return roots if conditions.empty?
1529
+ roots.select do |root|
1530
+ # The sign of the condition at the root is decided, not measured
1531
+ # against 1e-9: the root 10**-12 of log(x)*(x - 10**-12) is inside
1532
+ # the domain of log, and a tolerance said it was on the edge.
1533
+ # Undecided keeps the root.
1534
+ next true unless root.is_a?(Expression) && root.variables.empty?
1535
+ conditions.all? do |condition|
1536
+ sign = begin
1537
+ Decide.sign(Expression.lift(condition.lhs - condition.rhs).subs(x => root))
1538
+ rescue StandardError => rescued
1539
+ RCAS.guard!(rescued)
1540
+ nil
1541
+ end
1542
+ next true if sign.nil?
1543
+ case condition.op
1544
+ when :> then sign == :positive
1545
+ when :>= then sign != :negative
1546
+ when :< then sign == :negative
1547
+ when :<= then sign != :positive
1548
+ when :!= then sign != :zero
1549
+ else true
1550
+ end
1551
+ end
1552
+ end
1553
+ end
1554
+
1555
+ def transcendental_atom?(n)
1556
+ (n.is_a?(Fn)) || (n.is_a?(Pow) && !n.base.is_a?(Var) && !(n.exponent.is_a?(Num) && n.exponent.integer?)) ||
1557
+ (n.is_a?(Pow) && !n.exponent.is_a?(Num))
1558
+ end
1559
+
1560
+ # exp(v/l) for the exponentials exp(r_i*v) of f, l the least common
1561
+ # denominator of the r_i: every one of them is a whole power of it
1562
+ # (exp(x/2) and exp(x/3) are powers of exp(x/6)). nil when they are not
1563
+ # rational multiples of one argument.
1564
+ def common_exponential(atoms, x)
1565
+ exps = atoms.select { |n| n.is_a?(Fn) && n.name == :exp }
1566
+ return nil if exps.size < 2
1567
+ first = exps.first.args.first
1568
+ ratios = exps.map do |e|
1569
+ r = (e.args.first / first).simplify
1570
+ return nil unless r.is_a?(Num) && (r.value.is_a?(Integer) || r.value.is_a?(Rational))
1571
+ r.value.to_r
1572
+ end
1573
+ numerator = ratios.map(&:numerator).reduce(:gcd)
1574
+ denominator = ratios.map(&:denominator).reduce(:lcm)
1575
+ Fn.new(:exp, [(first * Num.new(Rational(numerator, denominator))).simplify])
1576
+ end
1577
+
1578
+ # r**(v/l) for the powers b_i**w_i of f with positive rational bases
1579
+ # that are powers of one r (4 = 2**2, 1/8 = 2**-3), l the least common
1580
+ # denominator of the exponents e_i*w_i measured against the first:
1581
+ # every one of them is then a whole power of it, and 4**x - 3*2**x + 2
1582
+ # is t**2 - 3*t + 2 in t = 2**x. A whole power is safe over CC, since
1583
+ # b**w is exp(w*log(b)) with a real log(b). nil when the bases have no
1584
+ # common root or the exponents are not rational multiples of one another.
1585
+ def common_power(atoms, x)
1586
+ powers = atoms.select { |n| n.is_a?(Pow) && power_base(n.base) && depends?(n.exponent, x) }
1587
+ return nil if powers.size < 2
1588
+ roots = powers.map { |n| power_base(n.base) }
1589
+ return nil unless roots.map(&:first).uniq.size == 1
1590
+ exponents = powers.zip(roots).map { |n, (_, e)| (n.exponent * e).simplify }
1591
+ first = exponents.first
1592
+ ratios = exponents.map do |w|
1593
+ r = (w / first).simplify
1594
+ return nil unless r.is_a?(Num) && (r.value.is_a?(Integer) || r.value.is_a?(Rational))
1595
+ r.value.to_r
1596
+ end
1597
+ numerator = ratios.map(&:numerator).reduce(:gcd)
1598
+ denominator = ratios.map(&:denominator).reduce(:lcm)
1599
+ Pow.new(Num.new(roots.first.first), (first * Num.new(Rational(numerator, denominator))).simplify)
1600
+ end
1601
+
1602
+ # A positive rational base b as [r, e] with b = r**e, r > 1 and not
1603
+ # itself a perfect power; nil for any other base.
1604
+ def power_base(base)
1605
+ return nil unless base.is_a?(Num) && (base.value.is_a?(Integer) || base.value.is_a?(Rational))
1606
+ b = base.value.to_r
1607
+ return nil unless b.positive? && b != 1
1608
+ sign = b > 1 ? 1 : -1
1609
+ b = 1 / b if sign.negative?
1610
+ e = [b.numerator, b.denominator].max.bit_length.downto(2).find do |k|
1611
+ [b.numerator, b.denominator].all? { |n| Simplify.integer_root(n, k)**k == n }
1612
+ end || 1
1613
+ r = Rational(Simplify.integer_root(b.numerator, e), Simplify.integer_root(b.denominator, e))
1614
+ [Simplify.normalize_number(r), sign * e]
1615
+ end
1616
+
1617
+ # Replace the atom u by t. exp(k*v) becomes t**k for every whole k, and
1618
+ # so does b**w when b = r**e and e*w is k times the exponent of u = r**v.
1619
+ def replace_atom(f, u, x, t)
1620
+ if u.is_a?(Pow) && power_base(u.base) && depends?(u.exponent, x)
1621
+ r, e = power_base(u.base)
1622
+ v = (u.exponent * e).simplify # u is r**v
1623
+ constant, table = Expand.table(f)
1624
+ rebuilt = {}
1625
+ table.each do |factors, coeff|
1626
+ new_factors = {}
1627
+ factors.each do |base, exp|
1628
+ root = exp.is_a?(Expression) && depends?(exp, x) && power_base(Expression.lift(base))
1629
+ if root
1630
+ return nil unless root.first == r
1631
+ ratio = (exp * root.last / v).simplify
1632
+ return nil unless ratio.is_a?(Num) && ratio.value.is_a?(Integer) # whole powers only, as for exp
1633
+ new_factors[t] = (new_factors[t] || 0) + ratio.value
1634
+ else
1635
+ new_factors[base] = exp
1636
+ end
1637
+ end
1638
+ rebuilt[new_factors] = (rebuilt[new_factors] || 0) + coeff
1639
+ end
1640
+ Simplify.rebuild_sum(constant, rebuilt)
1641
+ elsif u.is_a?(Fn) && u.name == :exp
1642
+ v = u.args.first
1643
+ constant, table = Expand.table(f)
1644
+ rebuilt = {}
1645
+ table.each do |factors, coeff|
1646
+ new_factors = {}
1647
+ factors.each do |base, exp|
1648
+ if base == Simplify.exp_base && exp.is_a?(Expression) && depends?(exp, x)
1649
+ ratio = (exp / v).simplify
1650
+ # a whole power only: exp(x) as exp(2*x)**(1/2) takes the
1651
+ # principal root and forgets the branch, and over CC that put
1652
+ # x = i*pi among the zeros of exp(2x) - 3*exp(x) + 2
1653
+ return nil unless ratio.is_a?(Num) && ratio.value.is_a?(Integer)
1654
+ new_factors[t] = (new_factors[t] || 0) + ratio.value
1655
+ else
1656
+ new_factors[base] = exp
1657
+ end
1658
+ end
1659
+ rebuilt[new_factors] = (rebuilt[new_factors] || 0) + coeff
1660
+ end
1661
+ Simplify.rebuild_sum(constant, rebuilt)
1662
+ else
1663
+ f.subs(u => t)
1664
+ end
1665
+ end
1666
+
1667
+ def root_denominator(f, x)
1668
+ _, table = Expand.table(f)
1669
+ table.each_key.flat_map { |factors| factors.select { |b, e| b == x && e.is_a?(Rational) }.map { |_, e| e.denominator } }.reduce(1, :lcm)
1670
+ end
1671
+
1672
+ def replace_root(f, x, q, t)
1673
+ constant, table = Expand.table(f)
1674
+ rebuilt = {}
1675
+ table.each do |factors, coeff|
1676
+ new_factors = factors.to_h do |base, exp|
1677
+ base == x ? [t, Simplify.normalize_number(Rational(exp) * q)] : [base, exp]
1678
+ end
1679
+ rebuilt[new_factors] = (rebuilt[new_factors] || 0) + coeff
1680
+ end
1681
+ Simplify.rebuild_sum(constant, rebuilt)
1682
+ end
1683
+
1684
+ # A target the function never takes is not an equation with complex
1685
+ # solutions, it is an equation with none. rcas can name one such gap:
1686
+ # tan(z) = (exp(2*i*z) - 1)/(i*(exp(2*i*z) + 1)) is i only where
1687
+ # exp(2*i*z) + 1 vanishes in the denominator, so the tangent omits
1688
+ # exactly +i and -i from the complex plane - which is also why atan(i)
1689
+ # does not evaluate. Without this, sin(x)**2 + cos(x)**2 = 0 came back
1690
+ # as two families built on atan(-i) and atan(i) (20 Sept 2026, the ninth
1691
+ # pass of the review). cos(u) = 2 is not of this kind and keeps its
1692
+ # answers: the cosine does reach 2, at i*log(2 + 3**(1/2)).
1693
+ def tangent_gap?(v)
1694
+ value = Expression.lift(v)
1695
+ return false unless value.is_a?(Num)
1696
+ number = Complex(value.value)
1697
+ number.real.zero? && number.imaginary.abs == 1
1698
+ rescue StandardError => rescued
1699
+ RCAS.guard!(rescued)
1700
+ false
1701
+ end
1702
+
1703
+ # Solutions of u = v for x, where u is a single atom containing x. With
1704
+ # all: true the period of sin, cos and tan is added, with an integer
1705
+ # parameter, so that every solution is covered and not just one period.
1706
+ def invert(u, v, x, depth, all: false)
1707
+ case u
1708
+ when Fn
1709
+ arg = u.args.first
1710
+ period = all ? period_parameter(arg - v, x) : nil
1711
+ turn = ->(multiple) { period ? multiple * PI * period : Num.new(0) }
1712
+ targets =
1713
+ case u.name
1714
+ # complete over CC (the fifth review's decision, as MuPAD and
1715
+ # solveset): exp(x) = 2 is log(2) + 2*pi*i*k; domain: RR keeps k = 0
1716
+ when :exp then [period ? Fn.new(:log, [v]) + 2 * PI * I * period : Fn.new(:log, [v])]
1717
+ when :surd then [v**u.args.last] # the real root: surd(u, n) = v is u = v**n, checked by verify
1718
+ when :log then [Fn.new(:exp, [v])]
1719
+ when :sin then [Fn.new(:asin, [v]) + turn.call(2), PI - Fn.new(:asin, [v]) + turn.call(2)]
1720
+ when :cos then [Fn.new(:acos, [v]) + turn.call(2), -Fn.new(:acos, [v]) + turn.call(2)]
1721
+ when :tan then tangent_gap?(v) ? [] : [Fn.new(:atan, [v]) + turn.call(1)]
1722
+ when :atan then [Fn.new(:tan, [v])]
1723
+ when :asin then [Fn.new(:sin, [v])]
1724
+ when :acos then [Fn.new(:cos, [v])]
1725
+ when :sinh then [Fn.new(:log, [v + RCAS.sqrt(v**2 + 1)])]
1726
+ when :cosh then [Fn.new(:log, [v + RCAS.sqrt(v**2 - 1)]), -Fn.new(:log, [v + RCAS.sqrt(v**2 - 1)])]
1727
+ else cannot_invert!(u, v)
1728
+ end
1729
+ # all: goes down with the equation: exp(sin(x)) = 1 is sin(x) = 0,
1730
+ # and that has a family, not two points
1731
+ targets.flat_map { |w| univariate((arg - w).simplify, x, depth + 1, all: all) }
1732
+ when Pow
1733
+ if depends?(u.base, x) && !depends?(u.exponent, x)
1734
+ return [] if principal_power_reaches?(u.exponent, v) == false
1735
+ univariate((u.base - v**(1 / u.exponent)).simplify, x, depth + 1, all: all)
1736
+ elsif !depends?(u.base, x)
1737
+ # b**u = exp(u*log(b)) = v: u*log(b) = log(v) + 2*pi*i*k, so
1738
+ # (-1)**x = 2 has the solutions 2*k - i*log(2)/pi (it answered [])
1739
+ turn = all ? period_parameter(u.exponent - v, x) : nil
1740
+ logarithm = turn ? Fn.new(:log, [v]) + 2 * PI * I * turn : Fn.new(:log, [v])
1741
+ roots_of_unity(u, v, x, depth, all: all) ||
1742
+ univariate((u.exponent - logarithm / Fn.new(:log, [u.base])).simplify, x, depth + 1, all: all)
1743
+ else
1744
+ cannot_invert!(u, v)
1745
+ end
1746
+ else
1747
+ cannot_invert!(u, v)
1748
+ end
1749
+ end
1750
+
1751
+ # w**e = exp(e*log(w)) with the principal log, whose imaginary part is
1752
+ # in (-pi, pi]: for 0 < |e| < 1 the power reaches only the sector
1753
+ # |arg| < |e|*pi, and one of its two edges. sqrt(exp(x)) = -1 has no
1754
+ # solution, and inverting it as exp(x) = (-1)**2 found the family
1755
+ # 2*pi*i*k - a family, so verify never saw it (the sixth review's
1756
+ # preflight). true, false, or nil when it cannot be decided.
1757
+ def principal_power_reaches?(e, v)
1758
+ return true unless e.is_a?(Num) && e.value.is_a?(Rational) && e.value.abs < 1 && !e.value.zero?
1759
+ return nil unless v.variables.empty?
1760
+ return e.value.positive? if Decide.zero?(v) == true
1761
+ arg = ComplexParts.arg(v).simplify
1762
+ bound = Num.new(e.value.abs) * PI
1763
+ case Decide.sign((bound - RCAS.abs(arg)).simplify)
1764
+ when :positive then true
1765
+ when :negative then false
1766
+ when :zero then Decide.sign(arg) == (e.value.positive? ? :positive : :negative)
1767
+ end
1768
+ rescue StandardError => rescued
1769
+ RCAS.guard!(rescued)
1770
+ nil
1771
+ end
1772
+
1773
+ # No inverse is known for u (x**x, erf, gamma, floor, an unknown
1774
+ # function): that is "can't", never "no solution" - x**x = 4 has the
1775
+ # root 2.
1776
+ def cannot_invert!(u, v)
1777
+ raise RCAS::Unsupported, "can't solve #{u} = #{v}: rcas knows no inverse of #{u.is_a?(Fn) ? u.name : u}"
1778
+ end
1779
+
1780
+ # Largest order of a root of unity we look for: (-1)**x is the one that
1781
+ # turns up, from cos(pi*x) with x an integer.
1782
+ MAX_ORDER = 12
1783
+
1784
+ # b**u = v where b is a root of unity: the logarithm is no use, because
1785
+ # the solutions repeat. (-1)**x = 1 holds for every even x and
1786
+ # (-1)**x = -1 for every odd one, and answering with one of them - which
1787
+ # is what log gave - drops all the rest. nil when b is not one, so that
1788
+ # 2**x = 4 keeps the ordinary route.
1789
+ def roots_of_unity(u, v, x, depth, all: false)
1790
+ base = u.base
1791
+ return nil unless base.is_a?(Num) && v.is_a?(Num)
1792
+ order = (2..MAX_ORDER).find { |n| unity?(Simplify.pow_number(base.value, n), 1) }
1793
+ return nil if order.nil?
1794
+ turn = all ? period_parameter(u.exponent - v, x) : nil
1795
+ found = (0...order).filter_map do |j|
1796
+ next nil unless unity?(Simplify.pow_number(base.value, j), v.value)
1797
+ target = Num.new(j)
1798
+ target = (target + Num.new(order) * turn).simplify if turn
1799
+ univariate((u.exponent - target).simplify, x, depth + 1)
1800
+ end.flatten
1801
+ # a value the powers of b never take: not "no solution" but the
1802
+ # logarithm's (complex) ones
1803
+ found.empty? ? nil : found
1804
+ end
1805
+
1806
+ def unity?(value, target) = Scalar.zero?(Expression.lift(Simplify.normalize_number(value - target)))
1807
+
1808
+ # Drop the candidates that demonstrably fail the equation: the roots a
1809
+ # squaring or a case split invented. The residual f(r) is decided by
1810
+ # Decide, so a root is dropped only when the residual is shown to be
1811
+ # non-zero - a residual of 4e-5 at exp(x) = 10**10 is rounding, and
1812
+ # an unknown x is not always called x (the old check substituted the
1813
+ # literal keyword x: and was a no-op for every other name). A root with
1814
+ # a parameter in it cannot be checked by a number and stays.
1815
+ def verify(f, x, roots)
1816
+ roots.select do |r|
1817
+ next true unless r.is_a?(Expression)
1818
+ next false unless Integrate.defined_value?(r) # log(0) is no number, let alone a root
1819
+ next true unless r.variables.empty?
1820
+ next float_residual_small?(f.subs(x => r)) if floaty?(f) || floaty?(r)
1821
+ residual = begin
1822
+ f.subs(x => r).simplify
1823
+ rescue ZeroDivisionError
1824
+ next false
1825
+ rescue StandardError => rescued
1826
+ RCAS.guard!(rescued)
1827
+ next true
1828
+ end
1829
+ Integrate.defined_value?(residual) && Decide.zero?(residual) != false
1830
+ end
1831
+ end
1832
+
1833
+ # A Float root, or an equation with Float coefficients, is checked the
1834
+ # way Equation#holds? checks one: against the running error bound of
1835
+ # the substituted equation, not by Decide.zero? of a residual that is
1836
+ # already a single Float - that was 1e-16 of noise decided "not zero",
1837
+ # and solve(1.0*x**3 - 6.0*x**2 + 12.0*x - 8.0, x) answered [] (the
1838
+ # sixth review's preflight). The root is itself a few ulp out, which
1839
+ # the bound does not know, hence the slack; a residual whose error
1840
+ # cannot be bounded keeps the root.
1841
+ FLOAT_SLACK = 1e3
1842
+
1843
+ def floaty?(e) = e.each_node.any? { |n| n.is_a?(Num) && (n.value.is_a?(Float) || (n.value.is_a?(Complex) && n.value.real.is_a?(Float))) }
1844
+
1845
+ def float_residual_small?(difference)
1846
+ found, error = Decide.float_with_error(difference)
1847
+ found.nil? || found.abs <= FLOAT_SLACK * Decide::ERROR_MARGIN * error
1848
+ end
1849
+
1850
+ # ---- systems ---------------------------------------------------------------------
1851
+
1852
+ def system(targets, vars, domain: nil)
1853
+ fs = targets.map { |t| to_zero(t).simplify }
1854
+ unknowns = Array(vars).map { |v| Expression.lift(v) }
1855
+ raise ArgumentError, "solve: list the unknowns, e.g. solve([...], [x, y])" if unknowns.empty?
1856
+
1857
+ solutions =
1858
+ if fs.all? { |f| linear_in?(f, unknowns) }
1859
+ linear_system(fs, unknowns)
1860
+ elsif (found = polynomial_system(fs, unknowns))
1861
+ found
1862
+ elsif fs.size == 2 && unknowns.size == 2
1863
+ polynomial_pair(fs, unknowns)
1864
+ else
1865
+ raise RCAS::Unsupported, "only linear systems and polynomial systems with rational coefficients are supported"
1866
+ end
1867
+ restrict_system(solutions, unknowns, domain).map { |sol| sol.is_a?(Hash) ? Assignment[sol] : sol }
1868
+ end
1869
+
1870
+ # A solution of a system survives when every unknown in it does.
1871
+ def restrict_system(solutions, unknowns, domain)
1872
+ wanted = unknowns.to_h { |x| [x, domain || RCAS.assumption(x.name)] }.compact
1873
+ return solutions if wanted.empty?
1874
+ solutions.reject do |solution|
1875
+ next false unless solution.is_a?(Hash)
1876
+ solution.any? { |x, value| wanted[x] && Infer.excluded?(value, wanted[x]) }
1877
+ end
1878
+ end
1879
+
1880
+ # Polynomial systems over QQ by a lex Gröbner basis [CLO15, ch. 2 §8, ch. 3 §1]:
1881
+ # the basis is triangular, so the last unknown has a univariate polynomial;
1882
+ # its roots are substituted into the rest. Parameters make the
1883
+ # coefficients rational functions, and the basis is taken over
1884
+ # Frac(QQ[params]) - the generic answer, as for a linear system, which
1885
+ # may differ where a leading coefficient in the parameters vanishes.
1886
+ # nil when a coefficient is not rational (sqrt(2), sin(a)), so that the
1887
+ # resultant route can try.
1888
+ def polynomial_system(fs, unknowns)
1889
+ names = unknowns.map(&:name)
1890
+ params = fs.flat_map(&:variables).uniq - names
1891
+ ring = params.empty? ? QQ[*names] : QQ[*params].fraction_field[*names]
1892
+ polys = fs.map do |f|
1893
+ ring.call(f)
1894
+ rescue DomainError
1895
+ return nil
1896
+ end
1897
+ basis = Groebner.basis(polys, :lex)
1898
+ return [] if basis.size == 1 && basis.first.constant?
1899
+ unless Groebner.zero_dimensional?(basis, :lex)
1900
+ raise RCAS::Unsupported, "the system has infinitely many solutions; its Gröbner basis is #{basis.map(&:to_s).join(', ')}"
1901
+ end
1902
+ # over Frac(QQ[params]) the coefficients are fractions such as
1903
+ # 1/(1/a**2 + 1); the numerator is the same equation, generically
1904
+ basis = basis.map { |g| params.empty? ? g.to_expr : RationalFunction.numer(g.to_expr) }
1905
+ triangular(basis, unknowns, {}).map { |sol| unknowns.to_h { |u| [u, sol[u]] } }
1906
+ end
1907
+
1908
+ def triangular(basis, unknowns, known)
1909
+ return [known] if unknowns.empty?
1910
+ x = unknowns.last
1911
+ rest = unknowns[0...-1].map(&:name)
1912
+ substituted = basis.map { |g| g.subs(known).simplify }.reject { |g| Scalar.zero?(g) }
1913
+ univariate = substituted.select { |g| (g.variables & rest).empty? }
1914
+ return [] if univariate.any? { |g| g.variables.empty? } # a non-zero constant: no solution on this branch
1915
+ raise RCAS::Unsupported, "no univariate polynomial in #{x} after substituting #{known}" if univariate.empty?
1916
+ pivot = univariate.min_by { |g| polynomial_coefficients(g, x)&.size || Float::INFINITY }
1917
+ coeffs = polynomial_coefficients(pivot, x) or raise RCAS::Unsupported, "#{pivot} is not a polynomial in #{x}"
1918
+ roots = dedupe(polynomial_roots(coeffs).map(&:simplify))
1919
+ roots = roots.select { |r| univariate.all? { |g| Scalar.zero?(g.subs(x => r).simplify) } }
1920
+ roots.flat_map { |r| triangular(basis, unknowns[0...-1], known.merge(x => r)) }
1921
+ end
1922
+
1923
+ def linear_in?(f, unknowns)
1924
+ names = unknowns.map(&:name)
1925
+ _, table = Expand.table(f)
1926
+ table.each_key.all? do |factors|
1927
+ degree = 0
1928
+ factors.each do |base, exp|
1929
+ if base.is_a?(Var) && names.include?(base.name)
1930
+ return false unless exp.is_a?(Integer) && exp >= 0
1931
+ degree += exp
1932
+ elsif (base.variables & names).any? || (exp.is_a?(Expression) && (exp.variables & names).any?)
1933
+ return false
1934
+ end
1935
+ end
1936
+ degree <= 1
1937
+ end
1938
+ end
1939
+
1940
+ def linear_system(fs, unknowns)
1941
+ n = unknowns.size
1942
+ rows = fs.map do |f|
1943
+ constant, table = Expand.table(f)
1944
+ row = Array.new(n) { Num.new(0) }
1945
+ rhs = Num.new(-constant)
1946
+ table.each do |factors, coeff|
1947
+ c = Num.new(coeff)
1948
+ index = nil
1949
+ factors.each do |base, exp|
1950
+ if base.is_a?(Var) && (i = unknowns.index(base))
1951
+ index = i
1952
+ else
1953
+ c *= Simplify.power_node(base, exp)
1954
+ end
1955
+ end
1956
+ index ? row[index] = Scalar.add(row[index], c.simplify) : rhs = Scalar.sub(rhs, c.simplify)
1957
+ end
1958
+ row + [rhs]
1959
+ end
1960
+ reduced, pivots = Elimination.rref(rows)
1961
+ return [] if pivots.include?(n)
1962
+ solution = {}
1963
+ pivots.each_with_index do |p, i|
1964
+ value = reduced[i][n]
1965
+ (0...n).each do |j|
1966
+ next if j == p || Scalar.zero?(reduced[i][j])
1967
+ value = Scalar.sub(value, Scalar.mul(reduced[i][j], unknowns[j]))
1968
+ end
1969
+ solution[unknowns[p]] = value.cancel
1970
+ end
1971
+ [solution]
1972
+ end
1973
+
1974
+ # Two equations whose coefficients are not rational - sqrt(2), or a
1975
+ # parameter next to one - by the resultant in x [GCL92, ch. 9]: its
1976
+ # roots in y are substituted back, and a root of the first equation is
1977
+ # kept where the second vanishes too. Parameters are indeterminates of
1978
+ # the ring, so the coefficients in y are read with coefficient_in.
1979
+ def polynomial_pair(fs, unknowns)
1980
+ x, y = unknowns
1981
+ names = [x.name, y.name]
1982
+ params = fs.flat_map(&:variables).uniq - names
1983
+ ring = [QQ, RR, CC].map { |k| PolynomialRing.new(k, params + names) }.find do |r|
1984
+ fs.all? { |e| Infer.where_defined { r.include?(e) } }
1985
+ end
1986
+ raise RCAS::Unsupported, "only linear systems and polynomial systems are supported: #{fs.map(&:to_s).join(', ')}" unless ring
1987
+ f, g = fs.map { |e| ring.call(e) }
1988
+ res = f.resultant(g, x.name)
1989
+ raise RCAS::Unsupported, "the equations share a common factor" if res.zero?
1990
+ ys = polynomial_roots((0..res.degree(y.name)).map { |k| res.coefficient_in(y.name, k).to_expr })
1991
+ dedupe(ys.map(&:simplify)).flat_map do |y0|
1992
+ xs = univariate(fs[0].subs(y => y0), x, 1)
1993
+ xs.select { |x0| vanishes_at?(fs[1], x => x0, y => y0) }.map { |x0| { x => x0, y => y0 } }
1994
+ end
1995
+ end
1996
+
1997
+ def vanishes_at?(f, point)
1998
+ value = f.subs(point).simplify
1999
+ value.variables.empty? ? Equation.new(value, 0).holds? : Scalar.zero?(value)
2000
+ end
2001
+ end
2002
+ end