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