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,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Renders an expression tree as Ruby-flavoured infix text with the minimum
5
+ # number of parentheses needed to preserve the tree structure.
6
+ module Printer
7
+ ADDITIVE = 1 # + -
8
+ MULTIPLICATIVE = 2 # * /
9
+ UNARY = 3 # -x
10
+ POWER = 4 # **
11
+ ATOM = 5
12
+
13
+ module_function
14
+
15
+ def negative_number?(e) = e.is_a?(Num) && e.value.is_a?(Numeric) && e.value.real? && e.value.negative?
16
+
17
+ def print(expr)
18
+ case expr
19
+ when Var then expr.name.to_s
20
+ when Num then number(expr.value)
21
+ when Neg then "-#{wrap(expr.arg, UNARY, :inner)}"
22
+ when Add then negative_number?(expr.right) ? binary(Sub.new(expr.left, Num.new(-expr.right.value)), " - ") : binary(expr, " + ")
23
+ # a negative number on the right reads as the other sign: x - (-7/10)
24
+ # is x + 7/10 (fifth review, 3.3); the tree itself is unchanged
25
+ when Sub then negative_number?(expr.right) ? binary(Add.new(expr.left, Num.new(-expr.right.value)), " + ") : binary(expr, " - ")
26
+ when Mul then binary(expr, "*")
27
+ when Div then binary(expr, "/")
28
+ when Pow then "#{wrap(expr.base, POWER, :left)}**#{wrap(expr.exponent, POWER, :right)}"
29
+ when Const then RCAS.symbol(expr.name)
30
+ when RootOf then "RootOf(#{print(expr.poly.to_expr.subs(Var.new(expr.var) => Var.new(:x)))}, #{expr.index})"
31
+ when Fn
32
+ return "e" if expr.name == :exp && expr.args == [Num.new(1)]
33
+ if expr.name == :factorial && expr.args.size == 1
34
+ arg = expr.args.first
35
+ atom = arg.is_a?(Var) || (arg.is_a?(Num) && number_precedence(arg.value) == ATOM)
36
+ return atom ? "#{print(arg)}!" : "(#{print(arg)})!"
37
+ end
38
+ "#{expr.name}(#{expr.args.map { |a| print(a) }.join(', ')})"
39
+ when Integral then "integral(#{expr.children.map { |c| print(c) }.join(', ')})"
40
+ when Limit then "limit(#{print(expr.expr)}, #{print(expr.var)}, #{print(expr.point)})"
41
+ when Sum then "sum(#{expr.children.map { |c| print(c) }.join(', ')})"
42
+ when Product then "product(#{expr.children.map { |c| print(c) }.join(', ')})"
43
+ when Derivative then "D(#{print(expr.expr)}, #{print(expr.var)}#{expr.order == 1 ? '' : ", #{expr.order}"})"
44
+ when Piecewise then "piecewise(#{expr.branches.map { |cond, value| "#{condition(cond)} => #{print(value)}" }.join(', ')})"
45
+ else raise ArgumentError, "don't know how to print #{expr.class}"
46
+ end
47
+ end
48
+
49
+ # The condition of a piecewise branch, written so that the whole node
50
+ # can be typed back in: `x < 0`, `x.eq(0)`, `interval(0, 1)`, `:else`.
51
+ def condition(cond)
52
+ case cond
53
+ when Inequality then "#{print(cond.lhs)} #{Inequality::OPS[cond.op]} #{print(cond.rhs)}"
54
+ when Equation
55
+ # a method call binds tighter than any operator: (exp(a) - 1).eq(0)
56
+ lhs = print(cond.lhs)
57
+ bare = cond.lhs.is_a?(Var) || cond.lhs.is_a?(Fn) || cond.lhs.is_a?(Const) || (cond.lhs.is_a?(Num) && !lhs.start_with?("-"))
58
+ "#{bare ? lhs : "(#{lhs})"}.eq(#{print(cond.rhs)})"
59
+ when Interval then interval(cond)
60
+ when RealSet then cond.intervals.map { |i| interval(i) }.join(" | ")
61
+ else cond.inspect
62
+ end
63
+ end
64
+
65
+ def interval(i)
66
+ opens = [("left_open: true" if i.left_open), ("right_open: true" if i.right_open)].compact
67
+ "interval(#{print(i.low)}, #{print(i.high)}#{opens.empty? ? '' : ", #{opens.join(', ')}"})"
68
+ end
69
+
70
+ def precedence(expr)
71
+ case expr
72
+ when Add, Sub then ADDITIVE
73
+ when Mul, Div then MULTIPLICATIVE
74
+ when Neg then UNARY
75
+ when Pow then POWER
76
+ when Num then number_precedence(expr.value)
77
+ else ATOM
78
+ end
79
+ end
80
+
81
+ OPERATORS = { Add => " + ", Sub => " - ", Mul => "*", Div => "/" }.freeze
82
+
83
+ # Walks the left spine of same-precedence operators iteratively so that
84
+ # long chains like a + b + c + ... don't recurse once per term.
85
+ def binary(expr, _op)
86
+ prec = precedence(expr)
87
+ spine = []
88
+ node = expr
89
+ while OPERATORS.key?(node.class) && precedence(node) == prec
90
+ spine << node
91
+ node = node.left
92
+ end
93
+ out = wrap(node, prec, :left)
94
+ spine.reverse_each { |n| out = "#{out}#{OPERATORS[n.class]}#{wrap(n.right, prec, :right, n)}" }
95
+ out
96
+ end
97
+
98
+ # Parenthesize +child+ when it binds looser than its parent, or when it
99
+ # binds equally but sits in a position where that would change meaning.
100
+ def wrap(child, parent_prec, position, parent = nil)
101
+ s = print(child)
102
+ child_prec = precedence(child)
103
+ needs_parens =
104
+ case position
105
+ when :left then child_prec < parent_prec || (parent_prec == POWER && child_prec == POWER) || (child_prec == parent_prec && child.is_a?(Num))
106
+ when :right then child_prec < parent_prec || (child_prec == parent_prec && !associative_right?(parent, child)) || child.is_a?(Neg) || (child.is_a?(Num) && child_prec == UNARY)
107
+ when :inner then child_prec <= parent_prec
108
+ end
109
+ needs_parens ? "(#{s})" : s
110
+ end
111
+
112
+ # a + (b - c) and a*(b*c) read fine without parentheses; a - (b + c) and
113
+ # a/(b*c) do not.
114
+ def associative_right?(parent, child)
115
+ (parent.is_a?(Add) && (child.is_a?(Add) || child.is_a?(Sub))) || (parent.is_a?(Mul) && child.is_a?(Mul))
116
+ end
117
+
118
+ def number(value)
119
+ case value
120
+ when Rational then value.denominator == 1 ? value.numerator.to_s : "#{value.numerator}/#{value.denominator}"
121
+ when Complex then complex(value)
122
+ else value.to_s
123
+ end
124
+ end
125
+
126
+ def complex(value)
127
+ re, im = value.real, value.imaginary
128
+ im_s =
129
+ case im
130
+ when 1 then "i"
131
+ when -1 then "-i"
132
+ else "#{number(im)}*i"
133
+ end
134
+ return im_s if re.zero?
135
+ sign = im.negative? ? " - " : " + "
136
+ "#{number(re)}#{sign}#{im_s.delete_prefix('-')}"
137
+ end
138
+
139
+ def number_precedence(value)
140
+ return value.printer_precedence if value.respond_to?(:printer_precedence)
141
+ if value.is_a?(Complex)
142
+ return ADDITIVE unless value.real.zero?
143
+ return value.imaginary == 1 ? ATOM : (value.imaginary == -1 ? UNARY : MULTIPLICATIVE)
144
+ end
145
+ return UNARY if value.respond_to?(:negative?) && value.negative?
146
+ return MULTIPLICATIVE if value.is_a?(Rational) && value.denominator != 1
147
+ ATOM
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # product(f, k, a, b): an unevaluated product, returned when no closed form is known.
5
+ class Product < Expression
6
+ attr_reader :term, :var, :from, :to
7
+
8
+ def initialize(term, var, from, to)
9
+ @term = term
10
+ @var = var
11
+ @from = from
12
+ @to = to
13
+ freeze
14
+ end
15
+
16
+ def bound_variable = var
17
+ def children = [term, var, from, to]
18
+ def rebuild(term, var, from, to) = Product.new(term, var, from, to)
19
+ def to_sexp = [:product, term.to_sexp, var.to_sexp, from.to_sexp, to.to_sexp]
20
+ end
21
+
22
+ # Symbolic products.
23
+ #
24
+ # product(k, k, 1, n) # => n!
25
+ # product(2*k, k: 1..n) # => 2**n*n!
26
+ # product(2*k - 1, k: 1..n) # => 2**n*gamma(1/2 + n)/pi**(1/2)
27
+ # product(a**k, k: 1..n) # => a**(n/2 + n**2/2)
28
+ #
29
+ # A term is split into factors: constants give powers, a**u(k) gives
30
+ # a**sum(u), and a polynomial in k that factors into linear factors over QQ
31
+ # gives ratios of factorials (integer shifts) or gamma values (rational
32
+ # shifts), since prod_{k=a}^{b} (k + r) = gamma(b + r + 1) / gamma(a + r).
33
+ # A linear factor whose root is a parameter gets the same ratio, under the
34
+ # generic assumption that the root is not inside the range.
35
+ # Products with integer bounds that fit no pattern are multiplied out.
36
+ #
37
+ # Sources (keys: MANUAL.md, Sources): [GKP94, §5.5] for the gamma function
38
+ # and rising factorials; the rest is textbook algebra.
39
+ module Products
40
+ module_function
41
+
42
+ def product(f, k, from, to)
43
+ f = Expression.lift(f).simplify
44
+ k = Expression.lift(k)
45
+ # -1 + 3 is the bound 2 (what subs leaves), and 5/2 is no bound at all:
46
+ # a product steps over integers, as a sum does (fourth review, S11)
47
+ from = Expression.lift(from).simplify
48
+ to = Expression.lift(to).simplify
49
+ raise ArgumentError, "product: the index must be a symbol, got #{k}" unless k.is_a?(Var)
50
+ [from, to].each do |b|
51
+ next unless b.is_a?(Num) && b.value.is_a?(Numeric) && b.value.real? && b.value != b.value.round
52
+ raise ArgumentError, "product: the bounds must be integers, got #{b}"
53
+ end
54
+ if (reversed = Summation.karr(from, to))
55
+ # Karr's convention, multiplicatively: prod_{a}^{b} = 1/prod_{b+1}^{a-1}
56
+ return Num.new(1) if reversed == :empty
57
+ return (1 / product(f, k, reversed.first, reversed.last)).simplify
58
+ end
59
+ count = (to - from + 1).simplify
60
+ return (f**count).simplify unless f.variables.include?(k.name)
61
+ # a factor with no value makes a product with none: 1/k at k = 0
62
+ return UNDEFINED if Summation.pole_in_range?(f, k, from, to)
63
+ return Product.new(f, k, from, to) if Summation.pole_past_start?(f, k, from, to)
64
+ return Product.new(f, k, from, to) if Limits.infinite?(to) || Limits.infinite?(from)
65
+
66
+ closed_form(f, k, from, to, count) || direct(f, k, from, to) || Product.new(f, k, from, to)
67
+ end
68
+
69
+ def depends?(e, k) = e.variables.include?(k.name)
70
+
71
+ def closed_form(f, k, from, to, count)
72
+ f = f.cancel if f.is_a?(Add) || f.is_a?(Sub) # (k + 1)/k rather than 1 + 1/k
73
+ coeff, factors = Simplify.factorize(f)
74
+ result = Num.new(coeff)**count
75
+ factors.each do |base, e|
76
+ exponent = Expression.lift(e)
77
+ if !depends?(base, k) && !depends?(exponent, k)
78
+ result *= base**(exponent * count)
79
+ elsif !depends?(base, k) # a**u(k) = a**sum(u)
80
+ s = Summation.sum(exponent, k, from, to)
81
+ return nil if s.is_a?(Sum)
82
+ result *= Simplify.power_node(base, s)
83
+ elsif e.is_a?(Integer) && (g = polynomial_product(base, k, from, to, count))
84
+ result *= g**e
85
+ else
86
+ return nil
87
+ end
88
+ end
89
+ result.simplify
90
+ end
91
+
92
+ # prod p(k) for p with linear factors over QQ, else nil.
93
+ def polynomial_product(base, k, from, to, count)
94
+ poly = begin
95
+ QQ[k.name].call(base)
96
+ rescue DomainError
97
+ return parametric_product(base, k, from, to, count)
98
+ end
99
+ factorization = poly.factor
100
+ result = factorization.unit**count
101
+ factorization.factors.each do |g, m|
102
+ return nil unless g.degree == 1
103
+ a = g.coeff(1).value
104
+ b = g.coeff(0).value
105
+ result *= (Num.new(a)**count)**m unless a == 1
106
+ ratio = gamma_ratio(Rational(b, a), from, to) or return nil
107
+ result *= ratio**m
108
+ end
109
+ result
110
+ end
111
+
112
+ # prod_{k=from}^{to} (k + r) = gamma(to + r + 1) / gamma(from + r); with factorials
113
+ # for integer r. Zero when a factor vanishes inside the range.
114
+ def gamma_ratio(r, from, to)
115
+ lower = (from + Num.new(r)).simplify # the first factor
116
+ if lower.is_a?(Num) && lower.value.is_a?(Integer) && lower.value <= 0
117
+ upper = (to + Num.new(r)).simplify
118
+ return Num.new(0) if upper.is_a?(Num) && upper.value >= 0
119
+ # a symbolic upper bound reaches the zero only from some n on:
120
+ # product(k - 3, k, 1, n) is 1, -2, 2 at n = 0, 1, 2 and was 0 for
121
+ # every n, and product(k - 1, k, 1, n) is the empty product 1 at
122
+ # n = 0 (fourth review); no single closed form says both
123
+ return nil unless upper.is_a?(Num)
124
+ end
125
+ if r.denominator == 1
126
+ RCAS.factorial((to + Num.new(r)).simplify) / RCAS.factorial((from + Num.new(r) - 1).simplify)
127
+ else
128
+ RCAS.gamma((to + Num.new(r) + 1).simplify) / RCAS.gamma(lower)
129
+ end
130
+ end
131
+
132
+ # A linear factor whose root is a parameter, a - k: the same gamma ratio.
133
+ # Whether the root falls inside the range cannot be decided, so this is
134
+ # the generic answer, as everywhere else where a symbolic quantity would
135
+ # have to be zero for it to be wrong.
136
+ def parametric_product(base, k, from, to, count)
137
+ coefficients = Solve.polynomial_coefficients(base.expand, k)
138
+ return nil unless coefficients && coefficients.size == 2
139
+ lead = coefficients.last
140
+ return nil if depends?(lead, k) || Scalar.zero?(lead)
141
+ r = (coefficients.first / lead).cancel
142
+ ((lead**count) * RCAS.gamma((to + r + 1).simplify) / RCAS.gamma((from + r).simplify)).simplify
143
+ rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
144
+ nil
145
+ end
146
+
147
+ # Integer bounds: multiply out (up to 1000 factors).
148
+ def direct(f, k, from, to)
149
+ return nil unless from.is_a?(Num) && to.is_a?(Num) && from.value.is_a?(Integer) && to.value.is_a?(Integer)
150
+ return Num.new(1) if to.value < from.value
151
+ return nil if to.value - from.value > 1000
152
+ (from.value..to.value).reduce(Num.new(1)) { |acc, i| (acc * f.subs(k => i)).simplify }
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Linear q-difference equations,
5
+ #
6
+ # p_0(x)*f(x) + p_1(x)*f(q*x) + ... + p_r(x)*f(q**r*x) = 0,
7
+ #
8
+ # and their q-hypergeometric solutions: Petkovsek's algorithm with the
9
+ # shift x -> q*x in place of n -> n + 1. A q-hypergeometric term is one
10
+ # whose ratio f(q*x)/f(x) is rational in x, and the same theorem holds with
11
+ # the same shape,
12
+ #
13
+ # f(q*x)/f(x) = z * a(x)/b(x) * c(q*x)/c(x),
14
+ #
15
+ # with a(x) a divisor of p_0(x), b(x) a divisor of p_r(q**(r - 1)*x),
16
+ # gcd(a(x), b(q**h*x)) = 1 for every integer h >= 0, z free of x and c a
17
+ # polynomial. The polynomial c is found with a degree bound of its own: the
18
+ # top-degree terms give sum_j lc(Q_j)*(q**d)**j = 0, so q**d has to be a
19
+ # root of that polynomial in one variable and d is read off it.
20
+ #
21
+ # Solutions are reported at x = q**n, where a q-hypergeometric term is a
22
+ # product of q-Pochhammer symbols and powers - the form in which such
23
+ # answers are written and the form q-summation can use. Every candidate is
24
+ # verified on the ratio before it is returned.
25
+ #
26
+ # Sources (keys: MANUAL.md, Sources): [Koe14, ch. 12]; [APP98] for the
27
+ # q-analogue of Petkovsek's algorithm; [GR04] for the notation.
28
+ module QDifference
29
+ MAX_DIVISORS = 64
30
+
31
+ module_function
32
+
33
+ # qsolve(eq(f(q*x), (1 - a*x)*f(x)), f, x, q) => f(q**n) = C1*(a; q)_n
34
+ def qsolve(equation, f, x, q, n: :n)
35
+ name, x, q, coeffs, forcing = normalize(equation, f, x, q)
36
+ raise RCAS::Unsupported, "qsolve: #{forcing} makes the equation inhomogeneous" unless Scalar.zero?(forcing)
37
+ index = Var.new(n)
38
+ order = coeffs.size - 1
39
+ found = solutions(coeffs, x, q, index)
40
+ raise RCAS::Unsupported, "qsolve: no q-hypergeometric solutions (see qhyper)" if found.empty?
41
+ if found.size < order
42
+ raise RCAS::Unsupported, "qsolve: only #{found.size} of #{order} solutions are q-hypergeometric: " \
43
+ "#{found.map(&:to_s).join(', ')} (see qhyper)"
44
+ end
45
+ general = found.each_with_index.map { |t, i| Var.new(:"C#{i + 1}") * t }.reduce(:+).simplify
46
+ Equation.new(Fn.new(name, [(q**index).simplify]), general)
47
+ end
48
+
49
+ # The ratios f(q*x)/f(x) of the q-hypergeometric solutions: the honest
50
+ # description, free of any choice of index.
51
+ def qhyper(equation, f, x, q)
52
+ _, x, q, coeffs, forcing = normalize(equation, f, x, q)
53
+ raise RCAS::Unsupported, "qhyper: #{equation} is not homogeneous" unless Scalar.zero?(forcing)
54
+ ratios(coeffs, x, q)
55
+ end
56
+
57
+ # The solutions as terms in n, where x = q**n.
58
+ def solutions(coeffs, x, q, n)
59
+ ratios(coeffs, x, q).filter_map { |ratio| term(ratio, x, q, n) }
60
+ end
61
+
62
+ # ---- reading the equation ----------------------------------------------------
63
+
64
+ # [name, x, q, [p_0, ..., p_r], forcing]
65
+ def normalize(equation, f, x, q)
66
+ x = Expression.lift(x)
67
+ q = Expression.lift(q)
68
+ raise ArgumentError, "qsolve: the indeterminate must be a symbol, got #{x}" unless x.is_a?(Var)
69
+ name = Recurrence.sequence_name(f)
70
+ g = Solve.to_zero(equation).simplify
71
+ shifts = shift_table(g, name, x, q)
72
+ raise ArgumentError, "#{equation} contains no #{name}(q**j*#{x})" if shifts.empty?
73
+ low = shifts.values.min
74
+ unless low.zero?
75
+ g = g.subs(x => (q**(-low) * x).simplify).simplify
76
+ shifts = shift_table(g, name, x, q)
77
+ end
78
+ order = shifts.values.max
79
+ raise ArgumentError, "#{equation} is not a q-difference equation: only #{name}(#{x}) occurs" if order.zero?
80
+ ds = (0..order).map { |j| Var.new(:"_f#{j}") }
81
+ h = g.subs(shifts.to_h { |t, j| [t, ds[j]] })
82
+ raise RCAS::Unsupported, "only linear q-difference equations are supported" unless Solve.linear_in?(h, ds)
83
+ coeffs = ds.map { |d| Solve.polynomial_coefficients(h, d)&.[](1) || Num.new(0) }
84
+ forcing = Simplify.negate(h.subs(ds.to_h { |d| [d, Num.new(0)] })).simplify
85
+ coeffs, forcing = Recurrence.clear_denominators(coeffs, forcing, x)
86
+ [name, x, q, coeffs.map { |c| c.expand }, forcing]
87
+ end
88
+
89
+ # { f(q**j*x) node => j }
90
+ def shift_table(g, name, x, q)
91
+ g.each_node.select { |e| e.is_a?(Fn) && e.name == name }.uniq.to_h do |t|
92
+ raise ArgumentError, "#{t}: one argument expected" unless t.args.size == 1
93
+ j = power_of_q((t.args.first / x).cancel, q)
94
+ raise RCAS::Unsupported, "#{t}: the argument must be #{x} times a power of #{q}" if j.nil?
95
+ [t, j]
96
+ end
97
+ end
98
+
99
+ # ---- q-Petkovsek -------------------------------------------------------------
100
+
101
+ def ratios(coeffs, x, q)
102
+ r = coeffs.size - 1
103
+ return [] if r < 1 || Scalar.zero?(coeffs.first) || Scalar.zero?(coeffs.last)
104
+ ring = QQ[*([x.name, q.name] | coeffs.flat_map { |c| c.variables.to_a })]
105
+ first = ring.call(coeffs.first)
106
+ last = ring.call(coeffs.last.subs(x => (q**(r - 1) * x).simplify).expand)
107
+ found = []
108
+ divisors(first, x).each do |a|
109
+ divisors(last, x).each do |b|
110
+ next unless QSummation.dispersion(a, b, q, x).empty?
111
+ candidates(coeffs, a, b, x, q, r).each do |ratio|
112
+ found << ratio unless found.any? { |other| Scalar.zero?((other - ratio).cancel) }
113
+ end
114
+ end
115
+ end
116
+ found
117
+ rescue DomainError, NotImplementedError, RCAS::Unsupported, ZeroDivisionError
118
+ []
119
+ end
120
+
121
+ # For one (a, b): the possible z, and for each the polynomials c.
122
+ def candidates(coeffs, a, b, x, q, r)
123
+ base = (0..r).map do |j|
124
+ aj = (0...j).reduce(Num.new(1)) { |acc, i| acc * a.to_expr.subs(x => (q**i * x).simplify) }
125
+ bj = (j...r).reduce(Num.new(1)) { |acc, i| acc * b.to_expr.subs(x => (q**i * x).simplify) }
126
+ (coeffs[j] * aj * bj).expand
127
+ end
128
+ zs(base, x, r).flat_map do |z|
129
+ qs = base.each_with_index.map { |poly, j| (poly * z**j).expand }
130
+ polynomial_solutions(qs, x, q).filter_map do |c|
131
+ ratio = (z * a.to_expr * c.subs(x => (q * x).simplify) / (b.to_expr * c)).cancel
132
+ ratio if satisfies?(coeffs, ratio, x, q, r)
133
+ end
134
+ end
135
+ end
136
+
137
+ # The z for which the top-degree terms can cancel.
138
+ def zs(base, x, r)
139
+ degrees = base.map { |poly| Solve.polynomial_coefficients(poly, x)&.size }
140
+ return [] if degrees.any?(&:nil?)
141
+ top = degrees.each_index.select { |j| degrees[j] == degrees.max }
142
+ return [] if top.size < 2
143
+ coefficients = Array.new(r + 1) { Num.new(0) }
144
+ top.each { |j| coefficients[j] = Solve.polynomial_coefficients(base[j], x).last }
145
+ Solve.polynomial_roots(coefficients).map(&:simplify)
146
+ .reject { |z| Scalar.zero?(z) || z.is_a?(RootOf) }
147
+ .uniq { |z| z.to_s }
148
+ rescue NotImplementedError, RCAS::Unsupported, DomainError
149
+ []
150
+ end
151
+
152
+ # sum_j p_j(x)*ratio(x)*ratio(q*x)*...*ratio(q**(j - 1)*x) = 0?
153
+ def satisfies?(coeffs, ratio, x, q, r)
154
+ total = Num.new(0)
155
+ product = Num.new(1)
156
+ (0..r).each do |j|
157
+ total += coeffs[j] * product
158
+ product = (product * ratio.subs(x => (q**j * x).simplify)).cancel if j < r
159
+ end
160
+ Scalar.zero?(total.cancel)
161
+ rescue ZeroDivisionError
162
+ false
163
+ end
164
+
165
+ # ---- polynomial solutions of a q-difference equation --------------------------
166
+
167
+ # Polynomial solutions c of sum_j Q_j(x)*c(q**j*x) = 0, a basis.
168
+ def polynomial_solutions(coeffs, x, q)
169
+ bound = degree_bound(coeffs, x, q)
170
+ return [] if bound.nil? || bound.negative?
171
+ ws = (0..bound).map { |i| Var.new(:"_qc#{i}") }
172
+ c = ws.each_with_index.reduce(Num.new(0)) { |acc, (w, i)| acc + w * x**i }
173
+ residual = coeffs.each_with_index.reduce(Num.new(0)) do |acc, (poly, j)|
174
+ acc + poly * c.subs(x => (q**j * x).simplify)
175
+ end
176
+ conditions = Solve.polynomial_coefficients(residual.expand, x) or return []
177
+ solution = QSummation.linear_solve(conditions, ws) or return []
178
+ general = c.subs(solution)
179
+ free = ws - solution.keys
180
+ free.filter_map do |one|
181
+ value = general.subs(free.to_h { |w| [w, Num.new(w == one ? 1 : 0)] }).simplify
182
+ Scalar.zero?(value) ? nil : value
183
+ end
184
+ end
185
+
186
+ # A solution of degree d makes the top-degree terms
187
+ # sum_j lc(Q_j)*(q**d)**j*x**(D + d) cancel, so q**d is a root of that
188
+ # polynomial in one variable; d is read off the roots that are powers
189
+ # of q.
190
+ def degree_bound(coeffs, x, q)
191
+ degrees = coeffs.map { |poly| Solve.polynomial_coefficients(poly, x)&.size }
192
+ return nil if degrees.any?(&:nil?)
193
+ top = degrees.each_index.select { |j| degrees[j] == degrees.max }
194
+ return nil if top.empty?
195
+ return nil if top.size < 2 # a single top term cannot vanish
196
+ coefficients = Array.new(coeffs.size) { Num.new(0) }
197
+ top.each { |j| coefficients[j] = Solve.polynomial_coefficients(coeffs[j], x).last }
198
+ Solve.polynomial_roots(coefficients).map(&:simplify)
199
+ .filter_map { |z| power_of_q(z, q) }
200
+ .reject(&:negative?).max
201
+ rescue NotImplementedError, RCAS::Unsupported, DomainError
202
+ nil
203
+ end
204
+
205
+ # ---- from a ratio to a term ---------------------------------------------------
206
+
207
+ # The term u(n) = f(q**n) with f(q*x)/f(x) = ratio: every factor
208
+ # (1 - a*x) of the ratio contributes a q-Pochhammer symbol (a; q)_n, a
209
+ # factor x contributes q**(n*(n - 1)/2), and what is left of the constant
210
+ # contributes z**n. nil when a factor is not linear in x.
211
+ def term(ratio, x, q, n)
212
+ pair = Fraction.as_fraction(ratio, [x.name, q.name] | ratio.variables.to_a) or return nil
213
+ numerator, denominator = pair
214
+ parts = [[numerator, 1], [denominator, -1]]
215
+ constant = Num.new(1)
216
+ total = Num.new(1)
217
+ parts.each do |poly, sign|
218
+ pieces = linear_pieces(poly, x, q) or return nil
219
+ pieces.each do |kind, value|
220
+ case kind
221
+ when :constant then constant = sign.positive? ? constant * value : constant / value
222
+ when :power then total *= Simplify.power_node(q, ((n * (n - 1) / 2) * sign * value).simplify)
223
+ when :pochhammer
224
+ factor = pochhammer(value, q, n)
225
+ total = sign.positive? ? total * factor : total / factor
226
+ end
227
+ end
228
+ end
229
+ (Simplify.power_node(constant.simplify, n) * total).simplify
230
+ rescue DomainError, ZeroDivisionError, NotImplementedError, RCAS::Unsupported
231
+ nil
232
+ end
233
+
234
+ # (a; q)_n, except that (q**(-m); q)_n vanishes from n = m + 1 on: start
235
+ # the product past the zero instead, as Petkovsek's (n - 1)! does for
236
+ # u(n + 1) = n*u(n).
237
+ def pochhammer(a, q, n)
238
+ m = power_of_q(a, q)
239
+ return Fn.new(:qpochhammer, [a, q, n]) if m.nil? || m.positive?
240
+ Fn.new(:qpochhammer, [q, q, (n + m - 1).simplify])
241
+ end
242
+
243
+ # A polynomial in x as constants, powers of x and factors 1 - a*x.
244
+ def linear_pieces(poly, x, q)
245
+ pieces = []
246
+ factorization = poly.factor
247
+ pieces << [:constant, factorization.unit] unless Scalar.one?(factorization.unit)
248
+ factorization.factors.each do |factor, multiplicity|
249
+ degree = factor.degree(x.name)
250
+ case degree
251
+ when 0 then pieces << [:constant, Simplify.power_node(factor.to_expr, multiplicity)]
252
+ when 1
253
+ low = factor.coefficient_in(x.name, 0).to_expr
254
+ high = factor.coefficient_in(x.name, 1).to_expr
255
+ if Scalar.zero?(low) # a bare x
256
+ pieces << [:power, multiplicity]
257
+ pieces << [:constant, Simplify.power_node(high, multiplicity)]
258
+ else
259
+ multiplicity.times { pieces << [:pochhammer, Simplify.negate(high / low).cancel] }
260
+ pieces << [:constant, Simplify.power_node(low, multiplicity)]
261
+ end
262
+ else return nil
263
+ end
264
+ end
265
+ pieces
266
+ rescue NotImplementedError, RCAS::Unsupported, DomainError
267
+ nil
268
+ end
269
+
270
+ # ---- shared helpers ----------------------------------------------------------
271
+
272
+ # Every divisor of +poly+ that involves x, and 1.
273
+ def divisors(poly, x)
274
+ return [poly.ring.one] if poly.degree(x.name).zero?
275
+ list = [poly.ring.one]
276
+ poly.factor.factors.each do |factor, multiplicity|
277
+ next if factor.degree(x.name).zero? # a unit of QQ(q)[x]
278
+ list = list.flat_map { |d| (0..multiplicity).map { |i| d * factor**i } }
279
+ return [poly.ring.one] if list.size > MAX_DIVISORS
280
+ end
281
+ list.uniq { |d| d.to_expr.to_s }
282
+ rescue NotImplementedError, RCAS::Unsupported, DomainError
283
+ [poly.ring.one]
284
+ end
285
+
286
+ # d with value = q**d, or nil.
287
+ def power_of_q(value, q)
288
+ value = Expression.lift(value).simplify
289
+ return 0 if Scalar.one?(value)
290
+ coefficient, factors = Simplify.factorize(value)
291
+ return nil unless coefficient == 1 && factors.size == 1 && factors.key?(q)
292
+ d = factors[q]
293
+ d.is_a?(Integer) ? d : nil
294
+ end
295
+ end
296
+ end