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