rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
data/lib/rcas/domains.rb
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Raised when an object is not an element of the domain it is used in.
|
|
5
|
+
class DomainError < ArgumentError; end
|
|
6
|
+
|
|
7
|
+
# Marker for algebraic objects (Polynomial, Vector, Matrix) that know how to
|
|
8
|
+
# combine with a plain Expression on their left: `x * v`, `2 + f`.
|
|
9
|
+
module Algebraic
|
|
10
|
+
def rop(op, left)
|
|
11
|
+
raise TypeError, "can't apply #{op} to #{left.class} and #{self.class}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# m.in?(ZZ**[3, 3]), f.in?(QQ[x]): the same question an expression
|
|
15
|
+
# answers with x.in?(RR), and the same answer the domain gives to
|
|
16
|
+
# include?. Only a variable can be *declared* with `in`, so that stays
|
|
17
|
+
# on Expression.
|
|
18
|
+
def in?(domain) = domain.include?(self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The double-struck letters. They are always available as input (ℤ[x] is
|
|
22
|
+
# ZZ[x]); whether results print that way is the unicode setting, which is
|
|
23
|
+
# off by default so that output stays ASCII and pastes anywhere.
|
|
24
|
+
UNICODE = { "NN" => "ℕ", "ZZ" => "ℤ", "QQ" => "ℚ", "RR" => "ℝ", "CC" => "ℂ",
|
|
25
|
+
"pi" => "π", "oo" => "∞" }.freeze
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def unicode? = @unicode.nil? ? ENV["RCAS_UNICODE"] == "1" : @unicode
|
|
29
|
+
|
|
30
|
+
# RCAS.unicode = true prints ℤ, π and ∞ instead of ZZ, pi and oo.
|
|
31
|
+
def unicode=(value)
|
|
32
|
+
@unicode = value.nil? ? nil : !(value == false || value.to_s == "off" || value.to_s == "false")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The name to print for +ascii+ under the current setting.
|
|
36
|
+
def symbol(ascii) = unicode? ? UNICODE.fetch(ascii.to_s, ascii.to_s) : ascii.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# "x in ZZ": the statement, not the answer. `x.in?(ZZ)` decides membership
|
|
40
|
+
# and returns true or false; inside hold { } the same call is kept as this,
|
|
41
|
+
# the way `==` is kept as an Equation, so that a condition can be written
|
|
42
|
+
# down, printed, typeset (x \in \mathbb{Z}) and carried around.
|
|
43
|
+
#
|
|
44
|
+
# hold { x.in?(ZZ) } # => x in ZZ
|
|
45
|
+
# assumptions # => {:x=>x in ZZ}
|
|
46
|
+
class Membership
|
|
47
|
+
attr_reader :value, :domain
|
|
48
|
+
|
|
49
|
+
def initialize(value, domain)
|
|
50
|
+
@value = value.is_a?(Expression) || value.is_a?(Numeric) || value.is_a?(Symbol) ? Expression.lift(value) : value
|
|
51
|
+
@domain = domain
|
|
52
|
+
freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Does it hold? The domain decides, exactly where it can.
|
|
56
|
+
def holds? = domain.include?(value)
|
|
57
|
+
|
|
58
|
+
def variables = value.respond_to?(:variables) ? value.variables : []
|
|
59
|
+
def subs(*args) = Membership.new(value.subs(*args), domain)
|
|
60
|
+
def simplify = Membership.new(value.simplify, domain)
|
|
61
|
+
# hold { x.in?(ZZ) }.doit evaluates the formal nodes of the value (C9)
|
|
62
|
+
def evaluate = Membership.new(value.respond_to?(:evaluate) ? value.evaluate : value, domain)
|
|
63
|
+
alias doit evaluate
|
|
64
|
+
alias unhold evaluate
|
|
65
|
+
|
|
66
|
+
def ==(other) = other.is_a?(Membership) && other.value == value && other.domain == domain
|
|
67
|
+
alias eql? ==
|
|
68
|
+
def hash = [Membership, value, domain].hash
|
|
69
|
+
|
|
70
|
+
def to_s = "#{value} in #{domain}"
|
|
71
|
+
alias inspect to_s
|
|
72
|
+
|
|
73
|
+
def to_latex(wrap: nil) = "#{LaTeX.of(value, wrap: wrap)} \\in #{LaTeX.of(domain)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Common protocol of NN, ZZ, QQ, RR, CC, polynomial rings and fraction fields.
|
|
77
|
+
class Domain
|
|
78
|
+
def include?(_obj) = raise(RCAS::Unsupported)
|
|
79
|
+
alias member? include?
|
|
80
|
+
def ===(obj) = include?(obj)
|
|
81
|
+
|
|
82
|
+
def subset?(_other) = raise(RCAS::Unsupported)
|
|
83
|
+
def superset?(other) = other.subset?(self)
|
|
84
|
+
def <=(other) = subset?(other)
|
|
85
|
+
def >=(other) = superset?(other)
|
|
86
|
+
def <(other) = subset?(other) && self != other
|
|
87
|
+
def >(other) = superset?(other) && self != other
|
|
88
|
+
|
|
89
|
+
def ring? = true
|
|
90
|
+
def field? = false
|
|
91
|
+
def fraction_field = self
|
|
92
|
+
|
|
93
|
+
# A domain of numbers: its elements can be polynomial coefficients and
|
|
94
|
+
# matrix entries. The spaces say no, and that is what keeps a polynomial
|
|
95
|
+
# ring from being built over them.
|
|
96
|
+
def scalar? = true
|
|
97
|
+
|
|
98
|
+
# Smallest standard domain containing both.
|
|
99
|
+
def join(_other) = raise(RCAS::Unsupported)
|
|
100
|
+
|
|
101
|
+
# ZZ[x], QQ[x, y]
|
|
102
|
+
def [](*vars)
|
|
103
|
+
bad = vars.flatten.reject { |v| v.is_a?(Symbol) || v.is_a?(Var) || v.is_a?(String) }
|
|
104
|
+
unless bad.empty?
|
|
105
|
+
raise DomainError, "#{self}[#{bad.first.inspect}]: the brackets name the indeterminates of a " \
|
|
106
|
+
"polynomial ring, as in #{self}[x]; for an element of #{self} write #{self}.(#{bad.first})"
|
|
107
|
+
end
|
|
108
|
+
PolynomialRing.new(self, vars)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# QQ**3 is a vector space, QQ**[2, 3] a space of 2x3 matrices.
|
|
112
|
+
def **(shape)
|
|
113
|
+
shape.is_a?(Array) ? MatrixSpace.new(self, *shape) : VectorSpace.new(self, shape)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Declare a symbol to be an element of this domain and return it as a Var.
|
|
117
|
+
def var(name) = RCAS.assume(name => self) && Var.new(name)
|
|
118
|
+
|
|
119
|
+
def vector(*entries) = (self**entries.size)[*entries]
|
|
120
|
+
def matrix(rows) = MatrixSpace.new(self, rows.size, rows.first.size)[*rows]
|
|
121
|
+
|
|
122
|
+
def to_s = RCAS.symbol(name)
|
|
123
|
+
def inspect = to_s
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# NN, ZZ, QQ, RR, CC. Membership is by value for exact types (2/1 is an
|
|
127
|
+
# integer) and by type for floats (floats are reals).
|
|
128
|
+
class NumberSet < Domain
|
|
129
|
+
attr_reader :name, :rank
|
|
130
|
+
|
|
131
|
+
def initialize(name, rank)
|
|
132
|
+
@name = name.to_s
|
|
133
|
+
@rank = rank
|
|
134
|
+
freeze
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def include?(obj)
|
|
138
|
+
case obj
|
|
139
|
+
when Integer then obj >= 0 || rank >= 1
|
|
140
|
+
when Rational then obj.denominator == 1 ? include?(obj.numerator) : rank >= 2
|
|
141
|
+
when Complex then obj.imaginary.zero? ? include?(obj.real) : rank >= 4
|
|
142
|
+
when Numeric then obj.real? ? rank >= 3 : rank >= 4
|
|
143
|
+
when Symbol then include?(Var.new(obj))
|
|
144
|
+
when Expression then expression_member?(obj)
|
|
145
|
+
when Polynomial then obj.constant? && include?(obj.constant_term)
|
|
146
|
+
else false
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The inferred domain answers first; a constant it cannot place is
|
|
151
|
+
# simplified and looked at again: sqrt(2)**2 is 2 and pi - pi is 0,
|
|
152
|
+
# both integers (third review, C13).
|
|
153
|
+
def expression_member?(obj)
|
|
154
|
+
d = obj.domain
|
|
155
|
+
return true if d&.subset?(self)
|
|
156
|
+
return false unless obj.constant?
|
|
157
|
+
reduced = obj.simplify
|
|
158
|
+
return include?(reduced.value) if reduced.is_a?(Num)
|
|
159
|
+
# (1 + sqrt(2))**2 - 2*sqrt(2) is 3 once expanded (fourth review)
|
|
160
|
+
expanded = reduced.expand
|
|
161
|
+
return include?(expanded.value) if expanded.is_a?(Num)
|
|
162
|
+
reduced != obj && (d2 = reduced.domain) ? d2.subset?(self) : false
|
|
163
|
+
rescue StandardError => rescued
|
|
164
|
+
RCAS.guard!(rescued)
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def subset?(other)
|
|
169
|
+
case other
|
|
170
|
+
when NumberSet then rank <= other.rank
|
|
171
|
+
when PolynomialRing then subset?(other.base)
|
|
172
|
+
when FractionField then subset?(other.ring)
|
|
173
|
+
else false
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def ring? = rank >= 1
|
|
178
|
+
def field? = rank >= 2
|
|
179
|
+
def fraction_field = field? ? self : QQ
|
|
180
|
+
|
|
181
|
+
def join(other)
|
|
182
|
+
case other
|
|
183
|
+
when NumberSet then rank >= other.rank ? self : other
|
|
184
|
+
else other.join(self)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def [](*vars)
|
|
189
|
+
raise DomainError, "#{name} is not a ring; use ZZ or a field" unless ring?
|
|
190
|
+
super
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ZZ.(3), QQ.(1/2r): the value as an element of this set. The brackets
|
|
194
|
+
# build a polynomial ring, so this is the way in for an element - the
|
|
195
|
+
# same spelling a polynomial ring and a finite field already use.
|
|
196
|
+
def call(value)
|
|
197
|
+
lifted = Expression.lift(value)
|
|
198
|
+
raise DomainError, "#{lifted} is not in #{name}" unless include?(lifted)
|
|
199
|
+
lifted
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Smallest number set containing a Ruby number.
|
|
203
|
+
def self.of(value)
|
|
204
|
+
Sets::ALL.find { |s| s.include?(value) } or raise DomainError, "#{value.inspect} is not a number"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
NN = NumberSet.new(:NN, 0) # 0, 1, 2, ...
|
|
209
|
+
ZZ = NumberSet.new(:ZZ, 1)
|
|
210
|
+
QQ = NumberSet.new(:QQ, 2)
|
|
211
|
+
RR = NumberSet.new(:RR, 3)
|
|
212
|
+
CC = NumberSet.new(:CC, 4)
|
|
213
|
+
|
|
214
|
+
# The double-struck letters are constants, not methods: Ruby reads ℤ as an
|
|
215
|
+
# uppercase letter and therefore as a constant. ℤ[x] is ZZ[x].
|
|
216
|
+
ℕ = NN
|
|
217
|
+
ℤ = ZZ
|
|
218
|
+
ℚ = QQ
|
|
219
|
+
ℝ = RR
|
|
220
|
+
ℂ = CC
|
|
221
|
+
|
|
222
|
+
# `include RCAS::Sets` brings NN, ZZ, QQ, RR, CC (and ℕ ℤ ℚ ℝ ℂ) into scope.
|
|
223
|
+
module Sets
|
|
224
|
+
NN = RCAS::NN
|
|
225
|
+
ZZ = RCAS::ZZ
|
|
226
|
+
QQ = RCAS::QQ
|
|
227
|
+
RR = RCAS::RR
|
|
228
|
+
CC = RCAS::CC
|
|
229
|
+
ℕ = RCAS::NN
|
|
230
|
+
ℤ = RCAS::ZZ
|
|
231
|
+
ℚ = RCAS::QQ
|
|
232
|
+
ℝ = RCAS::RR
|
|
233
|
+
ℂ = RCAS::CC
|
|
234
|
+
ALL = [NN, ZZ, QQ, RR, CC].freeze
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# ---- variable assumptions ----------------------------------------------
|
|
238
|
+
|
|
239
|
+
@assumptions = {}
|
|
240
|
+
@signs = {}
|
|
241
|
+
|
|
242
|
+
SIGNS = { :> => :positive, :>= => :nonnegative, :< => :negative, :<= => :nonpositive }.freeze
|
|
243
|
+
|
|
244
|
+
class << self
|
|
245
|
+
# RCAS.assume(x: ZZ, y: RR) declares domains, RCAS.assume(x > 0) a sign.
|
|
246
|
+
# Both may be given at once: assume(x > 0, n: ZZ).
|
|
247
|
+
#
|
|
248
|
+
# With a block the assumptions hold for that block alone and whatever
|
|
249
|
+
# was declared before comes back afterwards, however the block ends:
|
|
250
|
+
#
|
|
251
|
+
# assume(x: ZZ) { solve(eq(x/3, 1/2), x) } # => []
|
|
252
|
+
#
|
|
253
|
+
# which is Mathematica's Assuming and Maple's `assuming`, written the
|
|
254
|
+
# way Ruby scopes anything else. The value is the block's.
|
|
255
|
+
#
|
|
256
|
+
# A statement is taken whole or not at all: everything is checked before
|
|
257
|
+
# anything is recorded, so a refused `assume(x: ZZ, y: 3)` leaves x as it
|
|
258
|
+
# was. A sign that no member of the declared domain has (x in NN and
|
|
259
|
+
# x < 0) is refused as well.
|
|
260
|
+
def assume(*facts, **table, &block)
|
|
261
|
+
return assuming(facts, table, &block) if block
|
|
262
|
+
signs = facts.map do |fact|
|
|
263
|
+
raise TypeError, "#{fact.inspect} is not a domain or a sign like x > 0" unless fact.is_a?(Inequality)
|
|
264
|
+
parse_sign(fact)
|
|
265
|
+
end
|
|
266
|
+
domains = table.map do |name, domain|
|
|
267
|
+
name = name.name if name.is_a?(Var)
|
|
268
|
+
raise TypeError, "#{name.inspect} is not a variable" unless name.is_a?(Symbol)
|
|
269
|
+
raise TypeError, "#{domain.inspect} is not a number set" unless domain.is_a?(NumberSet)
|
|
270
|
+
[name, domain]
|
|
271
|
+
end
|
|
272
|
+
new_domains = @assumptions.merge(domains.to_h)
|
|
273
|
+
new_signs = @signs.merge(signs.to_h)
|
|
274
|
+
new_signs.each do |name, sign|
|
|
275
|
+
domain = new_domains[name]
|
|
276
|
+
next unless domain && domain <= NN && %i[negative].include?(sign)
|
|
277
|
+
raise ArgumentError, "assume: #{name} in #{domain} cannot be negative"
|
|
278
|
+
end
|
|
279
|
+
@assumptions.replace(new_domains)
|
|
280
|
+
@signs.replace(new_signs)
|
|
281
|
+
true
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The whole table is saved and put back, so that an assume or a forget
|
|
285
|
+
# inside the block is local to it too - the dynamic scoping Mathematica
|
|
286
|
+
# gives $Assumptions.
|
|
287
|
+
def assuming(facts, table)
|
|
288
|
+
saved_assumptions = @assumptions.dup
|
|
289
|
+
saved_signs = @signs.dup
|
|
290
|
+
assume(*facts, **table)
|
|
291
|
+
yield
|
|
292
|
+
ensure
|
|
293
|
+
@assumptions.replace(saved_assumptions)
|
|
294
|
+
@signs.replace(saved_signs)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# x > 0, x <= 0: the sign of a variable, which simplification and abs use.
|
|
298
|
+
def parse_sign(fact)
|
|
299
|
+
variable, relation = fact.lhs.is_a?(Var) ? [fact.lhs, fact.op] : [fact.rhs, Inequality::FLIP[fact.op]]
|
|
300
|
+
other = fact.lhs.is_a?(Var) ? fact.rhs : fact.lhs
|
|
301
|
+
sign = SIGNS[relation]
|
|
302
|
+
unless variable.is_a?(Var) && sign && other.is_a?(Num) && other.value.zero?
|
|
303
|
+
raise TypeError, "assume: a sign is x > 0, x >= 0, x < 0 or x <= 0, got #{fact}"
|
|
304
|
+
end
|
|
305
|
+
[variable.name, sign]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def forget(*names)
|
|
309
|
+
if names.empty?
|
|
310
|
+
@assumptions.clear
|
|
311
|
+
@signs.clear
|
|
312
|
+
return true
|
|
313
|
+
end
|
|
314
|
+
names.each do |n|
|
|
315
|
+
key = n.is_a?(Var) ? n.name : n
|
|
316
|
+
@assumptions.delete(key)
|
|
317
|
+
@signs.delete(key)
|
|
318
|
+
end
|
|
319
|
+
true
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Domains as number sets, signs as the inequality they stand for.
|
|
323
|
+
# Every assumption as the statement it is: a sign was already an
|
|
324
|
+
# Inequality, and a domain is a Membership rather than the bare set.
|
|
325
|
+
# `assumption(name)` is the domain itself, which is what Infer and solve
|
|
326
|
+
# want.
|
|
327
|
+
# A name with both a domain and a sign lists both, as an Array: merging
|
|
328
|
+
# the two tables used to let the sign overwrite the domain.
|
|
329
|
+
def assumptions
|
|
330
|
+
domains = @assumptions.to_h { |name, domain| [name, Membership.new(Var.new(name), domain)] }
|
|
331
|
+
domains.merge(@signs.to_h { |name, sign| [name, sign_statement(name, sign)] }) { |_, domain, sign| [domain, sign] }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def sign_statement(name, sign)
|
|
335
|
+
relation = SIGNS.key(sign)
|
|
336
|
+
Inequality.new(Var.new(name), relation, Num.new(0))
|
|
337
|
+
end
|
|
338
|
+
def assumption(name) = @assumptions[name]
|
|
339
|
+
def signs = @signs.dup
|
|
340
|
+
|
|
341
|
+
# :positive, :nonnegative, :negative, :nonpositive or nil, for a variable
|
|
342
|
+
# or for an expression whose factors are all known.
|
|
343
|
+
def sign_of(expr)
|
|
344
|
+
case expr
|
|
345
|
+
when Symbol then @signs[expr]
|
|
346
|
+
when Var then @signs[expr.name] || (assumption(expr.name)&.<=(NN) ? :nonnegative : nil)
|
|
347
|
+
when Num then numeric_sign(expr.value)
|
|
348
|
+
when Const then numeric_sign(expr.value) # pi and oo are positive
|
|
349
|
+
when Expression then expression_sign(expr)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def numeric_sign(value)
|
|
354
|
+
return nil unless value.is_a?(Numeric) && value.real?
|
|
355
|
+
return nil if value.respond_to?(:nan?) && value.nan? # undefined has no sign
|
|
356
|
+
return :positive if value.positive?
|
|
357
|
+
return :negative if value.negative?
|
|
358
|
+
:nonnegative
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# A product of knowns, or an even power, or a sum of terms of one sign.
|
|
362
|
+
# -a is negative when a is positive (the fourth review: a pole at -a
|
|
363
|
+
# stayed "possibly inside" under a > 0), and a power of a positive base
|
|
364
|
+
# is positive only with a real exponent - |2**i| is 1, not 2**i.
|
|
365
|
+
def expression_sign(expr)
|
|
366
|
+
case expr
|
|
367
|
+
when Neg then negated_sign(sign_of(expr.arg))
|
|
368
|
+
when Sub then sum_sign(sign_of(expr.left), negated_sign(sign_of(expr.right)))
|
|
369
|
+
when Add then sum_sign(sign_of(expr.left), sign_of(expr.right))
|
|
370
|
+
when Mul then combine_signs(sign_of(expr.left), sign_of(expr.right))
|
|
371
|
+
when Div
|
|
372
|
+
# where the quotient is defined its denominator is not 0
|
|
373
|
+
below = { nonnegative: :positive, nonpositive: :negative }.fetch(sign_of(expr.right)) { |s| s }
|
|
374
|
+
combine_signs(sign_of(expr.left), below)
|
|
375
|
+
when Pow
|
|
376
|
+
base_sign = sign_of(expr.base)
|
|
377
|
+
exponent = expr.exponent
|
|
378
|
+
whole = exponent.is_a?(Num) && exponent.value.is_a?(Integer)
|
|
379
|
+
return :positive if base_sign == :positive && (whole || real?(exponent))
|
|
380
|
+
# An even power is not negative - of a real number. y**2 at y = 2i
|
|
381
|
+
# is -4, and an undeclared y is not real (the branch-cut policy),
|
|
382
|
+
# so abs(y**2 + 1) keeps its abs until y is declared.
|
|
383
|
+
if whole && exponent.value.even?
|
|
384
|
+
return :positive if base_sign == :negative
|
|
385
|
+
return :nonnegative if base_sign || real?(expr.base)
|
|
386
|
+
end
|
|
387
|
+
return base_sign if whole && %i[negative nonpositive].include?(base_sign)
|
|
388
|
+
nil
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
STRICT = { positive: :nonnegative, negative: :nonpositive }.freeze
|
|
393
|
+
UP = %i[positive nonnegative].freeze
|
|
394
|
+
DOWN = %i[negative nonpositive].freeze
|
|
395
|
+
|
|
396
|
+
def negated_sign(sign) = { positive: :negative, negative: :positive, nonnegative: :nonpositive, nonpositive: :nonnegative }[sign]
|
|
397
|
+
|
|
398
|
+
def sum_sign(left, right)
|
|
399
|
+
[UP, DOWN].each do |side|
|
|
400
|
+
next unless side.include?(left) && side.include?(right)
|
|
401
|
+
return [left, right].include?(side.first) ? side.first : side.last
|
|
402
|
+
end
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def combine_signs(left, right)
|
|
407
|
+
return nil if left.nil? || right.nil?
|
|
408
|
+
up = [left, right].count { |s| DOWN.include?(s) }.even?
|
|
409
|
+
strict = [left, right].all? { |s| STRICT.key?(s) }
|
|
410
|
+
if up
|
|
411
|
+
strict ? :positive : :nonnegative
|
|
412
|
+
else
|
|
413
|
+
strict ? :negative : :nonpositive
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def nonnegative?(expr) = %i[positive nonnegative].include?(sign_of(expr))
|
|
418
|
+
|
|
419
|
+
def real?(expr)
|
|
420
|
+
d = Infer.domain(expr)
|
|
421
|
+
!d.nil? && d <= RR
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Infers the smallest number set an expression's value must lie in, given
|
|
426
|
+
# the declared domains of its variables. Returns nil when unknown.
|
|
427
|
+
module Infer
|
|
428
|
+
module_function
|
|
429
|
+
|
|
430
|
+
def domain(expr)
|
|
431
|
+
case expr
|
|
432
|
+
when Num then expr.finite_field? ? expr.value.field : NumberSet.of(expr.value)
|
|
433
|
+
when Const then %i[oo undefined].include?(expr.name) ? nil : RR
|
|
434
|
+
when RootOf then expr.real? ? RR : CC
|
|
435
|
+
when Var then RCAS.assumption(expr.name) || (RCAS.signs[expr.name] ? RR : nil) # a sign says real
|
|
436
|
+
when Neg then no_naturals(domain(expr.arg))
|
|
437
|
+
when Add, Mul then join(domain(expr.left), domain(expr.right))
|
|
438
|
+
when Sub then no_naturals(join(domain(expr.left), domain(expr.right)))
|
|
439
|
+
when Div
|
|
440
|
+
below = domain(expr.right)
|
|
441
|
+
# no value where the denominator is 0: no claim until it is known not
|
|
442
|
+
# to be (C5 for x in NN, and the fifth review's decision for RR too)
|
|
443
|
+
return nil unless nonzero?(expr.right)
|
|
444
|
+
join(join(domain(expr.left), below), QQ)
|
|
445
|
+
when Pow then power(expr)
|
|
446
|
+
when Fn then function(expr)
|
|
447
|
+
when Piecewise then expr.values.map { |v| domain(v) }.reduce { |a, b| join(a, b) }
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def join(a, b)
|
|
452
|
+
return nil if a.nil? || b.nil?
|
|
453
|
+
a.join(b)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# How far from an integer a Float has to be before it is one no longer.
|
|
457
|
+
TOLERANCE = 1e-6
|
|
458
|
+
|
|
459
|
+
# Is `value` demonstrably outside `domain`? Membership is decided where
|
|
460
|
+
# it can be and left open where it cannot, so `false` means "not shown
|
|
461
|
+
# to be outside" and never "inside": a solution dropped by mistake is
|
|
462
|
+
# worse than one that should not be there. Solve filters its answers
|
|
463
|
+
# with this, so that a declared domain is respected.
|
|
464
|
+
def excluded?(value, domain)
|
|
465
|
+
return false unless domain.is_a?(NumberSet)
|
|
466
|
+
value = Expression.lift(value).simplify
|
|
467
|
+
return !domain.include?(value.value) if value.is_a?(Num)
|
|
468
|
+
return false unless value.variables.empty? # a parameter decides nothing
|
|
469
|
+
return true if domain <= QQ && irrational?(value)
|
|
470
|
+
numeric = value.evalf
|
|
471
|
+
return false unless numeric.is_a?(Numeric)
|
|
472
|
+
return domain <= RR if numeric.is_a?(Complex) && !numeric.imaginary.to_f.abs.zero?
|
|
473
|
+
real = numeric.is_a?(Complex) ? numeric.real.to_f : numeric.to_f
|
|
474
|
+
return true if domain == NN && real.negative?
|
|
475
|
+
domain <= ZZ && (real - real.round).abs > TOLERANCE
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Irrational for a reason rcas can name: an algebraic number whose
|
|
479
|
+
# minimal polynomial has degree above one (2**(1/2), a real RootOf), or
|
|
480
|
+
# a rational multiple of pi or e, which their transcendence settles.
|
|
481
|
+
# Everything else (pi + log(2)) is left open, as it is in the literature.
|
|
482
|
+
def irrational?(value)
|
|
483
|
+
algebraic = Algebraic.exact(value)
|
|
484
|
+
return !algebraic.poly.constant? if algebraic
|
|
485
|
+
coeff, factors = Simplify.factorize(value)
|
|
486
|
+
return false if coeff.zero? || factors.size != 1
|
|
487
|
+
base, exponent = factors.first
|
|
488
|
+
exponent == 1 && (base == PI || base == E)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def no_naturals(d) = d == NN ? ZZ : d
|
|
492
|
+
|
|
493
|
+
def power(expr)
|
|
494
|
+
base = domain(expr.base)
|
|
495
|
+
exp = expr.exponent
|
|
496
|
+
return nil if base.nil?
|
|
497
|
+
|
|
498
|
+
if exp.is_a?(Num)
|
|
499
|
+
v = exp.value
|
|
500
|
+
return base if v.is_a?(Integer) && v >= 0
|
|
501
|
+
# 1/x and x**(-1/2) have no value at 0, which x in NN may be: no
|
|
502
|
+
# claim, as for log(x) (fourth review, C5)
|
|
503
|
+
return nil if v.real? && v.negative? && !nonzero?(expr.base)
|
|
504
|
+
return base.join(QQ) if v.is_a?(Integer)
|
|
505
|
+
return CC unless v.real?
|
|
506
|
+
# a zero base was sent away above for a negative exponent, so not
|
|
507
|
+
# negative is enough: sqrt(pi*k) for k in NN
|
|
508
|
+
return RR if base <= NN || (base <= RR && non_negative_base?(expr.base))
|
|
509
|
+
return CC
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
ed = domain(exp)
|
|
513
|
+
return nil if ed.nil?
|
|
514
|
+
return base if ed <= NN
|
|
515
|
+
return base.join(QQ) if ed <= ZZ
|
|
516
|
+
return RR if ed <= RR && (base <= NN || (base <= RR && positive_base?(expr.base)))
|
|
517
|
+
CC
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# The matrix and vector constructors ask a different question from
|
|
521
|
+
# `domain`: are the entries real *where they have a value*? A removable
|
|
522
|
+
# pole (a**2 - 1)/(a - 1) is a real entry of a real matrix. Inside the
|
|
523
|
+
# block a denominator counts as not vanishing.
|
|
524
|
+
def where_defined
|
|
525
|
+
outer = Thread.current[:rcas_where_defined]
|
|
526
|
+
Thread.current[:rcas_where_defined] = true
|
|
527
|
+
yield
|
|
528
|
+
ensure
|
|
529
|
+
Thread.current[:rcas_where_defined] = outer
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# A denominator known not to vanish: by its sign, or decided for a
|
|
533
|
+
# constant.
|
|
534
|
+
def nonzero?(e)
|
|
535
|
+
return true if Thread.current[:rcas_where_defined] && !e.variables.empty?
|
|
536
|
+
return Decide.zero?(e) == false if e.variables.empty?
|
|
537
|
+
%i[positive negative].include?(RCAS.sign_of(e))
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# sqrt(pi) and sqrt(7 - 4*sqrt(3)) are real: the radicand is positive
|
|
541
|
+
# (fourth review: x**2 < pi was refused as "cannot decide").
|
|
542
|
+
def non_negative_base?(b)
|
|
543
|
+
sign = RCAS.sign_of(b)
|
|
544
|
+
return %i[positive nonnegative zero].include?(sign) if sign || !b.variables.empty?
|
|
545
|
+
%i[positive zero].include?(Decide.sign(b))
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def positive_base?(b)
|
|
549
|
+
sign = RCAS.sign_of(b)
|
|
550
|
+
return sign == :positive if sign || !b.variables.empty?
|
|
551
|
+
Decide.sign(b) == :positive
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def function(expr)
|
|
555
|
+
if expr.name == :surd && expr.args.size == 2
|
|
556
|
+
# the real root of a real number, for an odd index
|
|
557
|
+
base = domain(expr.args.first)
|
|
558
|
+
n = expr.args.last
|
|
559
|
+
odd = n.is_a?(Num) && n.value.is_a?(Integer) && n.value.odd?
|
|
560
|
+
return base && base <= RR && (odd || positive_base?(expr.args.first)) ? RR : nil
|
|
561
|
+
end
|
|
562
|
+
arg = expr.args.size == 1 ? domain(expr.args.first) : nil
|
|
563
|
+
return nil if arg.nil?
|
|
564
|
+
|
|
565
|
+
case expr.name
|
|
566
|
+
when :sin, :cos, :tan, :exp then arg <= RR ? RR : CC
|
|
567
|
+
when :abs then arg <= ZZ ? NN : arg
|
|
568
|
+
when :sign, :floor, :ceil, :round then ZZ
|
|
569
|
+
when :re, :im, :arg then RR
|
|
570
|
+
when :erf, :erfc then arg <= RR ? RR : CC
|
|
571
|
+
when :Ei, :Si, :Ci, :li then arg <= RR ? RR : CC
|
|
572
|
+
when :conj then arg
|
|
573
|
+
when :fibonacci then arg <= NN ? NN : ZZ
|
|
574
|
+
when :bernoulli, :harmonic then QQ
|
|
575
|
+
when :log then log_domain(expr.args.first, arg)
|
|
576
|
+
else CC
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
module Infer
|
|
582
|
+
module_function
|
|
583
|
+
|
|
584
|
+
# log(u) is real for a u shown to be positive; log(0) has no value at
|
|
585
|
+
# all, so it lies in no set (it was RR, and so was log(x) for x in NN,
|
|
586
|
+
# where 0 is a member: third review, C5); anything else may be complex.
|
|
587
|
+
def log_domain(u, arg)
|
|
588
|
+
return nil if u.is_a?(Num) && u.value.is_a?(Numeric) && u.value.zero?
|
|
589
|
+
sign = u.variables.empty? ? Decide.sign(u) : RCAS.sign_of(u)
|
|
590
|
+
return RR if sign == :positive
|
|
591
|
+
# 0 may be among the values of a natural number, where log has none;
|
|
592
|
+
# a square that may be 0 is still in CC wherever log is defined
|
|
593
|
+
# (a matrix of log(x**2) could not be built: fourth review)
|
|
594
|
+
return nil if (sign.nil? || sign == :nonnegative) && arg <= NN
|
|
595
|
+
CC
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# ZZ[x], QQ[x, y]: polynomials in the given variables with coefficients
|
|
600
|
+
# in +base+. Other variables appearing in an element act as parameters and
|
|
601
|
+
# must be declared members of the base domain.
|
|
602
|
+
class PolynomialRing < Domain
|
|
603
|
+
attr_reader :base, :vars
|
|
604
|
+
|
|
605
|
+
def initialize(base, vars)
|
|
606
|
+
raise DomainError, "#{base} is not a ring" unless base.ring?
|
|
607
|
+
unless base.scalar?
|
|
608
|
+
raise DomainError, "#{base} is not a domain of numbers: polynomial coefficients are scalars"
|
|
609
|
+
end
|
|
610
|
+
names = vars.flatten.map { |v| v.is_a?(Var) ? v.name : v.to_sym }
|
|
611
|
+
raise ArgumentError, "a polynomial ring needs at least one variable" if names.empty?
|
|
612
|
+
if base.is_a?(PolynomialRing)
|
|
613
|
+
names = base.vars + names
|
|
614
|
+
base = base.base
|
|
615
|
+
end
|
|
616
|
+
@base = base
|
|
617
|
+
@vars = names.uniq.freeze
|
|
618
|
+
freeze
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def name = "#{base}[#{vars.join(', ')}]"
|
|
622
|
+
|
|
623
|
+
def include?(obj)
|
|
624
|
+
case obj
|
|
625
|
+
when Polynomial then obj.ring.subset?(self) || include?(obj.to_expr)
|
|
626
|
+
when Numeric, Symbol then base.include?(obj) || (obj.is_a?(Symbol) && vars.include?(obj))
|
|
627
|
+
when Expression
|
|
628
|
+
call(obj)
|
|
629
|
+
true
|
|
630
|
+
else false
|
|
631
|
+
end
|
|
632
|
+
rescue DomainError, ZeroDivisionError
|
|
633
|
+
false
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Convert an expression to an element of this ring.
|
|
637
|
+
def call(expr) = Polynomial.from_expr(self, expr)
|
|
638
|
+
alias poly call
|
|
639
|
+
|
|
640
|
+
def subset?(other)
|
|
641
|
+
case other
|
|
642
|
+
when PolynomialRing then base.subset?(other.base) && (vars - other.vars).empty?
|
|
643
|
+
when FractionField then subset?(other.ring)
|
|
644
|
+
else false
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def join(other)
|
|
649
|
+
case other
|
|
650
|
+
when NumberSet then PolynomialRing.new(base.join(other), vars)
|
|
651
|
+
when PolynomialRing then PolynomialRing.new(base.join(other.base), vars | other.vars)
|
|
652
|
+
when FractionField
|
|
653
|
+
# Frac(QQ[a])[x] joined with its own coefficient field stays a
|
|
654
|
+
# polynomial ring in x; only shared variables force a fraction field.
|
|
655
|
+
(other.ring.vars & vars).empty? ? PolynomialRing.new(base.join(other), vars) : other.join(self)
|
|
656
|
+
else other.join(self)
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def fraction_field = FractionField.new(self)
|
|
661
|
+
def index(var) = vars.index(var)
|
|
662
|
+
def univariate? = vars.size == 1
|
|
663
|
+
|
|
664
|
+
def zero = Polynomial.new(self, {})
|
|
665
|
+
def one = Polynomial.new(self, { Array.new(vars.size, 0) => Num.new(1) })
|
|
666
|
+
def gens = vars.map { |v| call(Var.new(v)) }
|
|
667
|
+
def gen = gens.first
|
|
668
|
+
|
|
669
|
+
def ==(other) = other.is_a?(PolynomialRing) && other.base == base && other.vars == vars
|
|
670
|
+
alias eql? ==
|
|
671
|
+
def hash = [PolynomialRing, base, vars].hash
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Frac(ZZ[x]): rational functions. Results of division, inversion and
|
|
675
|
+
# elimination over a polynomial ring live here.
|
|
676
|
+
class FractionField < Domain
|
|
677
|
+
attr_reader :ring
|
|
678
|
+
|
|
679
|
+
def initialize(ring)
|
|
680
|
+
@ring = ring
|
|
681
|
+
freeze
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def name = "Frac(#{ring})"
|
|
685
|
+
def field? = true
|
|
686
|
+
|
|
687
|
+
def include?(obj)
|
|
688
|
+
case obj
|
|
689
|
+
when Polynomial then ring.include?(obj)
|
|
690
|
+
when Numeric, Symbol then ring.include?(obj) || ring.base.fraction_field.include?(obj)
|
|
691
|
+
when Expression then rational_function?(obj)
|
|
692
|
+
else false
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def subset?(other) = other.is_a?(FractionField) && ring.subset?(other.ring)
|
|
697
|
+
|
|
698
|
+
def join(other)
|
|
699
|
+
case other
|
|
700
|
+
when FractionField then FractionField.new(ring.join(other.ring))
|
|
701
|
+
else FractionField.new(ring.join(other))
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def ==(other) = other.is_a?(FractionField) && other.ring == ring
|
|
706
|
+
alias eql? ==
|
|
707
|
+
def hash = [FractionField, ring].hash
|
|
708
|
+
|
|
709
|
+
private
|
|
710
|
+
|
|
711
|
+
def rational_function?(expr)
|
|
712
|
+
coefficients = ring.base.fraction_field
|
|
713
|
+
case expr
|
|
714
|
+
when Num then coefficients.include?(expr)
|
|
715
|
+
when Var then ring.vars.include?(expr.name) || coefficients.include?(expr)
|
|
716
|
+
when Neg, Add, Sub, Mul, Div then expr.children.all? { |c| rational_function?(c) }
|
|
717
|
+
when Pow
|
|
718
|
+
if (expr.exponent.variables & ring.vars).empty? && expr.exponent.is_a?(Num) && expr.exponent.integer?
|
|
719
|
+
rational_function?(expr.base)
|
|
720
|
+
else
|
|
721
|
+
(expr.variables & ring.vars).empty? && coefficients.include?(expr)
|
|
722
|
+
end
|
|
723
|
+
when Fn then (expr.variables & ring.vars).empty? && coefficients.include?(expr)
|
|
724
|
+
else false
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
end
|