rcas 0.2.0

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