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,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# The q-analogues: everything in the q-world is built from the q-Pochhammer
|
|
5
|
+
# symbol
|
|
6
|
+
#
|
|
7
|
+
# (a; q)_n = (1 - a)(1 - a*q)...(1 - a*q**(n - 1)),
|
|
8
|
+
#
|
|
9
|
+
# which plays the part the rising factorial plays for ordinary
|
|
10
|
+
# hypergeometric terms. On top of it sit the q-bracket [n]_q, the
|
|
11
|
+
# q-factorial and the q-binomial coefficient:
|
|
12
|
+
#
|
|
13
|
+
# [n]_q = (1 - q**n)/(1 - q) -> n as q -> 1
|
|
14
|
+
# [n]_q! = [1]_q*[2]_q*...*[n]_q = (q; q)_n/(1 - q)**n
|
|
15
|
+
# qbinomial(n, k, q) = (q; q)_n/((q; q)_k*(q; q)_(n - k))
|
|
16
|
+
#
|
|
17
|
+
# Each is a function of its own so that output stays readable; the
|
|
18
|
+
# algorithms expand them into q-Pochhammer symbols themselves, the way the
|
|
19
|
+
# ordinary ones expand binomials into factorials. Integer arguments fold
|
|
20
|
+
# right away, as everywhere else in rcas.
|
|
21
|
+
#
|
|
22
|
+
# Sources (keys: MANUAL.md, Sources): [Koe14, ch. 10]; [GR04, ch. 1].
|
|
23
|
+
module QFunctions
|
|
24
|
+
NAMES = %i[qpochhammer qbracket qfactorial qbinomial].freeze
|
|
25
|
+
MAX_FOLD = 128
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# (a; q)_n, the q-Pochhammer symbol.
|
|
30
|
+
def qpochhammer(a, q, n)
|
|
31
|
+
a = Expression.lift(a)
|
|
32
|
+
q = Expression.lift(q)
|
|
33
|
+
n = Expression.lift(n)
|
|
34
|
+
m = integer(n)
|
|
35
|
+
return Fn.new(:qpochhammer, [a, q, n]) if m.nil? || m.negative? || m > MAX_FOLD
|
|
36
|
+
(0...m).reduce(Num.new(1)) { |acc, i| acc * (1 - a * q**i) }.simplify
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# [n]_q = 1 + q + ... + q**(n - 1).
|
|
40
|
+
def qbracket(n, q)
|
|
41
|
+
n = Expression.lift(n)
|
|
42
|
+
q = Expression.lift(q)
|
|
43
|
+
m = integer(n)
|
|
44
|
+
return Fn.new(:qbracket, [n, q]) if m.nil? || m.negative? || m > MAX_FOLD
|
|
45
|
+
(0...m).reduce(Num.new(0)) { |acc, i| acc + q**i }.simplify
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# [n]_q! = [1]_q*[2]_q*...*[n]_q.
|
|
49
|
+
def qfactorial(n, q)
|
|
50
|
+
n = Expression.lift(n)
|
|
51
|
+
q = Expression.lift(q)
|
|
52
|
+
m = integer(n)
|
|
53
|
+
return Fn.new(:qfactorial, [n, q]) if m.nil? || m.negative? || m > MAX_FOLD
|
|
54
|
+
(1..m).reduce(Num.new(1)) { |acc, i| acc * qbracket(Num.new(i), q) }.expand
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The Gaussian binomial coefficient: a polynomial in q whose value at
|
|
58
|
+
# q = 1 is the ordinary one.
|
|
59
|
+
def qbinomial(n, k, q)
|
|
60
|
+
n = Expression.lift(n)
|
|
61
|
+
k = Expression.lift(k)
|
|
62
|
+
q = Expression.lift(q)
|
|
63
|
+
top = integer(n)
|
|
64
|
+
bottom = integer(k)
|
|
65
|
+
return Fn.new(:qbinomial, [n, k, q]) if top.nil? || bottom.nil? || top.negative? || top > MAX_FOLD
|
|
66
|
+
return Num.new(0) if bottom.negative? || bottom > top
|
|
67
|
+
# the quotient is a polynomial: cancel it in a variable of its own and
|
|
68
|
+
# put q in afterwards, so that q = 1 and q = -1 give its values rather
|
|
69
|
+
# than 0/0 (third review, S10)
|
|
70
|
+
t = q.is_a?(Var) ? q : Var.new(:_qb)
|
|
71
|
+
numerator = ((top - bottom + 1)..top).reduce(Num.new(1)) { |acc, i| acc * (1 - t**i) }
|
|
72
|
+
denominator = (1..bottom).reduce(Num.new(1)) { |acc, i| acc * (1 - t**i) }
|
|
73
|
+
polynomial = (numerator / denominator).cancel.expand
|
|
74
|
+
t.equal?(q) ? polynomial : polynomial.subs(t => q).expand
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Constant folding, called by Functions.fold when Simplify meets one of
|
|
78
|
+
# these nodes: an integer index gives the product, whatever q is.
|
|
79
|
+
def fold(fn)
|
|
80
|
+
case fn.name
|
|
81
|
+
when :qpochhammer then fn.args.size == 3 ? qpochhammer(*fn.args) : nil
|
|
82
|
+
when :qbracket then fn.args.size == 2 ? qbracket(*fn.args) : nil
|
|
83
|
+
when :qfactorial then fn.args.size == 2 ? qfactorial(*fn.args) : nil
|
|
84
|
+
when :qbinomial then fn.args.size == 3 ? qbinomial(*fn.args) : nil
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ---- what the algorithms work with ------------------------------------------
|
|
89
|
+
|
|
90
|
+
# Everything rewritten in q-Pochhammer symbols, the way to_factorials
|
|
91
|
+
# rewrites binomials.
|
|
92
|
+
def to_pochhammer(expr)
|
|
93
|
+
expr = expr.map_children { |c| to_pochhammer(c) }
|
|
94
|
+
return expr unless expr.is_a?(Fn) && NAMES.include?(expr.name)
|
|
95
|
+
case expr.name
|
|
96
|
+
when :qbracket
|
|
97
|
+
n, q = expr.args
|
|
98
|
+
(1 - q**n) / (1 - q)
|
|
99
|
+
when :qfactorial
|
|
100
|
+
n, q = expr.args
|
|
101
|
+
Fn.new(:qpochhammer, [q, q, n]) / (1 - q)**n
|
|
102
|
+
when :qbinomial
|
|
103
|
+
n, k, q = expr.args
|
|
104
|
+
Fn.new(:qpochhammer, [q, q, n]) /
|
|
105
|
+
(Fn.new(:qpochhammer, [q, q, k]) * Fn.new(:qpochhammer, [q, q, (n - k).simplify]))
|
|
106
|
+
else expr
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Every q-Pochhammer symbol of one family written on the lowest index
|
|
111
|
+
# that occurs, (a; q)_(m + d) = (a; q)_m*(1 - a*q**m)*...*(1 - a*q**(m + d - 1)),
|
|
112
|
+
# so that a sum of such terms has a common factor and cancel can put it
|
|
113
|
+
# over one denominator.
|
|
114
|
+
def align(expr)
|
|
115
|
+
symbols = expr.each_node.select { |e| e.is_a?(Fn) && e.name == :qpochhammer && e.args.size == 3 }.uniq
|
|
116
|
+
return expr if symbols.size < 2
|
|
117
|
+
replacements = {}
|
|
118
|
+
symbols.group_by { |e| [e.args[0], e.args[1]] }.each_value do |family|
|
|
119
|
+
family.combination(2).each do |one, other|
|
|
120
|
+
d = (one.args[2] - other.args[2]).simplify
|
|
121
|
+
next unless d.is_a?(Num) && d.value.is_a?(Integer) && !d.zero?
|
|
122
|
+
big, small, steps = d.value.positive? ? [one, other, d.value] : [other, one, -d.value]
|
|
123
|
+
a, q, m = small.args
|
|
124
|
+
replacements[big] ||= (0...steps).reduce(small) { |acc, i| acc * (1 - a * q**(m + i)) }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
replacements.empty? ? expr : expr.subs(replacements)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# (a; q)_(m + d) / (a; q)_m = (1 - a*q**m)...(1 - a*q**(m + d - 1)):
|
|
131
|
+
# cancel q-Pochhammer symbols whose indices differ by an integer, so that
|
|
132
|
+
# a term ratio comes out as a rational function of q**k. Mutates
|
|
133
|
+
# +factors+ (base => exponent), like Combinatorics.merge_factorials.
|
|
134
|
+
def merge_pochhammers(factors)
|
|
135
|
+
loop do
|
|
136
|
+
symbols = factors.keys.select { |b| b.is_a?(Fn) && b.name == :qpochhammer }
|
|
137
|
+
pair = nil
|
|
138
|
+
symbols.combination(2).each do |one, other|
|
|
139
|
+
next unless one.args[0] == other.args[0] && one.args[1] == other.args[1]
|
|
140
|
+
d = (one.args[2] - other.args[2]).simplify
|
|
141
|
+
next unless d.is_a?(Num) && d.value.is_a?(Integer) && !d.zero?
|
|
142
|
+
pair = d.value.positive? ? [one, other, d.value] : [other, one, -d.value]
|
|
143
|
+
break
|
|
144
|
+
end
|
|
145
|
+
return factors unless pair
|
|
146
|
+
big, small, d = pair
|
|
147
|
+
a, q, m = small.args
|
|
148
|
+
exponent = factors.delete(big)
|
|
149
|
+
Simplify.add_factor(factors, small, exponent)
|
|
150
|
+
(0...d).each { |i| Simplify.add_factor(factors, (1 - a * q**(m + i)).simplify, exponent) }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def integer(value)
|
|
155
|
+
value.is_a?(Num) && value.value.is_a?(Integer) ? value.value : nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# q-summation: Gosper's algorithm in the q-world.
|
|
5
|
+
#
|
|
6
|
+
# A term is q-hypergeometric when t(k + 1)/t(k) is a rational function of
|
|
7
|
+
# q**k, the way an ordinary hypergeometric term has a rational function of
|
|
8
|
+
# k. Writing x for q**k turns the shift k -> k + 1 into x -> q*x, and
|
|
9
|
+
# everything Gosper does goes through with that one change: the
|
|
10
|
+
# q-Gosper-Petkovsek normal form
|
|
11
|
+
#
|
|
12
|
+
# t(k + 1)/t(k) = a(x)/b(x) * c(q*x)/c(x), gcd(a(x), b(q**h*x)) = 1
|
|
13
|
+
#
|
|
14
|
+
# for every integer h >= 0, and then the q-analogue of Gosper's equation
|
|
15
|
+
#
|
|
16
|
+
# a(x)*X(q*x) - b(x/q)*X(x) = c(x)
|
|
17
|
+
#
|
|
18
|
+
# for a polynomial X, from which the antidifference is
|
|
19
|
+
# S(k) = b(x/q)*X(x)/c(x) * t(k). The answer is checked by differencing it.
|
|
20
|
+
#
|
|
21
|
+
# Sources (keys: MANUAL.md, Sources): [Koe14, ch. 11]; [Koe93] for the
|
|
22
|
+
# q-analogue of the algorithm; [GR04] for the notation.
|
|
23
|
+
module QSummation
|
|
24
|
+
X = Var.new(:_qx) # stands for q**k while the algorithm runs
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# The term ratio t(k + 1)/t(k) as a rational function of q**k (as X), or
|
|
29
|
+
# nil when the term is not q-hypergeometric.
|
|
30
|
+
#
|
|
31
|
+
# A sum of terms is written as one q-hypergeometric term times a rational
|
|
32
|
+
# function of q**k: the first summand is the core, the others are
|
|
33
|
+
# measured against it, and the ratio of the whole is the ratio of the
|
|
34
|
+
# core times R(q*x)/R(x). Terms like q**(k + 1)/(q; q)_(k + 1) -
|
|
35
|
+
# q**k/(q; q)_k only telescope together, so taking them apart would lose
|
|
36
|
+
# the answer.
|
|
37
|
+
def ratio(f, k, q)
|
|
38
|
+
f = QFunctions.align(Expression.lift(f).simplify)
|
|
39
|
+
parts = summands(f)
|
|
40
|
+
core = parts.first
|
|
41
|
+
base = term_ratio(core, k, q) or return nil
|
|
42
|
+
return base if parts.size == 1
|
|
43
|
+
pieces = parts.map { |part| relative(part, core, k, q) }
|
|
44
|
+
return nil if pieces.any?(&:nil?)
|
|
45
|
+
total = pieces.reduce(:+).cancel
|
|
46
|
+
return nil if Scalar.zero?(total)
|
|
47
|
+
(base * total.subs(X => (q * X).simplify) / total).cancel
|
|
48
|
+
rescue ZeroDivisionError, DomainError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The ratio of a single q-hypergeometric term.
|
|
53
|
+
def term_ratio(f, k, q)
|
|
54
|
+
g = QFunctions.to_pochhammer(f)
|
|
55
|
+
in_x(cancel_q(g.subs(k => k + 1) / g), k, q)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# part/core as a rational function of q**k, or nil.
|
|
59
|
+
def relative(part, core, k, q)
|
|
60
|
+
in_x(cancel_q(QFunctions.to_pochhammer(part) / QFunctions.to_pochhammer(core)), k, q)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Cancel q-Pochhammer symbols whose indices differ by an integer first,
|
|
64
|
+
# then the rest as usual.
|
|
65
|
+
def cancel_q(quotient)
|
|
66
|
+
coefficient, factors = Simplify.factorize(quotient, simplify: true)
|
|
67
|
+
QFunctions.merge_pochhammers(factors)
|
|
68
|
+
Simplify.rebuild_product(coefficient, factors).cancel
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# q**(a*k + b) becomes q**b * X**a; nil when a k is left over.
|
|
72
|
+
def in_x(expr, k, q)
|
|
73
|
+
out = powers_in_x(expr, k, q)
|
|
74
|
+
out.variables.include?(k.name) ? nil : out
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def powers_in_x(expr, k, q) = powers_in(expr, k, q, X)
|
|
78
|
+
|
|
79
|
+
# q**(a*v + b) becomes q**b * target**a: X stands for q**k, and
|
|
80
|
+
# q-Zeilberger has a second one standing for q**n.
|
|
81
|
+
def powers_in(expr, v, q, target)
|
|
82
|
+
if expr.is_a?(Pow) && expr.base == q
|
|
83
|
+
cs = Solve.polynomial_coefficients(expr.exponent, v)
|
|
84
|
+
if cs && cs.size == 2 && cs[1].is_a?(Num) && cs[1].value.is_a?(Integer)
|
|
85
|
+
return (Simplify.power_node(q, cs[0]) * target**cs[1].value).simplify
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
expr.map_children { |c| powers_in(c, v, q, target) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# X back to q**k.
|
|
92
|
+
def in_k(expr, k, q) = expr.subs(X => q**k)
|
|
93
|
+
|
|
94
|
+
# q**n, for a parameter n, is not a polynomial in q: stand such powers in
|
|
95
|
+
# by variables of their own, so that the coefficients live in
|
|
96
|
+
# QQ(q, q**n) as they should. Returns the expression and { q**n => var }.
|
|
97
|
+
def abstract(expr, q)
|
|
98
|
+
powers = expr.each_node.select do |node|
|
|
99
|
+
node.is_a?(Pow) && node.base == q && !(node.exponent.is_a?(Num) && node.exponent.value.is_a?(Integer))
|
|
100
|
+
end.uniq
|
|
101
|
+
return [expr, {}] if powers.empty?
|
|
102
|
+
parameters = powers.each_with_index.to_h { |power, i| [power, Var.new(:"_qp#{i}")] }
|
|
103
|
+
[expr.subs(parameters), parameters]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ---- q-Gosper ----------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
# The q-antidifference: S(k) with S(k + 1) - S(k) = f(k), or nil.
|
|
109
|
+
def qgosper(f, k, q)
|
|
110
|
+
single(Expression.lift(f).simplify, k, q)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The summands of a sum, flattened.
|
|
114
|
+
def summands(f)
|
|
115
|
+
case f
|
|
116
|
+
when Add then summands(f.left) + summands(f.right)
|
|
117
|
+
when Sub then summands(f.left) + summands(f.right).map { |t| Simplify.negate(t) }
|
|
118
|
+
else [f]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def single(f, k, q)
|
|
123
|
+
r = ratio(f, k, q) or return nil
|
|
124
|
+
r, parameters = abstract(r, q)
|
|
125
|
+
pair = Fraction.as_fraction(r, [X.name, q.name] + parameters.values.map(&:name)) or return nil
|
|
126
|
+
a, b, c = normal_form(pair.first, pair.last, q)
|
|
127
|
+
bm = shift(b, q, -1)
|
|
128
|
+
bound = degree_bound(a, bm, c, q) or return nil
|
|
129
|
+
xs = (0..bound).map { |i| Var.new(:"_w#{i}") }
|
|
130
|
+
poly = xs.each_with_index.reduce(Num.new(0)) { |acc, (v, i)| acc + v * X**i }
|
|
131
|
+
equation = (a.to_expr * poly.subs(X => q * X) - bm.to_expr * poly - c.to_expr).expand
|
|
132
|
+
conditions = Solve.polynomial_coefficients(equation, X) or return nil
|
|
133
|
+
solution = linear_solve(conditions, xs) or return nil
|
|
134
|
+
value = poly.subs(solution).subs(xs.to_h { |v| [v, Num.new(0)] })
|
|
135
|
+
rational = (bm.to_expr * value / c.to_expr).cancel.subs(parameters.invert)
|
|
136
|
+
plain = (in_k(rational, k, q) * f).cancel
|
|
137
|
+
tidied = tidy(plain, k, q)
|
|
138
|
+
# Cancelling expands, which is worth it when it collapses the answer
|
|
139
|
+
# and not when it only spreads it out: keep the shorter form.
|
|
140
|
+
antidifference = tidied.to_s.length <= plain.to_s.length ? tidied : plain
|
|
141
|
+
telescopes?(antidifference, f, k, q) ? antidifference : nil
|
|
142
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# sum_{k=from}^{to} f(k) for a q-hypergeometric term: the antidifference
|
|
147
|
+
# at the ends, or nil.
|
|
148
|
+
def qsum(f, k, from, to, q)
|
|
149
|
+
g = qgosper(f, k, q) or return nil
|
|
150
|
+
ends(g, k, from, to, q) || ends(tidy(g, k, q), k, from, to, q)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The antidifference at both ends. A removable zero in the denominator
|
|
154
|
+
# (the form the answer happens to be written in) makes this nil, and the
|
|
155
|
+
# caller tries again with the cancelled form.
|
|
156
|
+
def ends(g, k, from, to, q)
|
|
157
|
+
(g.subs(k => (to + 1).simplify) - g.subs(k => from)).cancel
|
|
158
|
+
rescue ZeroDivisionError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# S(k + 1) - S(k) = f(k), checked at exact points; the linear algebra is
|
|
163
|
+
# exact, so this is a check on the derivation.
|
|
164
|
+
def telescopes?(s, f, k, q)
|
|
165
|
+
difference = s.subs(k => k + 1) - s - f
|
|
166
|
+
names = difference.variables.to_a
|
|
167
|
+
random = Random.new(20140610)
|
|
168
|
+
checked = 0
|
|
169
|
+
40.times do
|
|
170
|
+
point = names.to_h { |v| [v, Num.new(v == k.name ? random.rand(3..40) : Rational(random.rand(5..97), random.rand(2..7)))] }
|
|
171
|
+
value = begin
|
|
172
|
+
difference.subs(point).simplify
|
|
173
|
+
rescue ZeroDivisionError
|
|
174
|
+
next
|
|
175
|
+
end
|
|
176
|
+
return false unless Scalar.zero?(value)
|
|
177
|
+
checked += 1
|
|
178
|
+
break if checked >= 4
|
|
179
|
+
end
|
|
180
|
+
checked >= 4
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Cancel an expression that mixes q-Pochhammer symbols with powers of
|
|
184
|
+
# q**k: stand both in by variables, cancel as a rational function, put
|
|
185
|
+
# them back. Without it an antidifference keeps the shape of the term it
|
|
186
|
+
# came from, R(q**k)*f(k), instead of collapsing.
|
|
187
|
+
def tidy(expr, k, q)
|
|
188
|
+
expr = QFunctions.align(QFunctions.to_pochhammer(expr.simplify))
|
|
189
|
+
symbols = expr.each_node.select { |e| e.is_a?(Fn) && QFunctions::NAMES.include?(e.name) }.uniq
|
|
190
|
+
names = symbols.each_with_index.to_h { |symbol, i| [symbol, Var.new(:"_qa#{i}")] }
|
|
191
|
+
abstracted = powers_in_x(expr.subs(names), k, q)
|
|
192
|
+
return expr if abstracted.variables.include?(k.name)
|
|
193
|
+
cancelled = abstracted.cancel
|
|
194
|
+
in_k(cancelled, k, q).subs(names.invert).simplify
|
|
195
|
+
rescue ZeroDivisionError, DomainError
|
|
196
|
+
expr
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ---- linear algebra over the rational functions -------------------------------
|
|
200
|
+
|
|
201
|
+
# Solve a small linear system, cancelling every entry on the way.
|
|
202
|
+
# Elimination.rref leaves entries such as q + q*(q - 1)/(1 - q) standing
|
|
203
|
+
# and Scalar.zero? does not cancel them, so a consistent system can be
|
|
204
|
+
# declared inconsistent; here the entries are rational functions of q and
|
|
205
|
+
# cancelling each one keeps them small as well.
|
|
206
|
+
# => { unknown => value }, free unknowns standing for themselves, or nil.
|
|
207
|
+
def linear_solve(conditions, unknowns)
|
|
208
|
+
width = unknowns.size
|
|
209
|
+
rows = conditions.map do |condition|
|
|
210
|
+
row = unknowns.map { |u| Solve.polynomial_coefficients(condition, u)&.[](1) || Num.new(0) }
|
|
211
|
+
row + [Simplify.negate(condition.subs(unknowns.to_h { |u| [u, Num.new(0)] })).simplify]
|
|
212
|
+
end
|
|
213
|
+
pivots = []
|
|
214
|
+
rank = 0
|
|
215
|
+
(0...width).each do |column|
|
|
216
|
+
break if rank == rows.size
|
|
217
|
+
found = (rank...rows.size).find { |i| !vanishes?(rows[i][column]) }
|
|
218
|
+
next unless found
|
|
219
|
+
rows[rank], rows[found] = rows[found], rows[rank]
|
|
220
|
+
pivot = rows[rank][column]
|
|
221
|
+
rows[rank] = rows[rank].map { |v| (v / pivot).cancel }
|
|
222
|
+
rows.each_with_index do |row, i|
|
|
223
|
+
next if i == rank || vanishes?(row[column])
|
|
224
|
+
factor = row[column]
|
|
225
|
+
rows[i] = row.each_with_index.map { |v, j| (v - factor * rows[rank][j]).cancel }
|
|
226
|
+
end
|
|
227
|
+
pivots << column
|
|
228
|
+
rank += 1
|
|
229
|
+
end
|
|
230
|
+
return nil if rows.any? { |row| row[0...width].all? { |v| vanishes?(v) } && !vanishes?(row[width]) }
|
|
231
|
+
solution = {}
|
|
232
|
+
pivots.each_with_index do |column, i|
|
|
233
|
+
value = rows[i][width]
|
|
234
|
+
(0...width).each do |j|
|
|
235
|
+
next if j == column || vanishes?(rows[i][j])
|
|
236
|
+
value = (value - rows[i][j] * unknowns[j]).cancel
|
|
237
|
+
end
|
|
238
|
+
solution[unknowns[column]] = value
|
|
239
|
+
end
|
|
240
|
+
solution
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def vanishes?(value)
|
|
244
|
+
return true if Scalar.zero?(value)
|
|
245
|
+
value.is_a?(Expression) && Scalar.zero?(value.cancel)
|
|
246
|
+
rescue ZeroDivisionError
|
|
247
|
+
false
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ---- the polynomial side -----------------------------------------------------
|
|
251
|
+
|
|
252
|
+
# p(x)/q(x) = a(x)/b(x) * c(q*x)/c(x) with gcd(a(x), b(q**h*x)) = 1 for
|
|
253
|
+
# every integer h >= 0.
|
|
254
|
+
def normal_form(p, b, q, var = X)
|
|
255
|
+
a, c = p, p.ring.one
|
|
256
|
+
dispersion(a, b, q, var).each do |h|
|
|
257
|
+
g = a.gcd(shift(b, q, h, var))
|
|
258
|
+
next if g.constant?
|
|
259
|
+
a = a.exact_div(g)
|
|
260
|
+
b = b.exact_div(shift(g, q, -h, var))
|
|
261
|
+
(1..h).each { |i| c *= shift(g, q, -i, var) }
|
|
262
|
+
end
|
|
263
|
+
[a, b, c]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# p(q**h * x), in the variable +var+ (X while q-Gosper runs, the equation's
|
|
267
|
+
# own indeterminate for q-difference equations).
|
|
268
|
+
def shift(poly, q, h, var = X) = poly.ring.call(poly.to_expr.subs(var => (q**h * var).simplify).expand)
|
|
269
|
+
|
|
270
|
+
# The h >= 0 with gcd(a(x), b(q**h*x)) != 1. A shared root means
|
|
271
|
+
# q**h = beta/alpha for roots alpha of a and beta of b, so h cannot
|
|
272
|
+
# exceed the degree in q of the resultant of a(x) and b(y*x); within that
|
|
273
|
+
# bound the gcds are simply tried.
|
|
274
|
+
def dispersion(a, b, q, var = X)
|
|
275
|
+
y = Var.new(:_qy)
|
|
276
|
+
ring = QQ[*(a.ring.vars | [:_qy])]
|
|
277
|
+
shifted = ring.call(b.to_expr.subs(var => y * var).expand)
|
|
278
|
+
resultant = ring.call(a.to_expr).resultant(shifted, var.name)
|
|
279
|
+
return [] if resultant.zero?
|
|
280
|
+
bound = [resultant.degree(q.name), a.degree(var.name) + b.degree(var.name)].max
|
|
281
|
+
(0..bound).select { |h| !a.gcd(shift(b, q, h, var)).constant? }
|
|
282
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported
|
|
283
|
+
(0..(a.degree(var.name) + b.degree(var.name))).select { |h| !a.gcd(shift(b, q, h, var)).constant? }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# The degree of X in a(x)*X(q*x) - b(x/q)*X(x) = c(x): the leading terms
|
|
287
|
+
# give (lc(a)*q**d - lc(b))*x**(m + d), so either they do not cancel and
|
|
288
|
+
# d = deg c - m, or q**d = lc(b)/lc(a) pins d down.
|
|
289
|
+
def degree_bound(a, bm, c, q)
|
|
290
|
+
da, db, dc = a.degree(X.name), bm.degree(X.name), c.degree(X.name)
|
|
291
|
+
candidates = [dc - [da, db].max]
|
|
292
|
+
if da == db
|
|
293
|
+
power = power_of_q((bm.leading_coefficient_in(X.name).to_expr / a.leading_coefficient_in(X.name).to_expr).cancel, q)
|
|
294
|
+
candidates << power if power
|
|
295
|
+
end
|
|
296
|
+
bound = candidates.compact.max
|
|
297
|
+
bound.nil? || bound.negative? ? nil : bound
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# d with expr = q**d, or nil.
|
|
301
|
+
def power_of_q(expr, q)
|
|
302
|
+
coefficient, factors = Simplify.factorize(expr.simplify)
|
|
303
|
+
return nil unless coefficient == 1 && factors.size == 1 && factors.key?(q)
|
|
304
|
+
d = factors[q]
|
|
305
|
+
d.is_a?(Integer) ? d : nil
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# q-Zeilberger: creative telescoping in the q-world.
|
|
5
|
+
#
|
|
6
|
+
# For a term F(n, k) that is q-hypergeometric in both indices it finds
|
|
7
|
+
# coefficients sigma_j, rational in q**n, and a rational R with
|
|
8
|
+
#
|
|
9
|
+
# sum_j sigma_j*F(n + j, k) = G(k + 1) - G(k), G(k) = R(q**k)*F(n, k),
|
|
10
|
+
#
|
|
11
|
+
# so that sum_j sigma_j*S(n + j) = 0 for S(n) = sum_k F(n, k) over natural
|
|
12
|
+
# boundaries. It is Zeilberger's algorithm with q-Gosper inside: with
|
|
13
|
+
# x = q**k and y = q**n, the shift in k is x -> q*x and the shift in n is
|
|
14
|
+
# y -> q*y, and the pencil's Gosper equation
|
|
15
|
+
#
|
|
16
|
+
# a(x)*X(q*x) - b(x/q)*X(x) = c_0(x)*N(x)
|
|
17
|
+
#
|
|
18
|
+
# is again linear in the coefficients of X and the sigmas together.
|
|
19
|
+
#
|
|
20
|
+
# Sources (keys: MANUAL.md, Sources): [Koe14, ch. 12]; [Zei91] for the
|
|
21
|
+
# method; [GR04] for the notation.
|
|
22
|
+
module QZeilberger
|
|
23
|
+
MAX_ORDER = 2 # the q-world's coefficients are bigger; ask for more with max_order:
|
|
24
|
+
X = QSummation::X # q**k
|
|
25
|
+
Y = Var.new(:_qy1) # q**n
|
|
26
|
+
|
|
27
|
+
Certificate = Struct.new(:coefficients, :rational, :order, :term, :n, :k, :q) do
|
|
28
|
+
def recurrence(name)
|
|
29
|
+
coefficients.each_with_index.map { |c, j| c * Fn.new(name, [(n + j).simplify]) }.reduce(:+).simplify
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s = "#{recurrence(:S)} = 0"
|
|
33
|
+
def inspect = to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# The recurrence for S(n) = sum_k F(n, k), as an Equation in +s+ = S(n).
|
|
39
|
+
def qsumrecursion(term, k, q, s, max_order: MAX_ORDER)
|
|
40
|
+
n = Zeilberger.index_of(s)
|
|
41
|
+
k = Expression.lift(k)
|
|
42
|
+
found = certificate(term, n, k, q, max_order: max_order)
|
|
43
|
+
raise RCAS::Unsupported, "qsumrecursion: no recurrence of order #{max_order} or less for #{term}" if found.nil?
|
|
44
|
+
if boundary_terms?(found, n, k)
|
|
45
|
+
raise RCAS::Unsupported, "qsumrecursion: the telescoping identity holds for #{term}, but its " \
|
|
46
|
+
"boundary terms do not vanish, so the sum itself does not obey the recurrence"
|
|
47
|
+
end
|
|
48
|
+
Equation.new(found.recurrence(s.name), Num.new(0))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The certificate: R with sum_j sigma_j*F(n + j, k) = Delta_k(R*F).
|
|
52
|
+
def qsumcertificate(term, k, q, s, max_order: MAX_ORDER)
|
|
53
|
+
n = Zeilberger.index_of(s)
|
|
54
|
+
k = Expression.lift(k)
|
|
55
|
+
found = certificate(term, n, k, q, max_order: max_order)
|
|
56
|
+
raise RCAS::Unsupported, "qsumcertificate: no recurrence of order #{max_order} or less for #{term}" if found.nil?
|
|
57
|
+
found.rational
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# => Certificate or nil.
|
|
61
|
+
def certificate(term, n, k, q, max_order: MAX_ORDER)
|
|
62
|
+
term = Expression.lift(term).simplify
|
|
63
|
+
n = Expression.lift(n)
|
|
64
|
+
k = Expression.lift(k)
|
|
65
|
+
q = Expression.lift(q)
|
|
66
|
+
in_k = ratio_in(term, k, q, n, X) or return nil
|
|
67
|
+
in_n = ratio_in(term, n, q, k, Y) or return nil
|
|
68
|
+
(1..max_order).each do |order|
|
|
69
|
+
found = attempt(term, n, k, q, in_k, in_n, order)
|
|
70
|
+
return found if found
|
|
71
|
+
end
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The ratio in +v+ as a rational function of X and Y: the powers of the
|
|
76
|
+
# other index have to become the other variable too.
|
|
77
|
+
def ratio_in(term, v, q, other, target)
|
|
78
|
+
value = QSummation.ratio(term, v, q) or return nil
|
|
79
|
+
value = value.subs(X => target) unless target == X
|
|
80
|
+
QSummation.powers_in(value, other, q, target == X ? Y : X)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def attempt(term, n, k, q, in_k, in_n, order)
|
|
84
|
+
sigmas = (0..order).map { |j| Var.new(:"_qz#{j}") }
|
|
85
|
+
base = [X.name, Y.name, q.name] | term.variables.to_a.reject { |v| [n.name, k.name].include?(v) }
|
|
86
|
+
ring = QQ[*base]
|
|
87
|
+
big = QQ[*(base + sigmas.map(&:name))]
|
|
88
|
+
|
|
89
|
+
shifted = [Num.new(1)]
|
|
90
|
+
(1..order).each { |j| shifted << (shifted.last * in_n.subs(Y => (q**(j - 1) * Y).simplify)).cancel }
|
|
91
|
+
fractions = shifted.map { |a| Fraction.as_fraction(a, base) or return nil }
|
|
92
|
+
denominator = fractions.map(&:last).reduce(ring.one) { |acc, d| acc.lcm(d) }
|
|
93
|
+
numerators = fractions.map { |(p, d)| p * denominator.exact_div(d) }
|
|
94
|
+
pencil = sigmas.each_with_index.reduce(big.zero) do |acc, (s, j)|
|
|
95
|
+
acc + big.call(s) * numerators[j].to_ring(big)
|
|
96
|
+
end
|
|
97
|
+
return nil if pencil.zero?
|
|
98
|
+
|
|
99
|
+
pair = Fraction.as_fraction(in_k, base) or return nil
|
|
100
|
+
p = pair.first * denominator
|
|
101
|
+
d = pair.last * QSummation.shift(denominator, q, 1)
|
|
102
|
+
common = p.gcd(d)
|
|
103
|
+
p = p.exact_div(common)
|
|
104
|
+
d = d.exact_div(common)
|
|
105
|
+
a, b, c0 = QSummation.normal_form(p, d, q)
|
|
106
|
+
bm = QSummation.shift(b, q, -1)
|
|
107
|
+
c = c0.to_ring(big) * pencil
|
|
108
|
+
bound = QSummation.degree_bound(a.to_ring(big), bm.to_ring(big), c, q) or return nil
|
|
109
|
+
|
|
110
|
+
xs = (0..bound).map { |i| Var.new(:"_qw#{i}") }
|
|
111
|
+
poly = xs.each_with_index.reduce(Num.new(0)) { |acc, (v, i)| acc + v * X**i }
|
|
112
|
+
equation = (a.to_expr * poly.subs(X => (q * X).simplify) - bm.to_expr * poly - c.to_expr).expand
|
|
113
|
+
conditions = Solve.polynomial_coefficients(equation, X) or return nil
|
|
114
|
+
values = solve(conditions, xs, sigmas) or return nil
|
|
115
|
+
|
|
116
|
+
coefficients, factor = clear_denominators(sigmas.map { |s| values[s] }, q)
|
|
117
|
+
return nil if coefficients.all? { |value| Scalar.zero?(value) }
|
|
118
|
+
rational = (factor * bm.to_expr * poly.subs(values) / (c0.to_expr * denominator.to_expr)).cancel
|
|
119
|
+
found = Certificate.new(coefficients.map { |value| value.subs(Y => q**n).simplify },
|
|
120
|
+
rational.subs(Y => q**n, X => q**k).simplify, order, term, n, k, q)
|
|
121
|
+
verify(coefficients, rational, in_k, shifted, q) ? found : nil
|
|
122
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# As in the ordinary case: the recurrence carries over to the sum only
|
|
127
|
+
# when the boundary terms vanish, so sum_{k=0}^{n} F(n, k) is put into it
|
|
128
|
+
# for the first few n and a residue that is demonstrably non-zero counts
|
|
129
|
+
# against it.
|
|
130
|
+
def boundary_terms?(found, n, k)
|
|
131
|
+
order = found.coefficients.size - 1
|
|
132
|
+
(0..(order + 2)).any? do |i|
|
|
133
|
+
values = (0..order).map { |j| Zeilberger.value_at(found.term, n, k, Num.new(0), n, i + j) }
|
|
134
|
+
next false if values.any?(&:nil?)
|
|
135
|
+
total = values.each_with_index.reduce(Num.new(0)) do |acc, (value, j)|
|
|
136
|
+
acc + found.coefficients[j].subs(n => i) * value
|
|
137
|
+
end
|
|
138
|
+
Zeilberger.nonzero?(total)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A null space vector in which not every sigma vanishes.
|
|
143
|
+
def solve(conditions, xs, sigmas)
|
|
144
|
+
unknowns = xs + sigmas
|
|
145
|
+
solution = QSummation.linear_solve(conditions, unknowns) or return nil
|
|
146
|
+
free = unknowns - solution.keys
|
|
147
|
+
free.each do |one|
|
|
148
|
+
assignment = free.to_h { |v| [v, Num.new(v == one ? 1 : 0)] }
|
|
149
|
+
values = unknowns.to_h { |v| [v, (solution[v] || v).subs(assignment).cancel] }
|
|
150
|
+
next if sigmas.all? { |s| QSummation.vanishes?(values[s]) }
|
|
151
|
+
return values
|
|
152
|
+
end
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Coefficients rational in q**n cleared to polynomials, with the factor
|
|
157
|
+
# they were multiplied by (the certificate is scaled the same way).
|
|
158
|
+
def clear_denominators(coefficients, q)
|
|
159
|
+
vars = [Y.name, q.name] | coefficients.flat_map { |c| c.variables.to_a }
|
|
160
|
+
ring = QQ[*vars]
|
|
161
|
+
common = ring.one
|
|
162
|
+
coefficients.each do |c|
|
|
163
|
+
pair = Fraction.as_fraction(c, vars) or return [coefficients, Num.new(1)]
|
|
164
|
+
common = common.lcm(pair.last)
|
|
165
|
+
end
|
|
166
|
+
polynomials = coefficients.map { |c| ring.call((c * common.to_expr).cancel.expand) }
|
|
167
|
+
content = polynomials.reduce(ring.zero) { |acc, poly| acc.gcd(poly) }
|
|
168
|
+
factor = common
|
|
169
|
+
unless content.zero? || content.constant?
|
|
170
|
+
polynomials = polynomials.map { |poly| poly.exact_div(content) }
|
|
171
|
+
factor = factor.exact_div(content)
|
|
172
|
+
end
|
|
173
|
+
[polynomials.map(&:to_expr), factor.to_expr]
|
|
174
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
|
|
175
|
+
[coefficients, Num.new(1)]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# sum_j sigma_j*a_j(x) = R(q*x)*r(x) - R(x), at exact points.
|
|
179
|
+
def verify(coefficients, rational, in_k, shifted, q)
|
|
180
|
+
left = shifted.each_with_index.reduce(Num.new(0)) { |acc, (a, j)| acc + coefficients[j] * a }
|
|
181
|
+
difference = left - (rational.subs(X => (q * X).simplify) * in_k - rational)
|
|
182
|
+
names = difference.variables.to_a
|
|
183
|
+
random = Random.new(20140610)
|
|
184
|
+
checked = 0
|
|
185
|
+
40.times do
|
|
186
|
+
point = names.to_h { |v| [v, Num.new(Rational(random.rand(5..97), random.rand(2..7)))] }
|
|
187
|
+
value = begin
|
|
188
|
+
difference.subs(point).simplify
|
|
189
|
+
rescue ZeroDivisionError
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
return false unless Scalar.zero?(value)
|
|
193
|
+
checked += 1
|
|
194
|
+
break if checked >= 4
|
|
195
|
+
end
|
|
196
|
+
checked >= 4
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|