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