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,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # A function given case by case:
5
+ #
6
+ # piecewise(x < 0 => -x, :else => x**2)
7
+ #
8
+ # The conditions are Inequalities, Equations, Intervals, RealSets or the
9
+ # default `:else`, and the first condition that holds decides (the rule
10
+ # Maple and Mathematica use). Construction never rewrites: the branches
11
+ # are kept as written, and a branch is picked only once the indeterminate
12
+ # has a value.
13
+ class Piecewise < Expression
14
+ OTHERWISE = :else
15
+
16
+ attr_reader :branches # [[condition, value], ...] in the order given
17
+
18
+ def initialize(branches)
19
+ raise ArgumentError, "piecewise needs at least one branch" if branches.empty?
20
+ @branches = branches.map { |cond, value| [cond, Expression.lift(value)] }.freeze
21
+ freeze
22
+ end
23
+
24
+ def conditions = branches.map(&:first)
25
+ def values = branches.map(&:last)
26
+
27
+ # Only the values are children: a condition is not an Expression, so
28
+ # every generic rewrite carries it along untouched (see replace_with).
29
+ def children = values
30
+ def rebuild(*values) = Piecewise.new(conditions.zip(values))
31
+ def to_sexp = [:piecewise, *branches.map { |cond, value| [cond.to_s, value.to_sexp] }]
32
+
33
+ def ==(other) = other.is_a?(Piecewise) && other.branches == branches
34
+ alias eql? ==
35
+ def hash = [Piecewise, branches].hash
36
+
37
+ def variables
38
+ (conditions.flat_map { |c| Piecewises.condition_variables(c) } | values.flat_map(&:variables)).uniq.sort
39
+ end
40
+
41
+ def replace_with(table)
42
+ return table[self] if table.key?(self)
43
+ Piecewise.new(branches.map { |cond, value| [Piecewises.substitute(cond, table), value.replace_with(table)] })
44
+ end
45
+
46
+ # The indeterminate the conditions speak about.
47
+ def var(given = nil) = Piecewises.var(self, given)
48
+
49
+ # The value at a point; DomainError when no branch covers it.
50
+ def at(value) = Piecewises.at(self, value)
51
+
52
+ # [[RealSet, value], ...] with the first-match rule applied, so that the
53
+ # sets are disjoint and branches that nothing is left of are dropped.
54
+ def sets(x = nil) = Piecewises.located(self, Piecewises.var(self, x)).map { |_, set, value| [set, value] }
55
+
56
+ # The finite points where the definition changes.
57
+ def breakpoints(x = nil) = Piecewises.breakpoints(self, x)
58
+
59
+ # The jumps, and the corners where the one-sided derivatives differ.
60
+ def discontinuities(x = nil) = Piecewises.discontinuities(self, x)
61
+ def kinks(x = nil) = Piecewises.kinks(self, x)
62
+ def continuous?(x = nil) = discontinuities(x).empty?
63
+ def differentiable?(x = nil) = continuous?(x) && kinks(x).empty?
64
+ end
65
+
66
+ # Building, deciding and calculus for Piecewise nodes.
67
+ #
68
+ # Each condition becomes a RealSet (inequalities.rb) whenever an operation
69
+ # needs to know *where* a branch lives: integration cuts the range at the
70
+ # breakpoints and glues the antiderivatives together there, limits pick
71
+ # the branch they approach through, and solve keeps only the roots that
72
+ # lie in their own piece.
73
+ module Piecewises
74
+ module_function
75
+
76
+ # piecewise(x < 0 => -x, :else => x**2), a plain Hash, or an array of
77
+ # [condition, value] pairs.
78
+ def build(*positional, **keywords)
79
+ pairs = []
80
+ positional.each do |arg|
81
+ case arg
82
+ when Hash then pairs.concat(arg.to_a)
83
+ when Array then pairs.concat(arg.map { |pair| Array(pair) })
84
+ else raise ArgumentError, "piecewise: expected condition => value pairs, got #{arg.class}"
85
+ end
86
+ end
87
+ pairs.concat(keywords.to_a)
88
+ raise ArgumentError, "piecewise: expected condition => value pairs" if pairs.empty?
89
+ Piecewise.new(pairs.map { |cond, value| [check_condition(cond), value] })
90
+ end
91
+
92
+ def check_condition(cond)
93
+ case cond
94
+ when Inequality, Equation, Interval, RealSet then cond
95
+ when :else, :otherwise, true then Piecewise::OTHERWISE
96
+ else raise ArgumentError, "piecewise: #{cond.inspect} is not a condition"
97
+ end
98
+ end
99
+
100
+ # The otherwise-branch is the Symbol :else, and after the README's
101
+ # `include RCAS::Functions` a Symbol responds to simplify and subs as
102
+ # well - so the conditions are told apart by what they are, not by what
103
+ # they respond to (the Math.respond_to? trap once more: third review, S9).
104
+ def statement?(cond) = !cond.is_a?(Symbol) && (cond.is_a?(Expression) || cond.is_a?(Inequality) || cond.is_a?(Equation) || cond.is_a?(Membership) || cond.is_a?(Interval) || cond.is_a?(RealSet))
105
+
106
+ def condition_variables(cond) = statement?(cond) && cond.respond_to?(:variables) ? cond.variables : []
107
+
108
+ # The condition of a branch, typeset (latex.rb asks for this).
109
+ def condition_latex(cond, pw)
110
+ case cond
111
+ when Piecewise::OTHERWISE then "\\text{otherwise}"
112
+ when Interval, RealSet then "#{LaTeX.print(var(pw))} \\in #{cond.to_latex}"
113
+ else LaTeX.of(cond)
114
+ end
115
+ rescue StandardError => rescued
116
+ RCAS.guard!(rescued)
117
+ "\\text{#{cond}}"
118
+ end
119
+
120
+ def substitute(cond, table) = statement?(cond) && cond.respond_to?(:subs) ? cond.subs(table) : cond
121
+ def simplify_condition(cond) = statement?(cond) && cond.respond_to?(:simplify) ? cond.simplify : cond
122
+
123
+ # ---- deciding ----------------------------------------------------------
124
+
125
+ # Pick the branch the conditions decide on, or simplify what is left.
126
+ # The branches are walked in order and only the ones that can still
127
+ # happen are touched: the value of a branch that cannot (1/x under
128
+ # x < 0 at x = 0) is never even simplified.
129
+ def simplify(pw)
130
+ kept = []
131
+ pw.branches.each do |cond, value|
132
+ cond = simplify_condition(cond)
133
+ holds = decide(cond)
134
+ return safely(value) if holds && kept.empty?
135
+ next if holds == false
136
+ kept << [cond, value]
137
+ end
138
+ raise DomainError, "piecewise: no branch holds (#{pw.conditions.join('; ')})" if kept.empty?
139
+ Piecewise.new(kept.map { |cond, value| [cond, safely(value)] })
140
+ end
141
+
142
+ def safely(value)
143
+ value.simplify
144
+ rescue ZeroDivisionError
145
+ value
146
+ end
147
+
148
+ # true, false, or nil when the condition cannot be decided as it stands.
149
+ # Exact on the boundary (Scalar.zero? decides equality), numeric for the
150
+ # sign away from it.
151
+ def decide(cond)
152
+ return true if cond == Piecewise::OTHERWISE
153
+ case cond
154
+ when Interval, RealSet then nil
155
+ when Equation
156
+ cond.variables.empty? ? Scalar.zero?(cond.lhs - cond.rhs) : nil
157
+ when Inequality
158
+ return nil unless cond.variables.empty?
159
+ f = (cond.lhs - cond.rhs).simplify
160
+ return %i[<= >=].include?(cond.op) if Scalar.zero?(f)
161
+ # decided, not read off a Float (the fourth review's policy)
162
+ sign = Inequalities.sign_of(f)
163
+ return nil unless %i[positive negative zero].include?(sign)
164
+ value = { positive: 1, negative: -1, zero: 0 }.fetch(sign)
165
+ value.public_send(cond.op, 0)
166
+ when Membership
167
+ # k in ZZ at k = 5/2: a number is in a set or not; anything else waits
168
+ value = cond.value.simplify
169
+ value.is_a?(Num) ? cond.domain.include?(value.value) : nil
170
+ end
171
+ end
172
+
173
+ # The value of the first branch that holds. An undecidable condition
174
+ # stops the search: the node comes back unchanged rather than guessing.
175
+ def select(pw)
176
+ pw.branches.each do |cond, value|
177
+ holds = decide(cond)
178
+ return pw if holds.nil?
179
+ return value if holds
180
+ end
181
+ raise DomainError, "piecewise: no branch holds (#{pw.conditions.join('; ')})"
182
+ end
183
+
184
+ def at(pw, point)
185
+ x = var(pw)
186
+ value = Expression.lift(point)
187
+ pw.branches.each do |cond, branch|
188
+ holds = cond.is_a?(Interval) || cond.is_a?(RealSet) ? cond.include?(numeric(value)) : decide(substitute(cond, { x => value }))
189
+ raise DomainError, "piecewise: #{cond} does not decide at #{x} = #{point}" if holds.nil?
190
+ return branch.subs(x => value).simplify if holds
191
+ end
192
+ raise DomainError, "piecewise: no branch covers #{x} = #{point}"
193
+ end
194
+
195
+ def numeric(value)
196
+ v = value.is_a?(Numeric) ? value : Expression.lift(value).evalf
197
+ raise DomainError, "piecewise: #{value} is not a real point" unless v.is_a?(Numeric) && v.real?
198
+ v
199
+ end
200
+
201
+ # ---- where the branches live -------------------------------------------
202
+
203
+ def var(pw, given = nil)
204
+ return Expression.lift(given) if given
205
+ names = pw.conditions.flat_map { |c| condition_variables(c) }.uniq
206
+ names = pw.values.flat_map(&:variables).uniq if names.empty?
207
+ raise ArgumentError, "piecewise: which indeterminate? (#{names.join(', ')})" unless names.size == 1
208
+ Var.new(names.first)
209
+ end
210
+
211
+ # The RealSet a single condition describes.
212
+ def set_of(cond, x)
213
+ case cond
214
+ when Piecewise::OTHERWISE then RealSet.reals
215
+ when Interval then RealSet.new([cond])
216
+ when RealSet then cond
217
+ when Equation then RealSet.new(Solve.solve(cond, x.name, principal: true).map { |r| Interval.point(Expression.lift(r)) })
218
+ when Inequality then Inequalities.solve(cond, x)
219
+ end
220
+ end
221
+
222
+ # [[condition, RealSet, value], ...]: the branches with the first-match
223
+ # rule applied, so the sets are disjoint. Branches that are left with
224
+ # nothing disappear.
225
+ def located(pw, x)
226
+ taken = RealSet.empty
227
+ pw.branches.filter_map do |cond, value|
228
+ set = set_of(cond, x)
229
+ raise RCAS::Unsupported, "piecewise: can't locate #{cond}" if set.nil?
230
+ set -= taken
231
+ taken |= set
232
+ set.empty? ? nil : [cond, set, value]
233
+ end
234
+ end
235
+
236
+ def breakpoints(pw, given = nil)
237
+ x = var(pw, given)
238
+ points = located(pw, x).flat_map { |_, set, _| set.intervals.flat_map { |i| [i.low, i.high] } }
239
+ points.reject { |p| Limits.infinite?(p) }.uniq.sort_by { |p| p.evalf }
240
+ end
241
+
242
+ # ---- hoisting -----------------------------------------------------------
243
+
244
+ # An expression with a piecewise inside it is a piecewise: 2*f(x) with
245
+ # f = piecewise(c => v) is piecewise(c => 2*v), because the branches
246
+ # are disjoint. Integration and limits hoist before they start, so
247
+ # that sq(x)*sin(k*x) is integrated branch by branch and not with the
248
+ # piecewise as an opaque atom.
249
+ def hoist(expr)
250
+ node = expr.is_a?(Piecewise) ? nil : expr.each_node.find { |n| n.is_a?(Piecewise) }
251
+ return expr unless node
252
+ hoisted = Piecewise.new(node.branches.map { |cond, value| [cond, hoist(expr.subs(node => value))] })
253
+ flatten(hoisted)
254
+ end
255
+
256
+ # A branch whose value is itself a piecewise: intersect the conditions.
257
+ def flatten(pw)
258
+ return pw unless pw.values.any? { |value| value.is_a?(Piecewise) }
259
+ x = var(pw)
260
+ branches = located(pw, x).flat_map do |_, set, value|
261
+ value.is_a?(Piecewise) ? located(value, x).map { |_, inner, v| [set & inner, v] } : [[set, value]]
262
+ end
263
+ Piecewise.new(branches.reject { |set, _| set.empty? })
264
+ rescue ArgumentError, NotImplementedError, RCAS::Unsupported
265
+ pw
266
+ end
267
+
268
+ # ---- calculus ----------------------------------------------------------
269
+
270
+ # An antiderivative of each branch, shifted so that the pieces fit
271
+ # together where they meet. The constant is not cosmetic: without it
272
+ # every branch is still an antiderivative of its own piece, but the
273
+ # function jumps at the breakpoint and definite integrals across it come
274
+ # out wrong.
275
+ def integrate(pw, x)
276
+ x = Expression.lift(x)
277
+ pieces = located(pw, x).map { |cond, set, value| [cond, set, Integrate.integrate(value, x)] }
278
+ Piecewise.new(glue(pieces, x).map { |cond, _, antiderivative| [cond, antiderivative] })
279
+ end
280
+
281
+ # Walk the pieces from left to right, adding to each the constant that
282
+ # continues the one before it at their common endpoint.
283
+ def glue(pieces, x)
284
+ order = pieces.each_with_index.sort_by { |(_, set, _), _| set.intervals.first.low_value }
285
+ glued = []
286
+ order.each do |(cond, set, antiderivative), index|
287
+ previous = glued.last
288
+ constant = Num.new(0)
289
+ if previous && (point = meeting_point(previous.first[1], set))
290
+ constant = jump(previous.first[2], antiderivative, point, x)
291
+ end
292
+ glued << [[cond, set, (antiderivative + constant).simplify], index]
293
+ end
294
+ glued.sort_by(&:last).map(&:first)
295
+ end
296
+
297
+ # The point where two neighbouring sets meet, if they do.
298
+ def meeting_point(left, right)
299
+ high = left.intervals.last.high
300
+ low = right.intervals.first.low
301
+ return nil if Limits.infinite?(high) || Limits.infinite?(low)
302
+ Scalar.zero?(high - low) ? high : nil
303
+ end
304
+
305
+ # F_left(point) - F_right(point), or zero when that is not a number.
306
+ def jump(before, after, point, x)
307
+ value = (before - after).subs(x => point).simplify
308
+ return Num.new(0) if value.each_node.any? { |n| (n.is_a?(Num) && n.value.is_a?(Float) && !n.value.finite?) || n.is_a?(Integral) }
309
+ value
310
+ rescue StandardError => rescued
311
+ RCAS.guard!(rescued)
312
+ Num.new(0)
313
+ end
314
+
315
+ # Definite integral: the range is cut at the breakpoints and every piece
316
+ # integrated with its own branch.
317
+ def definite(pw, x, from, to)
318
+ x = Expression.lift(x)
319
+ from = Expression.lift(from)
320
+ to = Expression.lift(to)
321
+ return Neg.new(definite(pw, x, to, from)).simplify if from.evalf > to.evalf
322
+ range = RealSet.new([Interval.new(from, to)])
323
+ total = located(pw, x).sum(Num.new(0)) do |_, set, value|
324
+ (set & range).intervals.sum(Num.new(0)) do |piece|
325
+ Scalar.zero?(piece.high - piece.low) ? Num.new(0) : Integrate.definite(value, x, piece.low, piece.high)
326
+ end
327
+ end
328
+ total.simplify
329
+ end
330
+
331
+ # ---- limits -------------------------------------------------------------
332
+
333
+ # The limit picks the branch the point is approached through.
334
+ def limit(pw, x, point, dir = nil)
335
+ x = Expression.lift(x)
336
+ point = Expression.lift(point)
337
+ sides = dir ? [dir] : %i[left right]
338
+ values = sides.map { |side| one_sided(pw, x, point, side) }
339
+ return values.first if values.size == 1 && !values.first.nil?
340
+ return values.first if values.size == 2 && !values.first.nil? && agree?(*values)
341
+ Limit.new(pw, x, point)
342
+ end
343
+
344
+ def one_sided(pw, x, point, side)
345
+ branch = approaching(pw, x, point, side) or return nil
346
+ value = Limits.limit(branch, x, point, side)
347
+ value.is_a?(Limit) ? nil : value
348
+ end
349
+
350
+ # The branch that owns the points just to the left/right of the point.
351
+ def approaching(pw, x, point, side)
352
+ value = interval_value(point)
353
+ found = located(pw, x).find do |_, set, _|
354
+ set.intervals.any? do |i|
355
+ side == :left ? (i.low_value < value && value <= i.high_value) : (i.low_value <= value && value < i.high_value)
356
+ end
357
+ end
358
+ found&.last
359
+ end
360
+
361
+ def interval_value(point) = Limits.infinite?(point) ? (point == OO ? Float::INFINITY : -Float::INFINITY) : point.evalf
362
+
363
+ # ---- continuity ---------------------------------------------------------
364
+
365
+ # The breakpoints where the two sides disagree, or where the function's
366
+ # value is not the common limit: the jumps.
367
+ def discontinuities(pw, given = nil)
368
+ x = var(pw, given)
369
+ breakpoints(pw, x).reject do |point|
370
+ left = one_sided(pw, x, point, :left)
371
+ right = one_sided(pw, x, point, :right)
372
+ next false if left.nil? || right.nil?
373
+ value = begin
374
+ at(pw, point)
375
+ rescue DomainError
376
+ nil
377
+ end
378
+ agree?(left, right) && (value.nil? || agree?(left, value))
379
+ end
380
+ end
381
+
382
+ # The breakpoints where the function is continuous but the one-sided
383
+ # derivatives differ: the corners.
384
+ def kinks(pw, given = nil)
385
+ x = var(pw, given)
386
+ jumps = discontinuities(pw, x)
387
+ derivative = Piecewise.new(pw.branches.map { |cond, value| [cond, Differentiate.diff(value, x).simplify] })
388
+ breakpoints(pw, x).reject do |point|
389
+ next true if jumps.include?(point)
390
+ agree?(one_sided(derivative, x, point, :left), one_sided(derivative, x, point, :right))
391
+ end
392
+ end
393
+
394
+ def agree?(a, b)
395
+ return false if a.nil? || b.nil?
396
+ return true if a == b
397
+ return false if a.is_a?(Limit) || b.is_a?(Limit)
398
+ Scalar.zero?(Expression.lift(a) - Expression.lift(b))
399
+ rescue StandardError => rescued
400
+ RCAS.guard!(rescued)
401
+ false
402
+ end
403
+
404
+ # ---- solving -------------------------------------------------------------
405
+
406
+ # Solve branch by branch and keep the roots that lie in their own piece.
407
+ # A branch that is equal to the right-hand side everywhere contributes
408
+ # its whole set, and then the answer is a RealSet rather than a list.
409
+ # Every branch solved on its piece, completely: a periodic branch keeps
410
+ # all its periods inside the piece (sin(x) = 0 on x > 0 is
411
+ # {pi + pi*k | k in NN}, not pi alone), and a branch that is the
412
+ # right-hand side everywhere contributes its whole piece - solve answers
413
+ # that with a set now, and the old rescue waited for an error that no
414
+ # longer comes (third review, S8).
415
+ def solve(pw, rhs, given = nil)
416
+ x = var(pw, given)
417
+ rhs = Expression.lift(rhs)
418
+ roots = []
419
+ families = []
420
+ everywhere = []
421
+ located(pw, x).each do |_, set, value|
422
+ found = begin
423
+ Solve.solve(Equation.new(value, rhs), x.name, domain: RR)
424
+ rescue ArgumentError => e
425
+ raise unless e.message.include?("every value")
426
+ everywhere << set
427
+ next
428
+ end
429
+ unless found.is_a?(Array)
430
+ everywhere << (found.is_a?(RealSet) ? set & found : set)
431
+ next
432
+ end
433
+ found.each do |r|
434
+ if r.is_a?(ImageSet)
435
+ within(set, r).each { |m| m.is_a?(ImageSet) ? families << m : roots << m }
436
+ elsif inside?(set, r)
437
+ roots << r
438
+ end
439
+ end
440
+ end
441
+ roots = roots.uniq.sort_by { |r| Expression.lift(r).evalf }
442
+ return roots + families if everywhere.empty?
443
+ raise RCAS::Unsupported, "piecewise: a whole piece and infinitely many points do not make one set" unless families.empty?
444
+ everywhere.reduce(RealSet.new(roots.map { |r| Interval.point(Expression.lift(r)) })) { |a, b| a | b }
445
+ end
446
+
447
+ def inside?(set, root)
448
+ value = Expression.lift(root).evalf
449
+ value.is_a?(Numeric) && value.real? && set.include?(value)
450
+ end
451
+
452
+ MAX_POINTS = 1000
453
+
454
+ # The members of the family a + b*k that lie in the set: the points of a
455
+ # bounded piece, and a family over NN running away from the end of an
456
+ # unbounded one.
457
+ def within(set, family)
458
+ return [] if family.nonreal?
459
+ return [family] unless family.parameters.size == 1 && family.domain == ZZ
460
+ k = family.parameters.first
461
+ a = family.at(0)
462
+ b = (family.at(1) - a).simplify
463
+ av = Analysis.numeric(a)
464
+ bv = Analysis.numeric(b)
465
+ raise RCAS::Unsupported, "piecewise: can't place #{family} in #{set}" if av.nil? || bv.nil? || bv.zero?
466
+ set.intervals.flat_map do |interval|
467
+ lo, hi = interval.low_value, interval.high_value
468
+ first = lo.infinite? ? nil : ((lo - av) / bv)
469
+ last = hi.infinite? ? nil : ((hi - av) / bv)
470
+ first, last = last, first if bv.negative?
471
+ low_k = first && (first.ceil - 1)
472
+ high_k = last && (last.floor + 1)
473
+ if low_k && high_k
474
+ raise RCAS::Unsupported, "piecewise: too many solutions in #{interval}" if high_k - low_k > MAX_POINTS
475
+ (low_k..high_k).map { |i| family.at(i) }.select { |m| interval.include?(m) }
476
+ elsif low_k
477
+ start = (low_k..low_k + 3).find { |i| interval.include?(family.at(i)) }
478
+ start ? [ImageSet.new((a + b * (Num.new(start) + k)).simplify, [k], NN)] : []
479
+ elsif high_k
480
+ start = (high_k - 3..high_k).to_a.reverse.find { |i| interval.include?(family.at(i)) }
481
+ start ? [ImageSet.new((a + b * (Num.new(start) - k)).simplify, [k], NN)] : []
482
+ else
483
+ [family]
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end