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
data/lib/rcas/printer.rb
ADDED
|
@@ -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
|
data/lib/rcas/product.rb
ADDED
|
@@ -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
|