rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# A real interval with optional open ends; -oo and oo are allowed.
|
|
5
|
+
class Interval
|
|
6
|
+
attr_reader :low, :high, :left_open, :right_open
|
|
7
|
+
|
|
8
|
+
def initialize(low, high, left_open: false, right_open: false)
|
|
9
|
+
@low = Expression.lift(low)
|
|
10
|
+
@high = Expression.lift(high)
|
|
11
|
+
@left_open = left_open || Limits.infinite?(@low)
|
|
12
|
+
@right_open = right_open || Limits.infinite?(@high)
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.open(low, high) = new(low, high, left_open: true, right_open: true)
|
|
17
|
+
def self.closed(low, high) = new(low, high)
|
|
18
|
+
def self.point(v) = new(v, v)
|
|
19
|
+
def self.reals = new(Neg.new(OO).simplify, OO)
|
|
20
|
+
|
|
21
|
+
def point? = low == high
|
|
22
|
+
def low_value = Limits.infinite?(low) ? (low == OO ? Float::INFINITY : -Float::INFINITY) : low.evalf
|
|
23
|
+
def high_value = Limits.infinite?(high) ? (high == OO ? Float::INFINITY : -Float::INFINITY) : high.evalf
|
|
24
|
+
|
|
25
|
+
# Exact where the ends are close to v: 1 + 10**-15/2 lies in
|
|
26
|
+
# (1, 1 + 10**-15), which Floats cannot see (Inequalities.compare).
|
|
27
|
+
def include?(v)
|
|
28
|
+
v = Expression.lift(v)
|
|
29
|
+
value = v.evalf
|
|
30
|
+
return false unless value.is_a?(Numeric) && value.real?
|
|
31
|
+
below = Inequalities.compare(low, v)
|
|
32
|
+
above = Inequalities.compare(v, high)
|
|
33
|
+
return false if below.nil? || above.nil?
|
|
34
|
+
(left_open ? below.negative? : below <= 0) && (right_open ? above.negative? : above <= 0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def empty?
|
|
38
|
+
order = Inequalities.compare(low, high) || (low_value <=> high_value)
|
|
39
|
+
order.positive? || (order.zero? && (left_open || right_open))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other) = other.is_a?(Interval) && other.low == low && other.high == high && other.left_open == left_open && other.right_open == right_open
|
|
43
|
+
alias eql? ==
|
|
44
|
+
def hash = [Interval, low, high, left_open, right_open].hash
|
|
45
|
+
|
|
46
|
+
def to_s
|
|
47
|
+
return "{#{low}}" if point?
|
|
48
|
+
"#{left_open ? '(' : '['}#{low}, #{high}#{right_open ? ')' : ']'}"
|
|
49
|
+
end
|
|
50
|
+
alias inspect to_s
|
|
51
|
+
|
|
52
|
+
def to_latex(wrap: nil)
|
|
53
|
+
return "\\{#{LaTeX.print(low)}\\}" if point?
|
|
54
|
+
"\\left#{left_open ? '(' : '['}#{LaTeX.print(low)}, #{LaTeX.print(high)}\\right#{right_open ? ')' : ']'}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A finite union of disjoint intervals, the result of solving an inequality.
|
|
59
|
+
class RealSet
|
|
60
|
+
include Enumerable
|
|
61
|
+
attr_reader :intervals
|
|
62
|
+
|
|
63
|
+
def initialize(intervals)
|
|
64
|
+
@intervals = RealSet.normalize(intervals).freeze
|
|
65
|
+
freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.empty = new([])
|
|
69
|
+
def self.reals = new([Interval.reals])
|
|
70
|
+
|
|
71
|
+
# Intervals with symbolic endpoints, already in order: no normalisation.
|
|
72
|
+
def self.raw(intervals)
|
|
73
|
+
set = allocate
|
|
74
|
+
set.instance_variable_set(:@intervals, intervals.freeze)
|
|
75
|
+
set.freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sort, drop empty pieces, merge touching ones. The ends are compared
|
|
79
|
+
# exactly (Inequalities.compare), so two roots 10**-15 apart bound an
|
|
80
|
+
# interval instead of merging into a point.
|
|
81
|
+
def self.normalize(list)
|
|
82
|
+
order = ->(a, b) { Inequalities.order!(a, b) }
|
|
83
|
+
sorted = list.reject(&:empty?).sort do |a, b|
|
|
84
|
+
c = order.call(a.low, b.low)
|
|
85
|
+
c.zero? ? (a.left_open ? 1 : 0) <=> (b.left_open ? 1 : 0) : c
|
|
86
|
+
end
|
|
87
|
+
merged = []
|
|
88
|
+
sorted.each do |i|
|
|
89
|
+
last = merged.last
|
|
90
|
+
touch = last && order.call(i.low, last.high)
|
|
91
|
+
if last && (touch.negative? || (touch.zero? && !(i.left_open && last.right_open)))
|
|
92
|
+
reach = order.call(i.high, last.high)
|
|
93
|
+
if reach.positive? || (reach.zero? && !i.right_open)
|
|
94
|
+
merged[-1] = Interval.new(last.low, i.high, left_open: last.left_open, right_open: i.right_open)
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
merged << i
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
merged
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def each(&block) = intervals.each(&block)
|
|
104
|
+
def empty? = intervals.empty?
|
|
105
|
+
def include?(v) = intervals.any? { |i| i.include?(v) }
|
|
106
|
+
def |(other) = RealSet.new(intervals + other.intervals)
|
|
107
|
+
|
|
108
|
+
def &(other)
|
|
109
|
+
return other & self if other.is_a?(ScatteredSet)
|
|
110
|
+
pieces = intervals.product(other.intervals).map { |a, b| RealSet.intersect(a, b) }.compact
|
|
111
|
+
RealSet.new(pieces)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.intersect(a, b)
|
|
115
|
+
lo, lo_open = [[a.low, a.left_open, a.low_value], [b.low, b.left_open, b.low_value]].max_by { |_, open, v| [v, open ? 1 : 0] }.values_at(0, 1)
|
|
116
|
+
hi, hi_open = [[a.high, a.right_open, a.high_value], [b.high, b.right_open, b.high_value]].min_by { |_, open, v| [v, open ? 0 : 1] }.values_at(0, 1)
|
|
117
|
+
i = Interval.new(lo, hi, left_open: lo_open, right_open: hi_open)
|
|
118
|
+
i.empty? ? nil : i
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The gaps between the intervals: everything the set leaves out.
|
|
122
|
+
def complement
|
|
123
|
+
pieces = []
|
|
124
|
+
low, open = Neg.new(OO).simplify, false
|
|
125
|
+
intervals.each do |i|
|
|
126
|
+
pieces << Interval.new(low, i.low, left_open: open, right_open: !i.left_open)
|
|
127
|
+
low, open = i.high, !i.right_open
|
|
128
|
+
end
|
|
129
|
+
pieces << Interval.new(low, OO, left_open: open)
|
|
130
|
+
RealSet.new(pieces.reject { |i| i.point? && Limits.infinite?(i.low) })
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def -(other) = self & other.complement
|
|
134
|
+
|
|
135
|
+
def ==(other) = other.is_a?(RealSet) && other.intervals == intervals
|
|
136
|
+
alias eql? ==
|
|
137
|
+
def hash = [RealSet, intervals].hash
|
|
138
|
+
|
|
139
|
+
def to_s
|
|
140
|
+
return "{}" if empty?
|
|
141
|
+
intervals.map(&:to_s).join(" ∪ ")
|
|
142
|
+
end
|
|
143
|
+
alias inspect to_s
|
|
144
|
+
|
|
145
|
+
def to_latex(wrap: nil)
|
|
146
|
+
return "\\emptyset" if empty?
|
|
147
|
+
intervals.map(&:to_latex).join(" \\cup ")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# lhs < rhs, lhs <= rhs, lhs > rhs, lhs >= rhs
|
|
152
|
+
# Intervals, plus the members of some families, less the members of
|
|
153
|
+
# others: where gamma(x) is real (the line less {-k | k in NN}), where x**x
|
|
154
|
+
# is ((0, oo) and the negative integers) or (-2)**x (the integers). No
|
|
155
|
+
# finite union of intervals says any of these, and real_domain refused;
|
|
156
|
+
# every subexpression real (the fifth review's rule) is this set. The
|
|
157
|
+
# families are affine and counted by their own parameter domain;
|
|
158
|
+
# `within` is a set every member must lie in as well.
|
|
159
|
+
class ScatteredSet
|
|
160
|
+
attr_reader :base, :plus, :minus, :within
|
|
161
|
+
|
|
162
|
+
def initialize(base, plus: [], minus: [], within: RealSet.reals)
|
|
163
|
+
@base = base
|
|
164
|
+
@plus = plus
|
|
165
|
+
@minus = minus
|
|
166
|
+
@within = within
|
|
167
|
+
freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def include?(v)
|
|
171
|
+
v = Expression.lift(v)
|
|
172
|
+
return false unless within.include?(v)
|
|
173
|
+
(base.include?(v) || plus.any? { |set| ScatteredSet.member?(set, v) }) && minus.none? { |set| ScatteredSet.member?(set, v) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def empty? = base.empty? && plus.empty?
|
|
177
|
+
|
|
178
|
+
def &(other)
|
|
179
|
+
raise RCAS::Unsupported, "the intersection of #{self} and #{other} is not written here" unless other.is_a?(RealSet)
|
|
180
|
+
# with nothing added, the base alone carries the restriction
|
|
181
|
+
return ScatteredSet.new(base & other, minus: minus, within: within) if plus.empty?
|
|
182
|
+
ScatteredSet.new(base & other, plus: plus, minus: minus, within: within & other)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# v = b + s*k for a k of the family's domain, decided exactly.
|
|
186
|
+
def self.member?(set, v)
|
|
187
|
+
pair = set.affine or return false
|
|
188
|
+
b, step = pair
|
|
189
|
+
index = ((v - b) / step).simplify
|
|
190
|
+
index.is_a?(Num) && (index.value.is_a?(Integer) || (index.value.is_a?(Rational) && index.value.denominator == 1)) && set.domain.include?(index.value.to_i)
|
|
191
|
+
rescue ZeroDivisionError
|
|
192
|
+
false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def ==(other) = other.is_a?(ScatteredSet) && [other.base, other.plus, other.minus, other.within] == [base, plus, minus, within]
|
|
196
|
+
alias eql? ==
|
|
197
|
+
def hash = [ScatteredSet, base, plus, minus, within].hash
|
|
198
|
+
|
|
199
|
+
# Brackets wherever a union meets \ or ∩: `A ∪ B \ C` and
|
|
200
|
+
# `{k | k in ZZ} ∩ (-oo, 0) ∪ (0, oo)` read two ways (the sixth
|
|
201
|
+
# review's preflight).
|
|
202
|
+
def to_s = written(->(set) { set.to_s }, " ∪ ", " \\ ", " ∩ ", "{}", "(", ")")
|
|
203
|
+
alias inspect to_s
|
|
204
|
+
|
|
205
|
+
def to_latex(wrap: nil)
|
|
206
|
+
written(->(set) { LaTeX.of(set) }, " \\cup ", " \\setminus ", " \\cap ", "\\emptyset", "\\left(", "\\right)")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def written(show, cup, minus_sign, cap, empty, open, close)
|
|
212
|
+
pieces = []
|
|
213
|
+
pieces << show.call(base) unless base.empty?
|
|
214
|
+
pieces.concat(plus.map { |set| show.call(set) })
|
|
215
|
+
united = pieces.size > 1 || (pieces.size == 1 && !base.empty? && base.intervals.size > 1)
|
|
216
|
+
text = pieces.empty? ? empty : pieces.join(cup)
|
|
217
|
+
restricted = !minus.empty? || within != RealSet.reals
|
|
218
|
+
text = "#{open}#{text}#{close}" if united && restricted
|
|
219
|
+
unless minus.empty?
|
|
220
|
+
removed = minus.map { |set| show.call(set) }.join(cup)
|
|
221
|
+
text = "#{text}#{minus_sign}#{minus.size > 1 ? "#{open}#{removed}#{close}" : removed}"
|
|
222
|
+
end
|
|
223
|
+
unless within == RealSet.reals
|
|
224
|
+
bound = show.call(within)
|
|
225
|
+
bound = "#{open}#{bound}#{close}" if within.intervals.size > 1
|
|
226
|
+
text = "#{text}#{cap}#{bound}"
|
|
227
|
+
end
|
|
228
|
+
text
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
public
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
class Inequality
|
|
235
|
+
OPS = { :< => "<", :<= => "<=", :> => ">", :>= => ">=", :!= => "!=" }.freeze
|
|
236
|
+
FLIP = { :< => :>, :<= => :>=, :> => :<, :>= => :<=, :!= => :!= }.freeze
|
|
237
|
+
|
|
238
|
+
attr_reader :lhs, :op, :rhs
|
|
239
|
+
|
|
240
|
+
def initialize(lhs, op, rhs)
|
|
241
|
+
raise ArgumentError, "unknown relation #{op}" unless OPS.key?(op)
|
|
242
|
+
@lhs = Expression.lift(lhs)
|
|
243
|
+
@op = op
|
|
244
|
+
@rhs = Expression.lift(rhs)
|
|
245
|
+
freeze
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# As f OP 0 with OP in {<, <=}.
|
|
249
|
+
def normalized
|
|
250
|
+
f = (lhs - rhs).simplify
|
|
251
|
+
%i[< <= !=].include?(op) ? [f, op] : [(-f).simplify, FLIP[op]]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def strict? = %i[< > !=].include?(op)
|
|
255
|
+
def swap = Inequality.new(rhs, FLIP[op], lhs)
|
|
256
|
+
def subs(*args) = Inequality.new(lhs.subs(*args), op, rhs.subs(*args))
|
|
257
|
+
def simplify = Inequality.new(lhs.simplify, op, rhs.simplify)
|
|
258
|
+
# hold { a < b }.doit: both sides evaluated, the statement kept (C9)
|
|
259
|
+
def evaluate = Inequality.new(lhs.evaluate, op, rhs.evaluate)
|
|
260
|
+
alias doit evaluate
|
|
261
|
+
alias unhold evaluate
|
|
262
|
+
def variables = (lhs.variables | rhs.variables).sort
|
|
263
|
+
def solve(var = nil) = Inequalities.solve(self, var)
|
|
264
|
+
|
|
265
|
+
def holds?(**bindings)
|
|
266
|
+
value = Expression.lift((lhs - rhs).call(**bindings)).evalf
|
|
267
|
+
raise ArgumentError, "#{self} is not numeric with #{bindings}" unless value.is_a?(Numeric) && value.real?
|
|
268
|
+
value.public_send(op, 0)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def ==(other) = other.is_a?(Inequality) && other.lhs == lhs && other.op == op && other.rhs == rhs
|
|
272
|
+
alias eql? ==
|
|
273
|
+
def hash = [Inequality, lhs, op, rhs].hash
|
|
274
|
+
|
|
275
|
+
def to_s = "#{lhs} #{OPS[op]} #{rhs}"
|
|
276
|
+
alias inspect to_s
|
|
277
|
+
|
|
278
|
+
def to_latex(wrap: nil)
|
|
279
|
+
symbol = { :< => "<", :<= => "\\le", :> => ">", :>= => "\\ge", :!= => "\\ne" }[op]
|
|
280
|
+
"#{LaTeX.print(lhs)} #{symbol} #{LaTeX.print(rhs)}"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A solution that depends on a parameter: one value per region of the
|
|
285
|
+
# parameter line.
|
|
286
|
+
#
|
|
287
|
+
# solve(x**2 - a >= 0, x)
|
|
288
|
+
# => a <= 0: (-oo, oo)
|
|
289
|
+
# a > 0: (-oo, -a**(1/2)] ∪ [a**(1/2), oo)
|
|
290
|
+
class Cases
|
|
291
|
+
attr_reader :var, :branches # [[RealSet over var, value], ...]
|
|
292
|
+
|
|
293
|
+
def initialize(var, branches)
|
|
294
|
+
@var = var
|
|
295
|
+
@branches = branches.freeze
|
|
296
|
+
freeze
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def size = branches.size
|
|
300
|
+
def each(&block) = branches.each(&block)
|
|
301
|
+
include Enumerable
|
|
302
|
+
|
|
303
|
+
# The value for a concrete parameter, with the parameter substituted
|
|
304
|
+
# into the ends: at(1) of a > 0: (a, oo) is (1, oo).
|
|
305
|
+
def at(value)
|
|
306
|
+
branch = branches.find { |cond, _| cond.include?(value) }
|
|
307
|
+
raise ArgumentError, "#{var} = #{value} is not covered" unless branch
|
|
308
|
+
set = branch.last
|
|
309
|
+
return set unless set.is_a?(RealSet)
|
|
310
|
+
point = Expression.lift(value)
|
|
311
|
+
RealSet.new(set.intervals.map do |i|
|
|
312
|
+
Interval.new(i.low.subs(var => point).simplify, i.high.subs(var => point).simplify, left_open: i.left_open, right_open: i.right_open)
|
|
313
|
+
end)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def condition(set)
|
|
317
|
+
parts = set.intervals.map do |i|
|
|
318
|
+
lo, hi = i.low, i.high
|
|
319
|
+
if i.point? then "#{var} = #{lo}"
|
|
320
|
+
elsif Limits.infinite?(lo) && Limits.infinite?(hi) then "any #{var}"
|
|
321
|
+
elsif Limits.infinite?(lo) then "#{var} #{i.right_open ? '<' : '<='} #{hi}"
|
|
322
|
+
elsif Limits.infinite?(hi) then "#{var} #{i.left_open ? '>' : '>='} #{lo}"
|
|
323
|
+
else "#{lo} #{i.left_open ? '<' : '<='} #{var} #{i.right_open ? '<' : '<='} #{hi}"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
parts.join(" or ")
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def to_s
|
|
330
|
+
texts = branches.map { |cond, _| "#{condition(cond)}:" }
|
|
331
|
+
width = texts.map(&:size).max
|
|
332
|
+
branches.each_with_index.map { |(_, value), i| "#{texts[i].ljust(width)} #{value}" }.join("\n")
|
|
333
|
+
end
|
|
334
|
+
alias inspect to_s
|
|
335
|
+
|
|
336
|
+
def to_latex(wrap: nil)
|
|
337
|
+
rows = branches.map do |cond, value|
|
|
338
|
+
text = condition(cond).gsub("<=", "\\le").gsub(">=", "\\ge").gsub(" or ", "\\text{ or }")
|
|
339
|
+
"#{value.to_latex} & \\text{if } #{text}"
|
|
340
|
+
end
|
|
341
|
+
"\\begin{cases} #{rows.join(' \\\\ ')} \\end{cases}"
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Solving inequalities in one real variable by sign charts.
|
|
346
|
+
#
|
|
347
|
+
# solve(x**2 < 4, x) # => (-2, 2)
|
|
348
|
+
# solve(abs(x - 1) <= 2, x) # => [-1, 3]
|
|
349
|
+
# solve((x - 1)/(x + 1) >= 0, x) # => (-oo, -1) ∪ [1, oo)
|
|
350
|
+
# solve([x > 1, x < 3], x) # => (1, 3)
|
|
351
|
+
module Inequalities
|
|
352
|
+
module_function
|
|
353
|
+
|
|
354
|
+
def solve(target, var = nil)
|
|
355
|
+
list = target.is_a?(Array) ? target : [target]
|
|
356
|
+
raise ArgumentError, "solve: expected inequalities" unless list.all? { |t| t.is_a?(Inequality) }
|
|
357
|
+
x = var ? Expression.lift(var) : Solve.variable(list.map(&:lhs).reduce(:+) + list.map(&:rhs).reduce(:+), nil)
|
|
358
|
+
list.map { |ineq| single(ineq, x) }.reduce { |a, b| a & b }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# The inequality is solved in its simplified form, and then the poles
|
|
362
|
+
# of the inequality *as written* are taken out: x/x > 0 is 1 > 0 after
|
|
363
|
+
# simplify, and still says nothing at x = 0.
|
|
364
|
+
def single(ineq, x)
|
|
365
|
+
raw = Expression.lift(ineq.lhs - ineq.rhs)
|
|
366
|
+
f, op = ineq.normalized
|
|
367
|
+
params = f.variables - [x.name]
|
|
368
|
+
if params.size == 1
|
|
369
|
+
return Parametric.new(f, op, x, Var.new(params.first)).solve
|
|
370
|
+
elsif params.size > 1
|
|
371
|
+
raise RCAS::Unsupported, "inequalities with several parameters (#{params.join(', ')}) are not supported"
|
|
372
|
+
end
|
|
373
|
+
f = Num.new(0) if f.variables.include?(x.name) && Solve.rational_identity?(f, x)
|
|
374
|
+
without_poles(solved(f, op, x), raw, x)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def solved(f, op, x)
|
|
378
|
+
return not_equal(f, x) if op == :!=
|
|
379
|
+
return (Scalar.zero?(f) ? (op == :<= ? RealSet.reals : RealSet.empty) : constant_case(f, op)) unless f.variables.include?(x.name)
|
|
380
|
+
absolutes = f.each_node.select { |n| n.is_a?(Fn) && n.name == :abs && n.args.first.variables.include?(x.name) }.uniq
|
|
381
|
+
return sign_chart(f, op, x) if absolutes.empty?
|
|
382
|
+
piecewise(f, op, x, absolutes)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The set less the real zeros of the denominators of raw.
|
|
386
|
+
def without_poles(set, raw, x)
|
|
387
|
+
return set unless set.is_a?(RealSet) && !set.empty?
|
|
388
|
+
points = Analysis.denominators(raw, x).flat_map do |d|
|
|
389
|
+
zeros = Solve.solve(d, x, domain: RR)
|
|
390
|
+
raise RCAS::Unsupported, "the poles of #{raw} (the zeros of #{d}) are not a finite set of points" unless zeros.is_a?(Array) && zeros.none? { |z| z.is_a?(ImageSet) }
|
|
391
|
+
zeros.select { |z| real?(z) }.map { |z| real_part(z) }
|
|
392
|
+
end
|
|
393
|
+
return set if points.empty?
|
|
394
|
+
set - RealSet.new(points.map { |p| Interval.point(p) })
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# f != 0: everything except the zeros of f (and the points where f is undefined).
|
|
398
|
+
def not_equal(f, x)
|
|
399
|
+
return (Scalar.zero?(f) ? RealSet.empty : RealSet.reals) unless f.variables.include?(x.name)
|
|
400
|
+
num, den = Solve.numerator_denominator(f, x)
|
|
401
|
+
unless Solve.polynomial_coefficients(num, x) && Solve.polynomial_coefficients(den, x)
|
|
402
|
+
raise RCAS::Unsupported, "only polynomial and rational inequalities are supported: #{f}"
|
|
403
|
+
end
|
|
404
|
+
RealSet.new(regions(real_roots(num, x) + real_roots(den, x)))
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def constant_case(f, op)
|
|
408
|
+
sign = sign_of(f)
|
|
409
|
+
raise RCAS::Unsupported, "cannot decide the sign of #{f}" if sign.nil?
|
|
410
|
+
holds = case op
|
|
411
|
+
when :< then sign == :negative
|
|
412
|
+
when :<= then sign != :positive
|
|
413
|
+
end
|
|
414
|
+
holds ? RealSet.reals : RealSet.empty
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Split the real line at the zeros of the abs arguments; on each piece
|
|
418
|
+
# abs(u) is u or -u.
|
|
419
|
+
def piecewise(f, op, x, absolutes)
|
|
420
|
+
points = distinct(absolutes.flat_map { |a| real_roots(a.args.first, x) })
|
|
421
|
+
pieces = regions(points).map do |region|
|
|
422
|
+
t = sample(region)
|
|
423
|
+
replaced = absolutes.reduce(f) do |acc, a|
|
|
424
|
+
u = a.args.first
|
|
425
|
+
sign = sign_of(u.subs(x => t)) or raise RCAS::Unsupported, "cannot decide the sign of #{u} at #{t}"
|
|
426
|
+
acc.subs(a => (sign == :negative ? -u : u))
|
|
427
|
+
end
|
|
428
|
+
RealSet.new([region]) & single(Inequality.new(replaced, op, 0), x)
|
|
429
|
+
end
|
|
430
|
+
# the split points themselves - where f has a value there at all
|
|
431
|
+
points.each do |p|
|
|
432
|
+
value = begin
|
|
433
|
+
f.subs(x => p).simplify
|
|
434
|
+
rescue ZeroDivisionError
|
|
435
|
+
next
|
|
436
|
+
end
|
|
437
|
+
next unless Integrate.defined_value?(value)
|
|
438
|
+
sign = sign_of(value)
|
|
439
|
+
next if sign.nil?
|
|
440
|
+
pieces << RealSet.new([Interval.point(p)]) if op == :< ? sign == :negative : sign != :positive
|
|
441
|
+
end
|
|
442
|
+
pieces.reduce(RealSet.empty) { |acc, s| acc | s }
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def sign_chart(f, op, x)
|
|
446
|
+
num, den = Solve.numerator_denominator(f, x)
|
|
447
|
+
unless Solve.polynomial_coefficients(num, x) && Solve.polynomial_coefficients(den, x)
|
|
448
|
+
raise RCAS::Unsupported, "only polynomial and rational inequalities are supported: #{f}"
|
|
449
|
+
end
|
|
450
|
+
zeros = real_roots(num, x)
|
|
451
|
+
poles = real_roots(den, x)
|
|
452
|
+
points = distinct(zeros + poles)
|
|
453
|
+
pieces = []
|
|
454
|
+
regions(points).each do |region|
|
|
455
|
+
t = sample(region)
|
|
456
|
+
# the sign of the quotient from its cancelled parts: the sample may
|
|
457
|
+
# be a pole that cancelled, (x**2 - x)/x at 0 (fourth review, S17)
|
|
458
|
+
signs = [num, den].map { |part| sign_of(part.subs(x => t)) }
|
|
459
|
+
raise RCAS::Unsupported, "cannot decide the sign of #{f} at #{t}" if signs.include?(nil) || signs.last == :zero
|
|
460
|
+
sign = signs.first == :zero ? :zero : (signs.uniq.size == 1 ? :positive : :negative)
|
|
461
|
+
pieces << region if sign == :negative || (sign == :zero && op == :<=)
|
|
462
|
+
end
|
|
463
|
+
unless op == :<
|
|
464
|
+
zeros.each { |z| pieces << Interval.point(z) unless poles.any? { |p| compare(p, z)&.zero? } }
|
|
465
|
+
end
|
|
466
|
+
RealSet.new(pieces)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Real roots as exact expressions, ordered. A root is real when that is
|
|
470
|
+
# decided - (x - 1)**2 + 10**-26 has the roots 1 +- 10**-13*i, which a
|
|
471
|
+
# tolerance of 1e-12 on the imaginary part took for the real 1.
|
|
472
|
+
def real_roots(f, x)
|
|
473
|
+
return [] unless f.variables.include?(x.name)
|
|
474
|
+
roots = Solve.univariate(f, x, 0)
|
|
475
|
+
sort(distinct(roots.select { |r| real?(r) }.map { |r| real_part(r) }))
|
|
476
|
+
rescue ArgumentError
|
|
477
|
+
raise RCAS::Unsupported, "cannot find the real roots of #{f}"
|
|
478
|
+
rescue NotImplementedError, RCAS::Unsupported => e
|
|
479
|
+
raise RCAS::Unsupported, "cannot find the real roots of #{f}: #{e.message}"
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# true when r is shown to be real, false when shown not to be; a root
|
|
483
|
+
# that cannot be told is refused rather than guessed.
|
|
484
|
+
def real?(r)
|
|
485
|
+
r = Expression.lift(r)
|
|
486
|
+
return true if (d = Infer.domain(r)) && d <= RR
|
|
487
|
+
return r.value.is_a?(Numeric) && r.value.real? if r.is_a?(Num)
|
|
488
|
+
imaginary = ComplexParts.im(r)
|
|
489
|
+
decided = Decide.zero?(imaginary)
|
|
490
|
+
# a parameter with a declared sign: i*a is not real for a > 0
|
|
491
|
+
decided = false if decided.nil? && %i[positive negative].include?(RCAS.sign_of(imaginary))
|
|
492
|
+
raise RCAS::Unsupported, "cannot decide whether #{r} is real" if decided.nil?
|
|
493
|
+
decided
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def real_part(r)
|
|
497
|
+
r = Expression.lift(r)
|
|
498
|
+
return r if (d = Infer.domain(r)) && d <= RR
|
|
499
|
+
return Num.new(r.value.real) if r.is_a?(Num) && r.value.is_a?(Complex)
|
|
500
|
+
ComplexParts.re(r)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Open intervals between consecutive points, from -oo to oo.
|
|
504
|
+
def regions(points)
|
|
505
|
+
sorted = sort(distinct(points))
|
|
506
|
+
bounds = [Neg.new(OO).simplify] + sorted + [OO]
|
|
507
|
+
bounds.each_cons(2).map { |a, b| Interval.open(a, b) }
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# A point strictly inside an open region, exact: a short rational where
|
|
511
|
+
# the Floats can see the gap, the midpoint otherwise.
|
|
512
|
+
def sample(region)
|
|
513
|
+
lo, hi = region.low, region.high
|
|
514
|
+
return Num.new(0) if Limits.infinite?(lo) && Limits.infinite?(hi)
|
|
515
|
+
return (hi - 1).simplify if Limits.infinite?(lo)
|
|
516
|
+
return (lo + 1).simplify if Limits.infinite?(hi)
|
|
517
|
+
a, b = region.low_value.to_f, region.high_value.to_f
|
|
518
|
+
if a.finite? && b.finite? && b - a > 1e-9 * [1.0, a.abs, b.abs].max
|
|
519
|
+
r = Num.new(((a + b) / 2).rationalize(Rational((b - a) / 4)))
|
|
520
|
+
return r if compare(lo, r) == -1 && compare(r, hi) == -1
|
|
521
|
+
end
|
|
522
|
+
((lo + hi) / 2).simplify
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# The old Float test point, for callers that want a number.
|
|
526
|
+
def test_point(region) = sample(region).evalf.to_f
|
|
527
|
+
|
|
528
|
+
def sign_of(value)
|
|
529
|
+
value = Expression.lift(value).simplify
|
|
530
|
+
rank = infinity_rank(value)
|
|
531
|
+
return rank.positive? ? :positive : :negative unless rank.zero?
|
|
532
|
+
Decide.sign(value)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# -1, 0 or 1 for two real constants (the infinities included), or nil
|
|
536
|
+
# when it cannot be told. The Floats answer when they are well apart;
|
|
537
|
+
# close values are decided exactly.
|
|
538
|
+
def compare(a, b)
|
|
539
|
+
a = Expression.lift(a)
|
|
540
|
+
b = Expression.lift(b)
|
|
541
|
+
ra = infinity_rank(a)
|
|
542
|
+
rb = infinity_rank(b)
|
|
543
|
+
return ra <=> rb if ra != 0 || rb != 0
|
|
544
|
+
return 0 if a == b
|
|
545
|
+
difference = Sub.new(a, b)
|
|
546
|
+
# Floats decide only clear of their own rounding (the running error
|
|
547
|
+
# bound), and nothing decides by Floats alone: two values closer than
|
|
548
|
+
# a relative 1e-9 used to be ordered by them (fourth review, 2.4)
|
|
549
|
+
quick = Decide.float_sign(difference)
|
|
550
|
+
return quick == :positive ? 1 : -1 if quick
|
|
551
|
+
sign = sign_of(difference)
|
|
552
|
+
{ positive: 1, negative: -1, zero: 0 }[sign]
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# compare, or a refusal: an order the exact routes cannot settle is not
|
|
556
|
+
# read off Floats.
|
|
557
|
+
def order!(a, b)
|
|
558
|
+
compare(a, b) or raise RCAS::Unsupported, "cannot decide whether #{a} or #{b} is the larger"
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def infinity_rank(e)
|
|
562
|
+
return 1 if e == OO
|
|
563
|
+
return -1 if Limits.infinite?(e)
|
|
564
|
+
0
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def real_float(e) = RCAS.real_float(e)
|
|
568
|
+
|
|
569
|
+
def distinct(points)
|
|
570
|
+
points.each_with_object([]) { |p, out| out << p unless out.any? { |q| compare(p, q)&.zero? } }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def sort(points) = points.sort { |p, q| order!(p, q) }
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# One-parameter inequalities: split the parameter line at the values where
|
|
577
|
+
# the structure of the solution changes, solve exactly in each piece, and
|
|
578
|
+
# write the endpoints through the symbolic roots.
|
|
579
|
+
class Parametric
|
|
580
|
+
def initialize(f, op, x, a)
|
|
581
|
+
@f = f
|
|
582
|
+
@op = op
|
|
583
|
+
@x = x
|
|
584
|
+
@a = a
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def solve
|
|
588
|
+
num, den = Solve.numerator_denominator(@f, @x)
|
|
589
|
+
unless Solve.polynomial_coefficients(num, @x) && Solve.polynomial_coefficients(den, @x)
|
|
590
|
+
raise RCAS::Unsupported, "only polynomial and rational inequalities are supported: #{@f}"
|
|
591
|
+
end
|
|
592
|
+
roots = symbolic_roots(num) + symbolic_roots(den)
|
|
593
|
+
points = critical_values(roots, num, den)
|
|
594
|
+
pieces = []
|
|
595
|
+
Inequalities.regions(points).each do |region|
|
|
596
|
+
sample = sample_point(region)
|
|
597
|
+
set = Inequalities.single(Inequality.new(@f.subs(@a => sample), @op, 0), @x)
|
|
598
|
+
pieces << [region, symbolize(set, roots, sample)]
|
|
599
|
+
end
|
|
600
|
+
points.each do |c|
|
|
601
|
+
# x/a > 1 at a = 0 is no inequality at all: it holds for no x
|
|
602
|
+
at_point = begin
|
|
603
|
+
Inequalities.single(Inequality.new(@f.subs(@a => c), @op, 0), @x)
|
|
604
|
+
rescue ZeroDivisionError
|
|
605
|
+
RealSet.empty
|
|
606
|
+
end
|
|
607
|
+
pieces << [Interval.point(c), at_point]
|
|
608
|
+
end
|
|
609
|
+
pieces.sort_by! { |region, _| [region.low_value, region.point? ? 0 : 1] }
|
|
610
|
+
merged = merge(pieces)
|
|
611
|
+
return merged.first.last if merged.size == 1
|
|
612
|
+
Cases.new(@a, merged)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
private
|
|
616
|
+
|
|
617
|
+
def symbolic_roots(poly)
|
|
618
|
+
return [] unless poly.variables.include?(@x.name)
|
|
619
|
+
Solve.univariate(poly, @x, 0)
|
|
620
|
+
rescue NotImplementedError, RCAS::Unsupported, ArgumentError
|
|
621
|
+
raise RCAS::Unsupported, "cannot solve #{poly} = 0 for #{@x} with the parameter #{@a}"
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Parameter values where roots become real, coincide, or the degree drops.
|
|
625
|
+
def critical_values(roots, num, den)
|
|
626
|
+
values = []
|
|
627
|
+
roots.each do |r|
|
|
628
|
+
r.each_node do |n|
|
|
629
|
+
next unless n.is_a?(Pow) && n.exponent.is_a?(Num) && n.exponent.value.is_a?(Rational) && n.base.variables.include?(@a.name)
|
|
630
|
+
values.concat(parameter_roots(n.base))
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
roots.combination(2) { |r1, r2| values.concat(parameter_roots((r1 - r2).simplify)) }
|
|
634
|
+
[num, den].each do |poly|
|
|
635
|
+
coeffs = Solve.polynomial_coefficients(poly, @x)
|
|
636
|
+
next unless coeffs
|
|
637
|
+
values.concat(parameter_roots(coeffs.last)) if coeffs.last.variables.include?(@a.name)
|
|
638
|
+
# a coefficient that has no value there: x/a at a = 0
|
|
639
|
+
coeffs.each { |c| Analysis.denominators(c, @a).each { |d| values.concat(parameter_roots(d)) } }
|
|
640
|
+
end
|
|
641
|
+
# distinct and ordered exactly: two values that agree to ten digits
|
|
642
|
+
# are still two (a - 1 and a - 1 - 10**-12 were merged: fourth review)
|
|
643
|
+
Inequalities.sort(Inequalities.distinct(values))
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def parameter_roots(expr)
|
|
647
|
+
return [] unless expr.variables.include?(@a.name)
|
|
648
|
+
Inequalities.real_roots(expr, @a)
|
|
649
|
+
rescue NotImplementedError, RCAS::Unsupported
|
|
650
|
+
[]
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# A rational number strictly inside the region (exact arithmetic
|
|
654
|
+
# downstream), chosen exactly: a Float midpoint of (1, 1 + 10**-12)
|
|
655
|
+
# need not be inside it.
|
|
656
|
+
def sample_point(region)
|
|
657
|
+
point = Inequalities.sample(region)
|
|
658
|
+
point.is_a?(Num) ? point : Num.new(Rational(point.evalf.to_f))
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Replace numeric endpoints by the symbolic roots that produced them.
|
|
662
|
+
def symbolize(set, roots, sample)
|
|
663
|
+
intervals = set.intervals.map do |i|
|
|
664
|
+
Interval.new(symbolic_endpoint(i.low, roots, sample), symbolic_endpoint(i.high, roots, sample),
|
|
665
|
+
left_open: i.left_open, right_open: i.right_open)
|
|
666
|
+
end
|
|
667
|
+
RealSet.raw(intervals)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def symbolic_endpoint(value, roots, sample)
|
|
671
|
+
return value if Limits.infinite?(value)
|
|
672
|
+
match = roots.find { |r| Inequalities.compare(r.subs(@a.name => sample).simplify, value)&.zero? }
|
|
673
|
+
match || raise(RCAS::Unsupported, "cannot express the endpoint #{value} through the roots")
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Adjacent pieces with the same solution share one condition.
|
|
677
|
+
def merge(pieces)
|
|
678
|
+
merged = []
|
|
679
|
+
pieces.each do |region, set|
|
|
680
|
+
if merged.any? && merged.last.last == set
|
|
681
|
+
merged[-1] = [merged.last.first | RealSet.new([region]), set]
|
|
682
|
+
else
|
|
683
|
+
merged << [RealSet.new([region]), set]
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
merged
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
end
|