rcas 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module RCAS
|
|
6
|
+
module OpenMath
|
|
7
|
+
# The XML encoding of OpenMath objects [OM19, ch. 4]: one of the
|
|
8
|
+
# encodings, not the objects themselves.
|
|
9
|
+
#
|
|
10
|
+
# RCAS::OpenMath::XML.encode(om) # => "<OMOBJ ...>"
|
|
11
|
+
# RCAS::OpenMath::XML.decode(string) # => an OpenMath::Root
|
|
12
|
+
#
|
|
13
|
+
# The reader is written out here rather than taken from REXML, which is
|
|
14
|
+
# a bundled gem rather than the standard library; the encoding is a
|
|
15
|
+
# small, attribute-light subset of XML and needs no more than this.
|
|
16
|
+
# Namespace prefixes are accepted and ignored (every element of the
|
|
17
|
+
# encoding is in the OpenMath namespace).
|
|
18
|
+
module XML
|
|
19
|
+
NAMESPACE = "http://www.openmath.org/OpenMath"
|
|
20
|
+
|
|
21
|
+
ESCAPES = { "&" => "&", "<" => "<", ">" => ">", '"' => """, "'" => "'" }.freeze
|
|
22
|
+
ENTITIES = { "amp" => "&", "lt" => "<", "gt" => ">", "quot" => '"', "apos" => "'" }.freeze
|
|
23
|
+
|
|
24
|
+
Element = Struct.new(:name, :attrs, :children, :text)
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# +indent+: a number of spaces per level, or nil for a single line.
|
|
29
|
+
# +namespace+: write the xmlns attribute on the outermost element.
|
|
30
|
+
def encode(node, indent: nil, namespace: true, declaration: false)
|
|
31
|
+
node = Root.new(node) unless node.is_a?(Root)
|
|
32
|
+
out = +""
|
|
33
|
+
out << %(<?xml version="1.0" encoding="UTF-8"?>\n) if declaration
|
|
34
|
+
write(node, out, 0, indent, namespace)
|
|
35
|
+
out
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write(node, out, depth, indent, namespace)
|
|
39
|
+
pad = indent ? " " * (depth * indent) : ""
|
|
40
|
+
nl = indent ? "\n" : ""
|
|
41
|
+
tag, attrs, text, children = parts(node)
|
|
42
|
+
attrs = { "xmlns" => NAMESPACE }.merge(attrs) if namespace && depth.zero?
|
|
43
|
+
attrs = attrs.merge("id" => node.id) if node.id
|
|
44
|
+
open = "#{pad}<#{tag}#{attributes(attrs)}"
|
|
45
|
+
|
|
46
|
+
if node.is_a?(Foreign)
|
|
47
|
+
out << open << ">" << node.content << "</#{tag}>" << nl
|
|
48
|
+
elsif text.nil? && children.empty?
|
|
49
|
+
out << open << "/>" << nl
|
|
50
|
+
elsif children.empty?
|
|
51
|
+
out << open << ">" << escape(text) << "</#{tag}>" << nl
|
|
52
|
+
else
|
|
53
|
+
out << open << ">" << nl
|
|
54
|
+
children.each { |c| write(c, out, depth + 1, indent, false) }
|
|
55
|
+
out << pad << "</#{tag}>" << nl
|
|
56
|
+
end
|
|
57
|
+
out
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# tag, attributes, text content (nil for none), child nodes.
|
|
61
|
+
def parts(node)
|
|
62
|
+
case node
|
|
63
|
+
when Root then ["OMOBJ", { "version" => node.version }, nil, [node.object]]
|
|
64
|
+
when Int then ["OMI", {}, node.value.to_s, []]
|
|
65
|
+
when Double then ["OMF", { "dec" => float_text(node.value) }, nil, []]
|
|
66
|
+
when Text then ["OMSTR", {}, node.value, []]
|
|
67
|
+
when Bytes then ["OMB", {}, [node.value].pack("m0"), []]
|
|
68
|
+
when Variable then ["OMV", { "name" => node.name }, nil, []]
|
|
69
|
+
when ContentSymbol then ["OMS", symbol_attributes(node), nil, []]
|
|
70
|
+
when Application then ["OMA", {}, nil, node.children]
|
|
71
|
+
when BVar then ["OMBVAR", {}, nil, node.vars]
|
|
72
|
+
when Bind then ["OMBIND", {}, nil, node.children]
|
|
73
|
+
when Attribution then ["OMATTR", {}, nil, [AttrList.new(node.pairs), node.object]]
|
|
74
|
+
when AttrList then ["OMATP", {}, nil, node.pairs.flat_map(&:children)]
|
|
75
|
+
when Error then ["OME", {}, nil, node.children]
|
|
76
|
+
when Reference then ["OMR", { "href" => node.href }, nil, []]
|
|
77
|
+
when Foreign then ["OMFOREIGN", node.encoding ? { "encoding" => node.encoding } : {}, node.content, []]
|
|
78
|
+
else raise EncodeError, "no XML encoding for #{node.class}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The OMATP element: a flat list of key/value pairs inside an OMATTR.
|
|
83
|
+
# It is not an object of its own in the standard, so it is not one of
|
|
84
|
+
# the classes in objects.rb - it exists only while encoding.
|
|
85
|
+
class AttrList < Node
|
|
86
|
+
attr_reader :pairs
|
|
87
|
+
|
|
88
|
+
def initialize(pairs)
|
|
89
|
+
@pairs = pairs
|
|
90
|
+
freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def children = pairs
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def symbol_attributes(node)
|
|
97
|
+
attrs = {}
|
|
98
|
+
attrs["cdbase"] = node.cdbase if node.cdbase
|
|
99
|
+
attrs["cd"] = node.cd
|
|
100
|
+
attrs["name"] = node.name
|
|
101
|
+
attrs
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def attributes(attrs)
|
|
105
|
+
attrs.map { |k, v| %( #{k}="#{escape(v.to_s)}") }.join
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def float_text(value)
|
|
109
|
+
return "INF" if value == Float::INFINITY
|
|
110
|
+
return "-INF" if value == -Float::INFINITY
|
|
111
|
+
return "NaN" if value.nan?
|
|
112
|
+
value.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def escape(string) = string.to_s.gsub(/[&<>"']/) { |c| ESCAPES[c] }
|
|
116
|
+
|
|
117
|
+
def unescape(string)
|
|
118
|
+
string.to_s.gsub(/&(#x?\h+|\w+);/) do
|
|
119
|
+
entity = Regexp.last_match(1)
|
|
120
|
+
case entity
|
|
121
|
+
when /\A#x(\h+)\z/ then Regexp.last_match(1).to_i(16).chr(Encoding::UTF_8)
|
|
122
|
+
when /\A#(\d+)\z/ then Regexp.last_match(1).to_i.chr(Encoding::UTF_8)
|
|
123
|
+
else ENTITIES.fetch(entity) { raise ParseError, "unknown entity &#{entity};" }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ---- reading ----------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
def decode(source)
|
|
131
|
+
scanner = StringScanner.new(source.to_s)
|
|
132
|
+
element = read_element(scanner)
|
|
133
|
+
raise ParseError, "no XML element found" unless element
|
|
134
|
+
build(element)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def skip_junk(scanner)
|
|
138
|
+
loop do
|
|
139
|
+
scanner.skip(/\s+/)
|
|
140
|
+
break unless scanner.scan(/<\?.*?\?>/m) || scanner.scan(/<!--.*?-->/m) || scanner.scan(/<!DOCTYPE[^>]*>/m)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def read_element(scanner)
|
|
145
|
+
skip_junk(scanner)
|
|
146
|
+
return nil unless scanner.scan(/</)
|
|
147
|
+
name = scanner.scan(%r{[^\s/>]+}) or raise ParseError, "expected an element name"
|
|
148
|
+
attrs = read_attributes(scanner)
|
|
149
|
+
return Element.new(local(name), attrs, [], "") if scanner.scan(%r{\s*/>})
|
|
150
|
+
scanner.scan(/\s*>/) or raise ParseError, "unterminated <#{name}>"
|
|
151
|
+
return read_foreign(scanner, local(name), attrs) if local(name) == "OMFOREIGN"
|
|
152
|
+
read_content(scanner, local(name), attrs)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def read_attributes(scanner)
|
|
156
|
+
attrs = {}
|
|
157
|
+
loop do
|
|
158
|
+
scanner.skip(/\s+/)
|
|
159
|
+
break if scanner.check(%r{/?>})
|
|
160
|
+
key = scanner.scan(%r{[^\s=/>]+}) or raise ParseError, "expected an attribute name"
|
|
161
|
+
scanner.skip(/\s*=\s*/) or raise ParseError, "expected = after #{key}"
|
|
162
|
+
quote = scanner.scan(/["']/) or raise ParseError, "expected a quoted value for #{key}"
|
|
163
|
+
value = scanner.scan(/[^#{Regexp.escape(quote)}]*/)
|
|
164
|
+
scanner.skip(/#{Regexp.escape(quote)}/)
|
|
165
|
+
attrs[local(key)] = unescape(value)
|
|
166
|
+
end
|
|
167
|
+
attrs
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# OMFOREIGN holds data that is not OpenMath - a presentation-MathML or
|
|
171
|
+
# LaTeX rendering, an image, a comment - so its content is taken
|
|
172
|
+
# exactly as it stands, markup and all, and never parsed.
|
|
173
|
+
OPEN_FOREIGN = %r{<\s*(?:[\w.-]+:)?OMFOREIGN\b[^>]*>}.freeze
|
|
174
|
+
CLOSE_FOREIGN = %r{</\s*(?:[\w.-]+:)?OMFOREIGN\s*>}.freeze
|
|
175
|
+
|
|
176
|
+
def read_foreign(scanner, name, attrs)
|
|
177
|
+
raw = +""
|
|
178
|
+
depth = 1
|
|
179
|
+
until scanner.eos?
|
|
180
|
+
chunk = scanner.scan(/[^<]+/)
|
|
181
|
+
raw << chunk if chunk
|
|
182
|
+
break if scanner.eos?
|
|
183
|
+
|
|
184
|
+
if (close = scanner.scan(CLOSE_FOREIGN))
|
|
185
|
+
depth -= 1
|
|
186
|
+
break if depth.zero?
|
|
187
|
+
raw << close
|
|
188
|
+
elsif (open_tag = scanner.scan(OPEN_FOREIGN))
|
|
189
|
+
depth += 1
|
|
190
|
+
raw << open_tag
|
|
191
|
+
else
|
|
192
|
+
raw << scanner.scan(/</)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
raise ParseError, "unclosed <OMFOREIGN>" unless depth.zero?
|
|
196
|
+
Element.new(name, attrs, [], raw)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def read_content(scanner, name, attrs)
|
|
200
|
+
children = []
|
|
201
|
+
text = +""
|
|
202
|
+
loop do
|
|
203
|
+
raise ParseError, "unclosed <#{name}>" if scanner.eos?
|
|
204
|
+
|
|
205
|
+
if scanner.check(%r{</})
|
|
206
|
+
scanner.scan(%r{</[^>]*>}) or raise ParseError, "unterminated closing tag"
|
|
207
|
+
break
|
|
208
|
+
elsif scanner.check(/<!--/)
|
|
209
|
+
scanner.scan(/<!--.*?-->/m)
|
|
210
|
+
elsif scanner.check(/</)
|
|
211
|
+
children << read_element(scanner)
|
|
212
|
+
else
|
|
213
|
+
text << unescape(scanner.scan(/[^<]+/))
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
Element.new(name, attrs, children, text)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Namespace prefixes carry no information here: every element of the
|
|
220
|
+
# encoding lives in the OpenMath namespace.
|
|
221
|
+
def local(name) = name.split(":").last
|
|
222
|
+
|
|
223
|
+
def build(element)
|
|
224
|
+
node = case element.name
|
|
225
|
+
when "OMOBJ" then Root.new(build(only_child(element)), version: element.attrs.fetch("version", OBJECT_VERSION))
|
|
226
|
+
when "OMI" then Int.new(integer_value(element.text))
|
|
227
|
+
when "OMF" then Double.new(float_value(element.attrs))
|
|
228
|
+
when "OMSTR" then Text.new(element.text)
|
|
229
|
+
when "OMB" then Bytes.new(element.text.unpack1("m") || "")
|
|
230
|
+
when "OMV" then Variable.new(attribute(element, "name"))
|
|
231
|
+
when "OMS" then ContentSymbol.new(attribute(element, "cd"), attribute(element, "name"), cdbase: element.attrs["cdbase"])
|
|
232
|
+
when "OMA" then build_application(element)
|
|
233
|
+
when "OMBVAR" then BVar.new(element.children.map { |c| build(c) })
|
|
234
|
+
when "OMBIND" then build_binding(element)
|
|
235
|
+
when "OMATTR" then build_attribution(element)
|
|
236
|
+
when "OME" then build_error(element)
|
|
237
|
+
when "OMR" then Reference.new(attribute(element, "href"))
|
|
238
|
+
when "OMFOREIGN" then Foreign.new(element.text, encoding: element.attrs["encoding"])
|
|
239
|
+
else raise ParseError, "unknown OpenMath element <#{element.name}>"
|
|
240
|
+
end
|
|
241
|
+
id = element.attrs["id"] || element.attrs["xml:id"]
|
|
242
|
+
id ? node.identified(id) : node
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def build_application(element)
|
|
246
|
+
children = element.children.map { |c| build(c) }
|
|
247
|
+
raise ParseError, "<OMA> needs at least a head" if children.empty?
|
|
248
|
+
Application.new(children.first, children[1..])
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_binding(element)
|
|
252
|
+
children = element.children
|
|
253
|
+
unless children.size == 3 && children[1].name == "OMBVAR"
|
|
254
|
+
raise ParseError, "<OMBIND> needs a binder, an <OMBVAR> and a body"
|
|
255
|
+
end
|
|
256
|
+
Bind.new(build(children[0]), build(children[1]), build(children[2]))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_attribution(element)
|
|
260
|
+
pairs_element, object = element.children
|
|
261
|
+
raise ParseError, "<OMATTR> needs an <OMATP> and an object" unless pairs_element&.name == "OMATP" && object
|
|
262
|
+
flat = pairs_element.children.map { |c| build(c) }
|
|
263
|
+
raise ParseError, "<OMATP> needs an even number of children" unless flat.size.even?
|
|
264
|
+
Attribution.new(flat.each_slice(2).map { |k, v| AttrPair.new(k, v) }, build(object))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def build_error(element)
|
|
268
|
+
children = element.children.map { |c| build(c) }
|
|
269
|
+
raise ParseError, "<OME> needs a symbol" if children.empty?
|
|
270
|
+
Error.new(children.first, children[1..])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def only_child(element)
|
|
274
|
+
children = element.children
|
|
275
|
+
raise ParseError, "<#{element.name}> needs exactly one child" unless children.size == 1
|
|
276
|
+
children.first
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def attribute(element, name)
|
|
280
|
+
element.attrs[name] or raise ParseError, "<#{element.name}> needs a #{name} attribute"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# OMI is decimal, or hexadecimal written x1F / -x1F.
|
|
284
|
+
def integer_value(text)
|
|
285
|
+
stripped = text.to_s.strip
|
|
286
|
+
case stripped
|
|
287
|
+
when /\A(-?)x(\h+)\z/ then (Regexp.last_match(1) == "-" ? -1 : 1) * Regexp.last_match(2).to_i(16)
|
|
288
|
+
when /\A-?\d+\z/ then stripped.to_i
|
|
289
|
+
else raise ParseError, "not an integer: #{text.inspect}"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def float_value(attrs)
|
|
294
|
+
if (dec = attrs["dec"])
|
|
295
|
+
case dec
|
|
296
|
+
when "INF" then Float::INFINITY
|
|
297
|
+
when "-INF" then -Float::INFINITY
|
|
298
|
+
when "NaN" then Float::NAN
|
|
299
|
+
else Float(dec)
|
|
300
|
+
end
|
|
301
|
+
elsif (hex = attrs["hex"])
|
|
302
|
+
[hex].pack("H*").unpack1("G")
|
|
303
|
+
else
|
|
304
|
+
raise ParseError, "<OMF> needs a dec or hex attribute"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openmath/objects"
|
|
4
|
+
require_relative "openmath/xml"
|
|
5
|
+
require_relative "openmath/phrasebook"
|
|
6
|
+
require_relative "openmath/popcorn"
|
|
7
|
+
|
|
8
|
+
module RCAS
|
|
9
|
+
module OpenMath
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# An OpenMath object from its XML encoding, or from an object already
|
|
13
|
+
# parsed. Use #to_expression on the result for the rcas object.
|
|
14
|
+
def parse(source)
|
|
15
|
+
source.is_a?(Node) ? source : XML.decode(source)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The rcas object a piece of OpenMath stands for, unevaluated: the
|
|
19
|
+
# document says 1 + 2, and 1 + 2 is what comes back, not 3.
|
|
20
|
+
def read(source) = parse(source).to_expression
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Expression
|
|
24
|
+
# This expression as an OpenMath object; #to_xml writes the encoding.
|
|
25
|
+
def to_openmath = OpenMath::Phrasebook.to_openmath(self)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Equation
|
|
29
|
+
def to_openmath = OpenMath::Phrasebook.to_openmath(self)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module Functions
|
|
33
|
+
# openmath(x**2 + 1): the OpenMath object, whose to_xml is the XML
|
|
34
|
+
# encoding. Equations, intervals, matrices and number sets travel too.
|
|
35
|
+
def openmath(obj) = OpenMath::Phrasebook.to_openmath(obj)
|
|
36
|
+
|
|
37
|
+
# from_openmath(xml): the rcas object an OpenMath document stands for,
|
|
38
|
+
# held rather than evaluated - "1 + 2" comes back as 1 + 2.
|
|
39
|
+
def from_openmath(source) = OpenMath.read(source)
|
|
40
|
+
|
|
41
|
+
# popcorn(x**2 + 1): the OpenMath object in POPCORN, the notation meant
|
|
42
|
+
# for people rather than machines - "$x^2 + 1".
|
|
43
|
+
def popcorn(obj) = OpenMath::Phrasebook.to_openmath(obj).to_popcorn
|
|
44
|
+
|
|
45
|
+
# from_popcorn("$x^2 + 1"): the rcas object that notation stands for,
|
|
46
|
+
# held rather than evaluated.
|
|
47
|
+
def from_popcorn(source) = OpenMath::Popcorn.parse(source).to_expression
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Petkovsek's algorithm: the hypergeometric solutions of a linear
|
|
5
|
+
# recurrence with polynomial coefficients,
|
|
6
|
+
#
|
|
7
|
+
# p_0(n)*u(n) + p_1(n)*u(n + 1) + ... + p_r(n)*u(n + r) = 0.
|
|
8
|
+
#
|
|
9
|
+
# A hypergeometric term is one whose ratio u(n + 1)/u(n) is rational, and
|
|
10
|
+
# Petkovsek's theorem says every such ratio solving the recurrence can be
|
|
11
|
+
# written
|
|
12
|
+
#
|
|
13
|
+
# u(n + 1)/u(n) = z * a(n)/b(n) * c(n + 1)/c(n)
|
|
14
|
+
#
|
|
15
|
+
# with a, b monic, gcd(a(n), b(n + h)) = 1 for every integer h >= 0,
|
|
16
|
+
# a(n) | p_0(n), b(n) | p_r(n + r - 1), z a constant and c a polynomial.
|
|
17
|
+
# That turns an infinite search into a finite one: run through the monic
|
|
18
|
+
# divisors a and b, read the possible z off the leading coefficients (the
|
|
19
|
+
# top-degree terms have to cancel), and look for c with PolyRecurrence.
|
|
20
|
+
# Every candidate is verified on the ratio before it is returned.
|
|
21
|
+
#
|
|
22
|
+
# Sources (keys: MANUAL.md, Sources): [Pet92]; [Koe14, ch. 9];
|
|
23
|
+
# [PWZ96, ch. 8].
|
|
24
|
+
module Petkovsek
|
|
25
|
+
MAX_DIVISORS = 64
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# The ratios u(n + 1)/u(n) of the hypergeometric solutions, as rational
|
|
30
|
+
# functions of n.
|
|
31
|
+
def ratios(coeffs, n)
|
|
32
|
+
coeffs = coeffs.map { |c| Expression.lift(c).expand }
|
|
33
|
+
r = coeffs.size - 1
|
|
34
|
+
return [] if r < 1 || Scalar.zero?(coeffs.first) || Scalar.zero?(coeffs.last)
|
|
35
|
+
# first order: u(n + 1)/u(n) = -p0/p1 is the ratio, hypergeometric by
|
|
36
|
+
# definition - no search, and no divisor list to run out of (the
|
|
37
|
+
# search answered [] for (n + 1)...(n + 7): third review, S7)
|
|
38
|
+
return [(Simplify.negate(coeffs[0]) / coeffs[1]).cancel] if r == 1
|
|
39
|
+
ring = ring_for(coeffs, n) or return []
|
|
40
|
+
last = ring.call(coeffs.last.subs(n => n + r - 1).expand)
|
|
41
|
+
found = []
|
|
42
|
+
divisors(ring.call(coeffs.first)).each do |a|
|
|
43
|
+
divisors(last).each do |b|
|
|
44
|
+
next unless Summation.dispersion(a, b, n.name).empty?
|
|
45
|
+
candidates(coeffs, a, b, n, r).each do |ratio|
|
|
46
|
+
found << ratio unless found.any? { |f| Scalar.zero?((f - ratio).cancel) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
found
|
|
51
|
+
rescue TooMany
|
|
52
|
+
raise
|
|
53
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
54
|
+
[]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The solutions themselves: the ratios turned back into terms, in closed
|
|
58
|
+
# form wherever the product has one (factorials, powers, rationals).
|
|
59
|
+
def solutions(coeffs, n) = ratios(coeffs, n).map { |ratio| term(ratio, n) }
|
|
60
|
+
|
|
61
|
+
# The term with the given ratio, as a product from the first index at
|
|
62
|
+
# which the ratio is defined and non-zero.
|
|
63
|
+
def term(ratio, n)
|
|
64
|
+
i = Var.new(:_i)
|
|
65
|
+
Products.product(ratio.subs(n => i), i, Num.new(start_index(ratio, n)), (n - 1).simplify)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ---- the search --------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
# The (a, b) pair fixes A_j = a(n)...a(n + j - 1) and B_j = b(n + j)...
|
|
71
|
+
# b(n + r - 1); what is left is z and the polynomial c in
|
|
72
|
+
# sum_j p_j(n) z**j A_j(n) B_j(n) c(n + j) = 0.
|
|
73
|
+
def candidates(coeffs, a, b, n, r)
|
|
74
|
+
base = (0..r).map do |j|
|
|
75
|
+
aj = (0...j).reduce(Num.new(1)) { |acc, i| acc * shift(a, n, i) }
|
|
76
|
+
bj = (j...r).reduce(Num.new(1)) { |acc, i| acc * shift(b, n, i) }
|
|
77
|
+
(coeffs[j] * aj * bj).expand
|
|
78
|
+
end
|
|
79
|
+
zs(base, n, r).flat_map do |z|
|
|
80
|
+
qs = base.each_with_index.map { |q, j| (q * z**j).expand }
|
|
81
|
+
PolyRecurrence.basis(qs, n).filter_map do |c|
|
|
82
|
+
ratio = (z * a.to_expr * c.subs(n => n + 1) / (b.to_expr * c)).cancel
|
|
83
|
+
ratio if satisfies?(coeffs, ratio, n, r)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The constants z for which the top-degree terms of the transformed
|
|
89
|
+
# equation can cancel: the roots of sum_{j in J} lc(q_j)*z**j, where J
|
|
90
|
+
# collects the j of maximal degree.
|
|
91
|
+
def zs(base, n, r)
|
|
92
|
+
degrees = base.map { |q| Solve.polynomial_coefficients(q, n)&.size }
|
|
93
|
+
return [] if degrees.any?(&:nil?)
|
|
94
|
+
top = degrees.each_index.select { |j| degrees[j] == degrees.max }
|
|
95
|
+
return [] if top.size < 2 # one term alone cannot cancel
|
|
96
|
+
coefficients = Array.new(r + 1) { Num.new(0) }
|
|
97
|
+
top.each { |j| coefficients[j] = Solve.polynomial_coefficients(base[j], n).last }
|
|
98
|
+
Solve.polynomial_roots(coefficients).map(&:simplify)
|
|
99
|
+
.reject { |z| Scalar.zero?(z) || z.is_a?(RootOf) }
|
|
100
|
+
.uniq { |z| z.to_s }
|
|
101
|
+
rescue NotImplementedError, RCAS::Unsupported, DomainError
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Does a term with this ratio satisfy the recurrence? Checked as a
|
|
106
|
+
# rational identity: sum_j p_j(n) * ratio(n)*...*ratio(n + j - 1) = 0.
|
|
107
|
+
def satisfies?(coeffs, ratio, n, r)
|
|
108
|
+
total = Num.new(0)
|
|
109
|
+
product = Num.new(1)
|
|
110
|
+
(0..r).each do |j|
|
|
111
|
+
total += coeffs[j] * product
|
|
112
|
+
product = (product * ratio.subs(n => n + j)).cancel if j < r
|
|
113
|
+
end
|
|
114
|
+
Scalar.zero?(total.cancel)
|
|
115
|
+
rescue ZeroDivisionError
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ---- polynomials -------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def ring_for(coeffs, n)
|
|
122
|
+
vars = coeffs.flat_map { |c| c.variables.to_a }.uniq
|
|
123
|
+
vars = [n.name] | vars
|
|
124
|
+
QQ[*vars]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def shift(poly, n, h) = poly.to_expr.subs(n => n + h)
|
|
128
|
+
|
|
129
|
+
# Every monic divisor of +poly+, the constant 1 included.
|
|
130
|
+
def divisors(poly)
|
|
131
|
+
return [poly.ring.one] if poly.constant?
|
|
132
|
+
factorization = poly.factor
|
|
133
|
+
list = [poly.ring.one]
|
|
134
|
+
factorization.factors.each do |factor, multiplicity|
|
|
135
|
+
list = list.flat_map { |d| (0..multiplicity).map { |i| d * factor**i } }
|
|
136
|
+
# searching only the divisor 1 would be "no solution" for "too many
|
|
137
|
+
# to try": that is a refusal
|
|
138
|
+
raise TooMany, "hyper: #{poly} has more than #{MAX_DIVISORS} monic divisors to search" if list.size > MAX_DIVISORS
|
|
139
|
+
end
|
|
140
|
+
list.map(&:monic).uniq { |d| d.to_expr.to_s }
|
|
141
|
+
rescue DomainError
|
|
142
|
+
[poly.ring.one]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class TooMany < RCAS::Unsupported; end
|
|
146
|
+
|
|
147
|
+
# The first index from which the product of the ratio is finite and
|
|
148
|
+
# non-zero: past every non-negative integer root of numerator and
|
|
149
|
+
# denominator.
|
|
150
|
+
def start_index(ratio, n)
|
|
151
|
+
pair = Fraction.as_fraction(ratio, [n.name]) or return 0
|
|
152
|
+
roots = pair.flat_map do |poly|
|
|
153
|
+
coefficients = (0..poly.degree(n.name)).map { |i| poly.coefficient_in(n.name, i).to_expr }
|
|
154
|
+
next [] unless coefficients.all? { |c| c.variables.empty? }
|
|
155
|
+
Solve.polynomial_roots(coefficients)
|
|
156
|
+
end
|
|
157
|
+
integers = roots.filter_map { |r| r.simplify }
|
|
158
|
+
.select { |r| r.is_a?(Num) && r.value.is_a?(Integer) && !r.value.negative? }
|
|
159
|
+
.map(&:value)
|
|
160
|
+
integers.empty? ? 0 : integers.max + 1
|
|
161
|
+
rescue NotImplementedError, RCAS::Unsupported, DomainError, ZeroDivisionError
|
|
162
|
+
0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|