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,551 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ module OpenMath
5
+ # The phrasebook: the one place where rcas and OpenMath are told about
6
+ # each other. Nothing outside this file knows an OpenMath symbol, and no
7
+ # expression class carries a to_openmath of its own - the table below is
8
+ # the whole vocabulary, in both directions.
9
+ #
10
+ # One declaration, two indexes. Most rows are symmetric and say so in a
11
+ # line; the ones where the two systems disagree carry a pair of
12
+ # procedures instead. They disagree in three ways:
13
+ #
14
+ # - decoding is many to one. arith1.root(x, 2) and arith1.power(x, 1/2)
15
+ # are both Pow, transc1.ln and transc1.log both reach rcas's log.
16
+ # - OpenMath's operators are n-ary, rcas's are binary. Decoding folds
17
+ # left, encoding writes the tree it was given: construction never
18
+ # rewrites, so a + b + c stays the tree that was typed.
19
+ # - the interesting nodes are bindings, not applications. An integral,
20
+ # a sum, a derivative and a limit all carry fns1.lambda in OpenMath
21
+ # and a bound variable in rcas.
22
+ #
23
+ # Nothing is dropped in silence. A symbol with no row decodes to a held
24
+ # Fn named "cd.name" (an unknown function, exactly as u(n + 1) is), and
25
+ # an unknown function encodes back to the symbol it came from, so a
26
+ # document rcas does not understand survives the round trip. What rcas
27
+ # cannot express at all - a one-sided limit, a bare lambda - stays as
28
+ # such an Fn rather than being bent into the nearest node.
29
+ #
30
+ # Sources: the official content dictionaries [OM19]; arith1, nums1,
31
+ # transc1, relation1, logic1, fns1, calculus1, limit1, integer1,
32
+ # combinat1, complex1, rounding1, setname1, interval1, piece1, linalg2.
33
+ # The names rcas has and OpenMath has not (gamma, zeta, erf, Ei, Si, Ci,
34
+ # li, harmonic, sign, RootOf) are in rcas's own CD, RCAS_CD.
35
+ module Phrasebook
36
+ DECODE_APPLY = {} # ["cd", "name"] => ->(args, node) { rcas object or nil }
37
+ DECODE_CONST = {} # ["cd", "name"] => rcas object, for a symbol on its own
38
+ ENCODE_CLASS = {} # Expression subclass => ->(e) { OpenMath node }
39
+ ENCODE_FN = {} # rcas function name => ["cd", "name"]
40
+ ENCODE_CONST = {} # Const name => ["cd", "name"]
41
+
42
+ module_function
43
+
44
+ # ---- how a row is declared ------------------------------------------
45
+
46
+ # An n-ary OpenMath operator and the binary rcas node it becomes.
47
+ def operator(klass, cd, name)
48
+ DECODE_APPLY[[cd, name]] = ->(args, _) { args.map { |a| decode(a) }.reduce { |l, r| klass.new(l, r) } }
49
+ ENCODE_CLASS[klass] = ->(e) { Application.new(OpenMath.sym(cd, name), encode(e.left), encode(e.right)) }
50
+ end
51
+
52
+ def unary(klass, cd, name)
53
+ DECODE_APPLY[[cd, name]] = ->(args, _) { klass.new(decode(args.first)) }
54
+ ENCODE_CLASS[klass] = ->(e) { Application.new(OpenMath.sym(cd, name), encode(e.arg)) }
55
+ end
56
+
57
+ # A named function on both sides: sin, gamma, binomial, ...
58
+ def function(fn, cd, name)
59
+ DECODE_APPLY[[cd, name]] = ->(args, _) { Fn.new(fn, args.map { |a| decode(a) }) }
60
+ ENCODE_FN[fn] = [cd, name]
61
+ end
62
+
63
+ # The same, in rcas's own content dictionary.
64
+ def rcas_function(fn, name = fn.to_s) = function(fn, RCAS_CD, name)
65
+
66
+ # A symbol standing on its own: nums1.pi, setname1.Z.
67
+ def constant(value, cd, name, encode_as: nil)
68
+ DECODE_CONST[[cd, name]] = value
69
+ ENCODE_CONST[encode_as] = [cd, name] if encode_as
70
+ end
71
+
72
+ # A row the two systems do not see the same way.
73
+ def special(cd, name, decode: nil, encode: nil, klass: nil)
74
+ DECODE_APPLY[[cd, name]] = decode if decode
75
+ ENCODE_CLASS[klass] = encode if klass && encode
76
+ end
77
+
78
+ # ---- the table -------------------------------------------------------
79
+
80
+ operator Add, "arith1", "plus"
81
+ operator Sub, "arith1", "minus"
82
+ operator Mul, "arith1", "times"
83
+ operator Div, "arith1", "divide"
84
+ operator Pow, "arith1", "power"
85
+ unary Neg, "arith1", "unary_minus"
86
+
87
+ function :abs, "arith1", "abs"
88
+ function :gcd, "arith1", "gcd"
89
+ function :lcm, "arith1", "lcm"
90
+
91
+ function :exp, "transc1", "exp"
92
+ function :log, "transc1", "ln"
93
+ function :sin, "transc1", "sin"
94
+ function :cos, "transc1", "cos"
95
+ function :tan, "transc1", "tan"
96
+ function :sinh, "transc1", "sinh"
97
+ function :cosh, "transc1", "cosh"
98
+ function :tanh, "transc1", "tanh"
99
+ function :asin, "transc1", "arcsin"
100
+ function :acos, "transc1", "arccos"
101
+ function :atan, "transc1", "arctan"
102
+
103
+ function :factorial, "integer1", "factorial"
104
+ function :quo, "integer1", "quotient"
105
+ function :rem, "integer1", "remainder"
106
+ function :binomial, "combinat1", "binomial"
107
+ function :fibonacci, "combinat1", "Fibonacci"
108
+
109
+ function :floor, "rounding1", "floor"
110
+ function :ceil, "rounding1", "ceiling"
111
+
112
+ function :re, "complex1", "real"
113
+ function :im, "complex1", "imaginary"
114
+ function :conj, "complex1", "conjugate"
115
+ function :arg, "complex1", "argument"
116
+
117
+ # No official CD names these, so they are ours.
118
+ rcas_function :gamma
119
+ rcas_function :zeta
120
+ rcas_function :erf
121
+ rcas_function :erfc
122
+ rcas_function :sign
123
+ rcas_function :harmonic
124
+ rcas_function :Ei
125
+ rcas_function :Si
126
+ rcas_function :Ci
127
+ rcas_function :li
128
+
129
+ constant PI, "nums1", "pi", encode_as: :pi
130
+ constant OO, "nums1", "infinity", encode_as: :oo
131
+ constant E, "nums1", "e"
132
+ constant I, "nums1", "i"
133
+ # nums1.NaN is what an undefined value is called there; it decodes to
134
+ # rcas's own UNDEFINED, which is the node the arithmetic produces.
135
+ constant UNDEFINED, "nums1", "NaN", encode_as: :undefined
136
+
137
+ constant NN, "setname1", "N"
138
+ constant ZZ, "setname1", "Z"
139
+ constant QQ, "setname1", "Q"
140
+ constant RR, "setname1", "R"
141
+ constant CC, "setname1", "C"
142
+
143
+ # nums1.rational and complex1.complex_cartesian build a number rather
144
+ # than an operation; a Num goes back through #number, which picks the
145
+ # form the value calls for.
146
+ special "nums1", "rational", decode: lambda { |args, _|
147
+ p, q = args
148
+ return nil unless p.is_a?(Int) && q.is_a?(Int)
149
+ Num.new(Rational(p.value, q.value))
150
+ }
151
+
152
+ special "complex1", "complex_cartesian", decode: lambda { |args, _|
153
+ re, im = args.map { |a| decode(a) }
154
+ return Num.new(Complex(re.value, im.value)) if re.is_a?(Num) && im.is_a?(Num)
155
+ Add.new(re, Mul.new(im, I))
156
+ }
157
+
158
+ # transc1.log is the logarithm to a base, which rcas writes as a
159
+ # quotient of natural logarithms.
160
+ special "transc1", "log", decode: lambda { |args, _|
161
+ base, x = args.map { |a| decode(a) }
162
+ Div.new(Fn.new(:log, [x]), Fn.new(:log, [base]))
163
+ }
164
+
165
+ # arith1.root(x, n) is x**(1/n).
166
+ special "arith1", "root", decode: lambda { |args, _|
167
+ x, n = args
168
+ exponent = n.is_a?(Int) ? Num.new(Rational(1, n.value)) : Div.new(Num.new(1), decode(n))
169
+ Pow.new(decode(x), exponent)
170
+ }
171
+
172
+ # sum and product: a range and a lambda in OpenMath, a bound variable
173
+ # in rcas.
174
+ special "arith1", "sum", klass: Sum,
175
+ decode: ->(args, _) { range_binding(args) { |term, var, from, to| Sum.new(term, var, from, to) } },
176
+ encode: ->(e) { range_application("sum", e.from, e.to, e.var, e.term) }
177
+
178
+ special "arith1", "product", klass: Product,
179
+ decode: ->(args, _) { range_binding(args) { |term, var, from, to| Product.new(term, var, from, to) } },
180
+ encode: ->(e) { range_application("product", e.from, e.to, e.var, e.term) }
181
+
182
+ # calculus1: int and defint are one symbol in rcas, whose Integral
183
+ # node carries the bounds when it has them.
184
+ special "calculus1", "int", klass: Integral,
185
+ decode: lambda { |args, _|
186
+ pair = lambda_parts(args.first)
187
+ return nil unless pair
188
+ var, body = pair
189
+ Integral.new(body, var)
190
+ },
191
+ encode: lambda { |e|
192
+ return Application.new(OpenMath.sym("calculus1", "int"), lambda_of(e.var, e.integrand)) unless e.definite?
193
+ Application.new(OpenMath.sym("calculus1", "defint"),
194
+ Application.new(OpenMath.sym("interval1", "interval"), encode(e.from), encode(e.to)),
195
+ lambda_of(e.var, e.integrand))
196
+ }
197
+
198
+ special "calculus1", "defint", decode: lambda { |args, _|
199
+ range, fn = args
200
+ pair = interval_bounds(range)
201
+ return nil unless pair
202
+ from, to = pair
203
+ pair = lambda_parts(fn)
204
+ return nil unless pair
205
+ var, body = pair
206
+ Integral.new(body, var, from, to)
207
+ }
208
+
209
+ special "calculus1", "diff", klass: Derivative,
210
+ decode: lambda { |args, _|
211
+ pair = lambda_parts(args.first)
212
+ return nil unless pair
213
+ var, body = pair
214
+ Derivative.new(body, var)
215
+ },
216
+ encode: lambda { |e|
217
+ return Application.new(OpenMath.sym("calculus1", "diff"), lambda_of(e.var, e.expr)) if e.order == 1
218
+ Application.new(OpenMath.sym("calculus1", "nthdiff"), Int.new(e.order), lambda_of(e.var, e.expr))
219
+ }
220
+
221
+ special "calculus1", "nthdiff", decode: lambda { |args, _|
222
+ order, fn = args
223
+ return nil unless order.is_a?(Int)
224
+ pair = lambda_parts(fn)
225
+ return nil unless pair
226
+ var, body = pair
227
+ Derivative.new(body, var, order.value)
228
+ }
229
+
230
+ # limit1.limit(x0, direction, lambda). rcas's Limit node has no
231
+ # direction, so only the two-sided limits become one; above and below
232
+ # stay as they came, held.
233
+ special "limit1", "limit", klass: Limit,
234
+ decode: lambda { |args, _|
235
+ point, direction, fn = args
236
+ return nil unless direction.is_a?(ContentSymbol) && %w[both_sides null].include?(direction.name)
237
+ pair = lambda_parts(fn)
238
+ return nil unless pair
239
+ var, body = pair
240
+ Limit.new(body, var, decode(point))
241
+ },
242
+ encode: lambda { |e|
243
+ Application.new(OpenMath.sym("limit1", "limit"), encode(e.point),
244
+ OpenMath.sym("limit1", "both_sides"), lambda_of(e.var, e.expr))
245
+ }
246
+
247
+ # piece1.piece is (value, condition) - that way round.
248
+ special "piece1", "piecewise", klass: Piecewise,
249
+ decode: lambda { |args, _|
250
+ branches = args.map { |a| piece_branch(a) }
251
+ return nil if branches.any?(&:nil?)
252
+ Piecewise.new(branches)
253
+ },
254
+ encode: lambda { |e|
255
+ pieces = e.branches.map do |condition, value|
256
+ if condition == Piecewise::OTHERWISE
257
+ Application.new(OpenMath.sym("piece1", "otherwise"), encode(value))
258
+ else
259
+ Application.new(OpenMath.sym("piece1", "piece"), encode(value), encode(condition))
260
+ end
261
+ end
262
+ Application.new(OpenMath.sym("piece1", "piecewise"), pieces)
263
+ }
264
+
265
+ # RootOf: the algebraic number that is the index-th root of a
266
+ # polynomial. No official CD names it.
267
+ special RCAS_CD, "root_of", klass: RootOf,
268
+ decode: lambda { |args, _|
269
+ poly, index = args
270
+ return nil unless index.is_a?(Int)
271
+ begin
272
+ RootOf.new(decode(poly).to_poly, index.value)
273
+ rescue StandardError => rescued
274
+ RCAS.guard!(rescued)
275
+ nil
276
+ end
277
+ },
278
+ encode: ->(e) { Application.new(OpenMath.rcas_sym("root_of"), encode(e.poly.to_expr), Int.new(e.index)) }
279
+
280
+ # The intervals, on their own rather than as the range of a sum.
281
+ { "interval_oo" => [true, true], "interval_cc" => [false, false],
282
+ "interval_oc" => [true, false], "interval_co" => [false, true],
283
+ "interval" => [false, false], "integer_interval" => [false, false] }.each do |name, (left, right)|
284
+ DECODE_APPLY[["interval1", name]] = lambda { |args, _|
285
+ low, high = args.map { |a| decode(a) }
286
+ Interval.new(low, high, left_open: left, right_open: right)
287
+ }
288
+ end
289
+
290
+ # The leaves and the two nodes that are not operations.
291
+ ENCODE_CLASS[Num] = ->(e) { number(e.value) }
292
+ ENCODE_CLASS[Var] = ->(e) { name_head(e.name) }
293
+ ENCODE_CLASS[Const] = lambda { |e|
294
+ cd, name = ENCODE_CONST[e.name]
295
+ raise EncodeError, "no OpenMath symbol for the constant #{e}" unless cd
296
+ OpenMath.sym(cd, name)
297
+ }
298
+ ENCODE_CLASS[Fn] = lambda { |e|
299
+ return Error.new(encode(e.args.first), e.args[1..].map { |a| encode(a) }) if e.name == :openmath_error
300
+ return lambda_of(e.args.first, e.args.last) if e.name == :lambda && e.args.size == 2 && e.args.first.is_a?(Var)
301
+ cd, name = ENCODE_FN[e.name]
302
+ head = cd ? OpenMath.sym(cd, name, cdbase: (RCAS_CDBASE if cd == RCAS_CD)) : name_head(e.name)
303
+ Application.new(head, e.args.map { |a| encode(a) })
304
+ }
305
+
306
+ # ---- encoding --------------------------------------------------------
307
+
308
+ # The OpenMath object for an rcas object, wrapped in its OMOBJ.
309
+ def to_openmath(obj) = Root.new(encode(obj))
310
+
311
+ def encode(obj)
312
+ case obj
313
+ when Node then obj
314
+ when Expression then encode_expression(obj)
315
+ when Equation then Application.new(OpenMath.sym("relation1", "eq"), encode(obj.lhs), encode(obj.rhs))
316
+ when Membership then Application.new(OpenMath.sym("set1", "in"), encode(obj.value), encode(obj.domain))
317
+ when ImageSet then encode_image_set(obj)
318
+ when Inequality then encode_inequality(obj)
319
+ when Interval then encode_interval(obj)
320
+ when NumberSet then encode_number_set(obj)
321
+ when Matrix then encode_matrix(obj)
322
+ when Vector then Application.new(OpenMath.sym("linalg2", "vector"), obj.entries.map { |v| encode(v) })
323
+ when Numeric then number(obj)
324
+ when Symbol then name_head(obj)
325
+ when Array then Application.new(OpenMath.sym("list1", "list"), obj.map { |v| encode(v) })
326
+ else raise EncodeError, "no OpenMath encoding for #{obj.class}"
327
+ end
328
+ end
329
+
330
+ def encode_expression(e)
331
+ rule = ENCODE_CLASS[e.class]
332
+ raise EncodeError, "no OpenMath symbol for #{e.class} (#{e})" unless rule
333
+ rule.call(e)
334
+ end
335
+
336
+ RELATIONS = { :< => "lt", :<= => "leq", :> => "gt", :>= => "geq", :!= => "neq" }.freeze
337
+
338
+ def encode_inequality(ineq)
339
+ name = RELATIONS.fetch(ineq.op) { raise EncodeError, "no OpenMath symbol for #{ineq.op}" }
340
+ Application.new(OpenMath.sym("relation1", name), encode(ineq.lhs), encode(ineq.rhs))
341
+ end
342
+
343
+ def encode_interval(interval)
344
+ name = "interval_#{interval.left_open ? 'o' : 'c'}#{interval.right_open ? 'o' : 'c'}"
345
+ Application.new(OpenMath.sym("interval1", name), encode(interval.low), encode(interval.high))
346
+ end
347
+
348
+ SET_NAMES = { NN: "N", ZZ: "Z", QQ: "Q", RR: "R", CC: "C" }.freeze
349
+
350
+ def encode_number_set(set)
351
+ name = SET_NAMES.fetch(set.name.to_sym) { raise EncodeError, "no OpenMath symbol for #{set}" }
352
+ OpenMath.sym("setname1", name)
353
+ end
354
+
355
+ def encode_matrix(matrix)
356
+ rows = matrix.entries.map do |row|
357
+ Application.new(OpenMath.sym("linalg2", "matrixrow"), row.map { |v| encode(v) })
358
+ end
359
+ Application.new(OpenMath.sym("linalg2", "matrix"), rows)
360
+ end
361
+
362
+ # An indeterminate is an OMV; a name carrying a content dictionary
363
+ # ("arith3.foo", the way an unmapped symbol decodes) goes back to the
364
+ # symbol it came from.
365
+ def name_head(name)
366
+ text = name.to_s
367
+ return Variable.new(text) unless text.include?(".")
368
+ cd, symbol = text.split(".", 2)
369
+ OpenMath.sym(cd, symbol, cdbase: (RCAS_CDBASE if cd == RCAS_CD))
370
+ end
371
+
372
+ # Ruby numbers. Integers and floats are objects of their own in
373
+ # OpenMath; a rational is an application, and so is a complex number
374
+ # unless it is exactly i.
375
+ def number(value)
376
+ case value
377
+ when Integer then Int.new(value)
378
+ when Rational then Application.new(OpenMath.sym("nums1", "rational"), Int.new(value.numerator), Int.new(value.denominator))
379
+ when Float then Double.new(value)
380
+ when Complex then complex_number(value)
381
+ else
382
+ raise EncodeError, "no OpenMath encoding for #{value.class}" unless value.respond_to?(:to_r)
383
+ number(value.to_r)
384
+ end
385
+ end
386
+
387
+ def complex_number(value)
388
+ return OpenMath.sym("nums1", "i") if value.real.zero? && value.imaginary == 1
389
+ Application.new(OpenMath.sym("complex1", "complex_cartesian"), number(value.real), number(value.imaginary))
390
+ end
391
+
392
+ def range_application(name, from, to, var, term)
393
+ Application.new(OpenMath.sym("arith1", name),
394
+ Application.new(OpenMath.sym("interval1", "integer_interval"), encode(from), encode(to)),
395
+ lambda_of(var, term))
396
+ end
397
+
398
+ def lambda_of(var, body)
399
+ Bind.new(OpenMath.sym("fns1", "lambda"), BVar.new(Variable.new(var.name)), encode(body))
400
+ end
401
+
402
+ # ---- decoding --------------------------------------------------------
403
+
404
+ # The rcas object an OpenMath object stands for. Not always an
405
+ # Expression: relation1.eq is an Equation, interval1.interval_cc an
406
+ # Interval, setname1.Z a number set.
407
+ def to_expression(node)
408
+ node = OpenMath.dereference(node)
409
+ decode(node.is_a?(Root) ? node.object : node)
410
+ end
411
+
412
+ def decode(node)
413
+ case node
414
+ when Root then decode(node.object)
415
+ when Int, Double then Num.new(node.value)
416
+ when Variable then Var.new(node.name.to_sym)
417
+ when ContentSymbol then DECODE_CONST.fetch(node.key) { Var.new(:"#{node.cd}.#{node.name}") }
418
+ when Application then decode_application(node)
419
+ when Bind then decode_binding(node)
420
+ when Attribution then decode(node.object)
421
+ when Error then Fn.new(:openmath_error, [decode(node.symbol), *node.args.map { |a| decode(a) }])
422
+ when Reference then raise ParseError, "reference #{node.href} points nowhere"
423
+ when Text then raise ParseError, "no rcas expression for the string #{node.value.inspect}"
424
+ when Bytes then raise ParseError, "no rcas expression for a byte array"
425
+ else raise ParseError, "can't decode #{node.class}"
426
+ end
427
+ end
428
+
429
+ def decode_application(node)
430
+ head = node.head
431
+ if head.is_a?(ContentSymbol) && (rule = DECODE_APPLY[head.key])
432
+ result = rule.call(node.args, node)
433
+ return result if result
434
+ end
435
+ Fn.new(unmapped_name(head), node.args.map { |a| decode(a) })
436
+ end
437
+
438
+ # A binding rcas has no node for stays a held call, so re-encoding
439
+ # gives back what arrived.
440
+ def decode_binding(node)
441
+ var, body = lambda_parts(node)
442
+ return Fn.new(:lambda, [var, body]) if var
443
+ Fn.new(unmapped_name(node.binder), [*node.vars.map { |v| decode(v) }, decode(node.body)])
444
+ end
445
+
446
+ def unmapped_name(head)
447
+ case head
448
+ when ContentSymbol then :"#{head.cd}.#{head.name}"
449
+ when Variable then head.name.to_sym
450
+ else raise ParseError, "can't apply #{head}"
451
+ end
452
+ end
453
+
454
+ # fns1.lambda[x -> body] as [Var, expression]; nil for anything else.
455
+ def lambda_parts(node)
456
+ return nil unless node.is_a?(Bind) && node.binder.is_a?(ContentSymbol) && node.binder.key == %w[fns1 lambda]
457
+ return nil unless node.vars.size == 1 && node.vars.first.is_a?(Variable)
458
+ [Var.new(node.vars.first.name.to_sym), decode(node.body)]
459
+ end
460
+
461
+ def interval_bounds(node)
462
+ return nil unless node.is_a?(Application) && node.head.is_a?(ContentSymbol)
463
+ return nil unless node.head.cd == "interval1" && node.args.size == 2
464
+ node.args.map { |a| decode(a) }
465
+ end
466
+
467
+ # The shape shared by arith1.sum and arith1.product.
468
+ def range_binding(args)
469
+ range, fn = args
470
+ pair = interval_bounds(range)
471
+ return nil unless pair
472
+ from, to = pair
473
+ pair = lambda_parts(fn)
474
+ return nil unless pair
475
+ var, term = pair
476
+ yield term, var, from, to
477
+ end
478
+
479
+ def piece_branch(node)
480
+ return nil unless node.is_a?(Application) && node.head.is_a?(ContentSymbol)
481
+ case node.head.key
482
+ when %w[piece1 piece]
483
+ value, condition = node.args
484
+ return nil unless value && condition
485
+ decoded = piece_condition(condition)
486
+ decoded ? [decoded, decode(value)] : nil
487
+ when %w[piece1 otherwise]
488
+ [Piecewise::OTHERWISE, decode(node.args.first)]
489
+ end
490
+ end
491
+
492
+ def piece_condition(node)
493
+ return Piecewise::OTHERWISE if node.is_a?(ContentSymbol) && node.key == %w[logic1 true]
494
+ condition = decode_relation(node)
495
+ condition if condition.is_a?(Inequality) || condition.is_a?(Equation)
496
+ end
497
+
498
+ DECODE_RELATIONS = { "lt" => :<, "leq" => :<=, "gt" => :>, "geq" => :>=, "neq" => :!= }.freeze
499
+
500
+ def decode_relation(node)
501
+ return nil unless node.is_a?(Application) && node.head.is_a?(ContentSymbol) && node.head.cd == "relation1"
502
+ lhs, rhs = node.args.map { |a| decode(a) }
503
+ return Equation.new(lhs, rhs) if node.head.name == "eq"
504
+ op = DECODE_RELATIONS[node.head.name] or return nil
505
+ Inequality.new(lhs, op, rhs)
506
+ end
507
+
508
+ # relation1 builds an Equation or an Inequality, neither of which is
509
+ # an Expression, so it cannot go through the ordinary table.
510
+ %w[eq lt leq gt geq neq].each do |name|
511
+ DECODE_APPLY[["relation1", name]] = ->(args, node) { decode_relation(node) }
512
+ end
513
+
514
+ # {2*pi*k | k in ZZ} is the image of a number set under a function,
515
+ # which is what set1.map says: map(lambda k . 2*pi*k, setname1.Z).
516
+ def encode_image_set(set)
517
+ unless set.parameters.size == 1
518
+ raise EncodeError, "no OpenMath encoding for a family with #{set.parameters.size} parameters"
519
+ end
520
+ Application.new(OpenMath.sym("set1", "map"), lambda_of(set.parameters.first, set.expr), encode(set.domain))
521
+ end
522
+
523
+ DECODE_APPLY[%w[set1 map]] = lambda { |args, _|
524
+ next nil unless args.size == 2
525
+ pair = lambda_parts(args.first)
526
+ domain = decode(args.last)
527
+ pair && domain.is_a?(NumberSet) ? ImageSet.new(pair.last, [pair.first], domain) : nil
528
+ }
529
+
530
+ # set1.in is a Membership, which is a statement and not an Expression
531
+ # either. The set comes back as the number set it names.
532
+ DECODE_APPLY[%w[set1 in]] = lambda { |args, _|
533
+ next nil unless args.size == 2
534
+ value, domain = args.map { |a| decode(a) }
535
+ domain.is_a?(Domain) ? Membership.new(value, domain) : nil
536
+ }
537
+
538
+ # linalg2: a matrix of matrixrows, and a vector.
539
+ DECODE_APPLY[%w[linalg2 matrix]] = lambda { |args, _|
540
+ rows = args.map do |row|
541
+ return nil unless row.is_a?(Application) && row.head.is_a?(ContentSymbol) && row.head.key == %w[linalg2 matrixrow]
542
+ row.args.map { |v| decode(v) }
543
+ end
544
+ RCAS.matrix(rows)
545
+ }
546
+
547
+ DECODE_APPLY[%w[linalg2 vector]] = ->(args, _) { RCAS.vector(args.map { |v| decode(v) }) }
548
+ DECODE_APPLY[%w[list1 list]] = ->(args, _) { args.map { |v| decode(v) } }
549
+ end
550
+ end
551
+ end