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,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
|