Spaces:
Sleeping
Sleeping
""" | |
Expand Hypergeometric (and Meijer G) functions into named | |
special functions. | |
The algorithm for doing this uses a collection of lookup tables of | |
hypergeometric functions, and various of their properties, to expand | |
many hypergeometric functions in terms of special functions. | |
It is based on the following paper: | |
Kelly B. Roach. Meijer G Function Representations. | |
In: Proceedings of the 1997 International Symposium on Symbolic and | |
Algebraic Computation, pages 205-211, New York, 1997. ACM. | |
It is described in great(er) detail in the Sphinx documentation. | |
""" | |
# SUMMARY OF EXTENSIONS FOR MEIJER G FUNCTIONS | |
# | |
# o z**rho G(ap, bq; z) = G(ap + rho, bq + rho; z) | |
# | |
# o denote z*d/dz by D | |
# | |
# o It is helpful to keep in mind that ap and bq play essentially symmetric | |
# roles: G(1/z) has slightly altered parameters, with ap and bq interchanged. | |
# | |
# o There are four shift operators: | |
# A_J = b_J - D, J = 1, ..., n | |
# B_J = 1 - a_j + D, J = 1, ..., m | |
# C_J = -b_J + D, J = m+1, ..., q | |
# D_J = a_J - 1 - D, J = n+1, ..., p | |
# | |
# A_J, C_J increment b_J | |
# B_J, D_J decrement a_J | |
# | |
# o The corresponding four inverse-shift operators are defined if there | |
# is no cancellation. Thus e.g. an index a_J (upper or lower) can be | |
# incremented if a_J != b_i for i = 1, ..., q. | |
# | |
# o Order reduction: if b_j - a_i is a non-negative integer, where | |
# j <= m and i > n, the corresponding quotient of gamma functions reduces | |
# to a polynomial. Hence the G function can be expressed using a G-function | |
# of lower order. | |
# Similarly if j > m and i <= n. | |
# | |
# Secondly, there are paired index theorems [Adamchik, The evaluation of | |
# integrals of Bessel functions via G-function identities]. Suppose there | |
# are three parameters a, b, c, where a is an a_i, i <= n, b is a b_j, | |
# j <= m and c is a denominator parameter (i.e. a_i, i > n or b_j, j > m). | |
# Suppose further all three differ by integers. | |
# Then the order can be reduced. | |
# TODO work this out in detail. | |
# | |
# o An index quadruple is called suitable if its order cannot be reduced. | |
# If there exists a sequence of shift operators transforming one index | |
# quadruple into another, we say one is reachable from the other. | |
# | |
# o Deciding if one index quadruple is reachable from another is tricky. For | |
# this reason, we use hand-built routines to match and instantiate formulas. | |
# | |
from collections import defaultdict | |
from itertools import product | |
from functools import reduce | |
from math import prod | |
from sympy import SYMPY_DEBUG | |
from sympy.core import (S, Dummy, symbols, sympify, Tuple, expand, I, pi, Mul, | |
EulerGamma, oo, zoo, expand_func, Add, nan, Expr, Rational) | |
from sympy.core.mod import Mod | |
from sympy.core.sorting import default_sort_key | |
from sympy.functions import (exp, sqrt, root, log, lowergamma, cos, | |
besseli, gamma, uppergamma, expint, erf, sin, besselj, Ei, Ci, Si, Shi, | |
sinh, cosh, Chi, fresnels, fresnelc, polar_lift, exp_polar, floor, ceiling, | |
rf, factorial, lerchphi, Piecewise, re, elliptic_k, elliptic_e) | |
from sympy.functions.elementary.complexes import polarify, unpolarify | |
from sympy.functions.special.hyper import (hyper, HyperRep_atanh, | |
HyperRep_power1, HyperRep_power2, HyperRep_log1, HyperRep_asin1, | |
HyperRep_asin2, HyperRep_sqrts1, HyperRep_sqrts2, HyperRep_log2, | |
HyperRep_cosasin, HyperRep_sinasin, meijerg) | |
from sympy.matrices import Matrix, eye, zeros | |
from sympy.polys import apart, poly, Poly | |
from sympy.series import residue | |
from sympy.simplify.powsimp import powdenest | |
from sympy.utilities.iterables import sift | |
# function to define "buckets" | |
def _mod1(x): | |
# TODO see if this can work as Mod(x, 1); this will require | |
# different handling of the "buckets" since these need to | |
# be sorted and that fails when there is a mixture of | |
# integers and expressions with parameters. With the current | |
# Mod behavior, Mod(k, 1) == Mod(1, 1) == 0 if k is an integer. | |
# Although the sorting can be done with Basic.compare, this may | |
# still require different handling of the sorted buckets. | |
if x.is_Number: | |
return Mod(x, 1) | |
c, x = x.as_coeff_Add() | |
return Mod(c, 1) + x | |
# leave add formulae at the top for easy reference | |
def add_formulae(formulae): | |
""" Create our knowledge base. """ | |
a, b, c, z = symbols('a b c, z', cls=Dummy) | |
def add(ap, bq, res): | |
func = Hyper_Function(ap, bq) | |
formulae.append(Formula(func, z, res, (a, b, c))) | |
def addb(ap, bq, B, C, M): | |
func = Hyper_Function(ap, bq) | |
formulae.append(Formula(func, z, None, (a, b, c), B, C, M)) | |
# Luke, Y. L. (1969), The Special Functions and Their Approximations, | |
# Volume 1, section 6.2 | |
# 0F0 | |
add((), (), exp(z)) | |
# 1F0 | |
add((a, ), (), HyperRep_power1(-a, z)) | |
# 2F1 | |
addb((a, a - S.Half), (2*a, ), | |
Matrix([HyperRep_power2(a, z), | |
HyperRep_power2(a + S.Half, z)/2]), | |
Matrix([[1, 0]]), | |
Matrix([[(a - S.Half)*z/(1 - z), (S.Half - a)*z/(1 - z)], | |
[a/(1 - z), a*(z - 2)/(1 - z)]])) | |
addb((1, 1), (2, ), | |
Matrix([HyperRep_log1(z), 1]), Matrix([[-1/z, 0]]), | |
Matrix([[0, z/(z - 1)], [0, 0]])) | |
addb((S.Half, 1), (S('3/2'), ), | |
Matrix([HyperRep_atanh(z), 1]), | |
Matrix([[1, 0]]), | |
Matrix([[Rational(-1, 2), 1/(1 - z)/2], [0, 0]])) | |
addb((S.Half, S.Half), (S('3/2'), ), | |
Matrix([HyperRep_asin1(z), HyperRep_power1(Rational(-1, 2), z)]), | |
Matrix([[1, 0]]), | |
Matrix([[Rational(-1, 2), S.Half], [0, z/(1 - z)/2]])) | |
addb((a, S.Half + a), (S.Half, ), | |
Matrix([HyperRep_sqrts1(-a, z), -HyperRep_sqrts2(-a - S.Half, z)]), | |
Matrix([[1, 0]]), | |
Matrix([[0, -a], | |
[z*(-2*a - 1)/2/(1 - z), S.Half - z*(-2*a - 1)/(1 - z)]])) | |
# A. P. Prudnikov, Yu. A. Brychkov and O. I. Marichev (1990). | |
# Integrals and Series: More Special Functions, Vol. 3,. | |
# Gordon and Breach Science Publisher | |
addb([a, -a], [S.Half], | |
Matrix([HyperRep_cosasin(a, z), HyperRep_sinasin(a, z)]), | |
Matrix([[1, 0]]), | |
Matrix([[0, -a], [a*z/(1 - z), 1/(1 - z)/2]])) | |
addb([1, 1], [3*S.Half], | |
Matrix([HyperRep_asin2(z), 1]), Matrix([[1, 0]]), | |
Matrix([[(z - S.Half)/(1 - z), 1/(1 - z)/2], [0, 0]])) | |
# Complete elliptic integrals K(z) and E(z), both a 2F1 function | |
addb([S.Half, S.Half], [S.One], | |
Matrix([elliptic_k(z), elliptic_e(z)]), | |
Matrix([[2/pi, 0]]), | |
Matrix([[Rational(-1, 2), -1/(2*z-2)], | |
[Rational(-1, 2), S.Half]])) | |
addb([Rational(-1, 2), S.Half], [S.One], | |
Matrix([elliptic_k(z), elliptic_e(z)]), | |
Matrix([[0, 2/pi]]), | |
Matrix([[Rational(-1, 2), -1/(2*z-2)], | |
[Rational(-1, 2), S.Half]])) | |
# 3F2 | |
addb([Rational(-1, 2), 1, 1], [S.Half, 2], | |
Matrix([z*HyperRep_atanh(z), HyperRep_log1(z), 1]), | |
Matrix([[Rational(-2, 3), -S.One/(3*z), Rational(2, 3)]]), | |
Matrix([[S.Half, 0, z/(1 - z)/2], | |
[0, 0, z/(z - 1)], | |
[0, 0, 0]])) | |
# actually the formula for 3/2 is much nicer ... | |
addb([Rational(-1, 2), 1, 1], [2, 2], | |
Matrix([HyperRep_power1(S.Half, z), HyperRep_log2(z), 1]), | |
Matrix([[Rational(4, 9) - 16/(9*z), 4/(3*z), 16/(9*z)]]), | |
Matrix([[z/2/(z - 1), 0, 0], [1/(2*(z - 1)), 0, S.Half], [0, 0, 0]])) | |
# 1F1 | |
addb([1], [b], Matrix([z**(1 - b) * exp(z) * lowergamma(b - 1, z), 1]), | |
Matrix([[b - 1, 0]]), Matrix([[1 - b + z, 1], [0, 0]])) | |
addb([a], [2*a], | |
Matrix([z**(S.Half - a)*exp(z/2)*besseli(a - S.Half, z/2) | |
* gamma(a + S.Half)/4**(S.Half - a), | |
z**(S.Half - a)*exp(z/2)*besseli(a + S.Half, z/2) | |
* gamma(a + S.Half)/4**(S.Half - a)]), | |
Matrix([[1, 0]]), | |
Matrix([[z/2, z/2], [z/2, (z/2 - 2*a)]])) | |
mz = polar_lift(-1)*z | |
addb([a], [a + 1], | |
Matrix([mz**(-a)*a*lowergamma(a, mz), a*exp(z)]), | |
Matrix([[1, 0]]), | |
Matrix([[-a, 1], [0, z]])) | |
# This one is redundant. | |
add([Rational(-1, 2)], [S.Half], exp(z) - sqrt(pi*z)*(-I)*erf(I*sqrt(z))) | |
# Added to get nice results for Laplace transform of Fresnel functions | |
# https://functions.wolfram.com/07.22.03.6437.01 | |
# Basic rule | |
#add([1], [Rational(3, 4), Rational(5, 4)], | |
# sqrt(pi) * (cos(2*sqrt(polar_lift(-1)*z))*fresnelc(2*root(polar_lift(-1)*z,4)/sqrt(pi)) + | |
# sin(2*sqrt(polar_lift(-1)*z))*fresnels(2*root(polar_lift(-1)*z,4)/sqrt(pi))) | |
# / (2*root(polar_lift(-1)*z,4))) | |
# Manually tuned rule | |
addb([1], [Rational(3, 4), Rational(5, 4)], | |
Matrix([ sqrt(pi)*(I*sinh(2*sqrt(z))*fresnels(2*root(z, 4)*exp(I*pi/4)/sqrt(pi)) | |
+ cosh(2*sqrt(z))*fresnelc(2*root(z, 4)*exp(I*pi/4)/sqrt(pi))) | |
* exp(-I*pi/4)/(2*root(z, 4)), | |
sqrt(pi)*root(z, 4)*(sinh(2*sqrt(z))*fresnelc(2*root(z, 4)*exp(I*pi/4)/sqrt(pi)) | |
+ I*cosh(2*sqrt(z))*fresnels(2*root(z, 4)*exp(I*pi/4)/sqrt(pi))) | |
*exp(-I*pi/4)/2, | |
1 ]), | |
Matrix([[1, 0, 0]]), | |
Matrix([[Rational(-1, 4), 1, Rational(1, 4)], | |
[ z, Rational(1, 4), 0], | |
[ 0, 0, 0]])) | |
# 2F2 | |
addb([S.Half, a], [Rational(3, 2), a + 1], | |
Matrix([a/(2*a - 1)*(-I)*sqrt(pi/z)*erf(I*sqrt(z)), | |
a/(2*a - 1)*(polar_lift(-1)*z)**(-a)* | |
lowergamma(a, polar_lift(-1)*z), | |
a/(2*a - 1)*exp(z)]), | |
Matrix([[1, -1, 0]]), | |
Matrix([[Rational(-1, 2), 0, 1], [0, -a, 1], [0, 0, z]])) | |
# We make a "basis" of four functions instead of three, and give EulerGamma | |
# an extra slot (it could just be a coefficient to 1). The advantage is | |
# that this way Polys will not see multivariate polynomials (it treats | |
# EulerGamma as an indeterminate), which is *way* faster. | |
addb([1, 1], [2, 2], | |
Matrix([Ei(z) - log(z), exp(z), 1, EulerGamma]), | |
Matrix([[1/z, 0, 0, -1/z]]), | |
Matrix([[0, 1, -1, 0], [0, z, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])) | |
# 0F1 | |
add((), (S.Half, ), cosh(2*sqrt(z))) | |
addb([], [b], | |
Matrix([gamma(b)*z**((1 - b)/2)*besseli(b - 1, 2*sqrt(z)), | |
gamma(b)*z**(1 - b/2)*besseli(b, 2*sqrt(z))]), | |
Matrix([[1, 0]]), Matrix([[0, 1], [z, (1 - b)]])) | |
# 0F3 | |
x = 4*z**Rational(1, 4) | |
def fp(a, z): | |
return besseli(a, x) + besselj(a, x) | |
def fm(a, z): | |
return besseli(a, x) - besselj(a, x) | |
# TODO branching | |
addb([], [S.Half, a, a + S.Half], | |
Matrix([fp(2*a - 1, z), fm(2*a, z)*z**Rational(1, 4), | |
fm(2*a - 1, z)*sqrt(z), fp(2*a, z)*z**Rational(3, 4)]) | |
* 2**(-2*a)*gamma(2*a)*z**((1 - 2*a)/4), | |
Matrix([[1, 0, 0, 0]]), | |
Matrix([[0, 1, 0, 0], | |
[0, S.Half - a, 1, 0], | |
[0, 0, S.Half, 1], | |
[z, 0, 0, 1 - a]])) | |
x = 2*(4*z)**Rational(1, 4)*exp_polar(I*pi/4) | |
addb([], [a, a + S.Half, 2*a], | |
(2*sqrt(polar_lift(-1)*z))**(1 - 2*a)*gamma(2*a)**2 * | |
Matrix([besselj(2*a - 1, x)*besseli(2*a - 1, x), | |
x*(besseli(2*a, x)*besselj(2*a - 1, x) | |
- besseli(2*a - 1, x)*besselj(2*a, x)), | |
x**2*besseli(2*a, x)*besselj(2*a, x), | |
x**3*(besseli(2*a, x)*besselj(2*a - 1, x) | |
+ besseli(2*a - 1, x)*besselj(2*a, x))]), | |
Matrix([[1, 0, 0, 0]]), | |
Matrix([[0, Rational(1, 4), 0, 0], | |
[0, (1 - 2*a)/2, Rational(-1, 2), 0], | |
[0, 0, 1 - 2*a, Rational(1, 4)], | |
[-32*z, 0, 0, 1 - a]])) | |
# 1F2 | |
addb([a], [a - S.Half, 2*a], | |
Matrix([z**(S.Half - a)*besseli(a - S.Half, sqrt(z))**2, | |
z**(1 - a)*besseli(a - S.Half, sqrt(z)) | |
*besseli(a - Rational(3, 2), sqrt(z)), | |
z**(Rational(3, 2) - a)*besseli(a - Rational(3, 2), sqrt(z))**2]), | |
Matrix([[-gamma(a + S.Half)**2/4**(S.Half - a), | |
2*gamma(a - S.Half)*gamma(a + S.Half)/4**(1 - a), | |
0]]), | |
Matrix([[1 - 2*a, 1, 0], [z/2, S.Half - a, S.Half], [0, z, 0]])) | |
addb([S.Half], [b, 2 - b], | |
pi*(1 - b)/sin(pi*b)* | |
Matrix([besseli(1 - b, sqrt(z))*besseli(b - 1, sqrt(z)), | |
sqrt(z)*(besseli(-b, sqrt(z))*besseli(b - 1, sqrt(z)) | |
+ besseli(1 - b, sqrt(z))*besseli(b, sqrt(z))), | |
besseli(-b, sqrt(z))*besseli(b, sqrt(z))]), | |
Matrix([[1, 0, 0]]), | |
Matrix([[b - 1, S.Half, 0], | |
[z, 0, z], | |
[0, S.Half, -b]])) | |
addb([S.Half], [Rational(3, 2), Rational(3, 2)], | |
Matrix([Shi(2*sqrt(z))/2/sqrt(z), sinh(2*sqrt(z))/2/sqrt(z), | |
cosh(2*sqrt(z))]), | |
Matrix([[1, 0, 0]]), | |
Matrix([[Rational(-1, 2), S.Half, 0], [0, Rational(-1, 2), S.Half], [0, 2*z, 0]])) | |
# FresnelS | |
# Basic rule | |
#add([Rational(3, 4)], [Rational(3, 2),Rational(7, 4)], 6*fresnels( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) / ( pi * (exp(pi*I/4)*root(z,4)*2/sqrt(pi))**3 ) ) | |
# Manually tuned rule | |
addb([Rational(3, 4)], [Rational(3, 2), Rational(7, 4)], | |
Matrix( | |
[ fresnels( | |
exp( | |
pi*I/4)*root( | |
z, 4)*2/sqrt( | |
pi) ) / ( | |
pi * (exp(pi*I/4)*root(z, 4)*2/sqrt(pi))**3 ), | |
sinh(2*sqrt(z))/sqrt(z), | |
cosh(2*sqrt(z)) ]), | |
Matrix([[6, 0, 0]]), | |
Matrix([[Rational(-3, 4), Rational(1, 16), 0], | |
[ 0, Rational(-1, 2), 1], | |
[ 0, z, 0]])) | |
# FresnelC | |
# Basic rule | |
#add([Rational(1, 4)], [S.Half,Rational(5, 4)], fresnelc( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) / ( exp(pi*I/4)*root(z,4)*2/sqrt(pi) ) ) | |
# Manually tuned rule | |
addb([Rational(1, 4)], [S.Half, Rational(5, 4)], | |
Matrix( | |
[ sqrt( | |
pi)*exp( | |
-I*pi/4)*fresnelc( | |
2*root(z, 4)*exp(I*pi/4)/sqrt(pi))/(2*root(z, 4)), | |
cosh(2*sqrt(z)), | |
sinh(2*sqrt(z))*sqrt(z) ]), | |
Matrix([[1, 0, 0]]), | |
Matrix([[Rational(-1, 4), Rational(1, 4), 0 ], | |
[ 0, 0, 1 ], | |
[ 0, z, S.Half]])) | |
# 2F3 | |
# XXX with this five-parameter formula is pretty slow with the current | |
# Formula.find_instantiations (creates 2!*3!*3**(2+3) ~ 3000 | |
# instantiations ... But it's not too bad. | |
addb([a, a + S.Half], [2*a, b, 2*a - b + 1], | |
gamma(b)*gamma(2*a - b + 1) * (sqrt(z)/2)**(1 - 2*a) * | |
Matrix([besseli(b - 1, sqrt(z))*besseli(2*a - b, sqrt(z)), | |
sqrt(z)*besseli(b, sqrt(z))*besseli(2*a - b, sqrt(z)), | |
sqrt(z)*besseli(b - 1, sqrt(z))*besseli(2*a - b + 1, sqrt(z)), | |
besseli(b, sqrt(z))*besseli(2*a - b + 1, sqrt(z))]), | |
Matrix([[1, 0, 0, 0]]), | |
Matrix([[0, S.Half, S.Half, 0], | |
[z/2, 1 - b, 0, z/2], | |
[z/2, 0, b - 2*a, z/2], | |
[0, S.Half, S.Half, -2*a]])) | |
# (C/f above comment about eulergamma in the basis). | |
addb([1, 1], [2, 2, Rational(3, 2)], | |
Matrix([Chi(2*sqrt(z)) - log(2*sqrt(z)), | |
cosh(2*sqrt(z)), sqrt(z)*sinh(2*sqrt(z)), 1, EulerGamma]), | |
Matrix([[1/z, 0, 0, 0, -1/z]]), | |
Matrix([[0, S.Half, 0, Rational(-1, 2), 0], | |
[0, 0, 1, 0, 0], | |
[0, z, S.Half, 0, 0], | |
[0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0]])) | |
# 3F3 | |
# This is rule: https://functions.wolfram.com/07.31.03.0134.01 | |
# Initial reason to add it was a nice solution for | |
# integrate(erf(a*z)/z**2, z) and same for erfc and erfi. | |
# Basic rule | |
# add([1, 1, a], [2, 2, a+1], (a/(z*(a-1)**2)) * | |
# (1 - (-z)**(1-a) * (gamma(a) - uppergamma(a,-z)) | |
# - (a-1) * (EulerGamma + uppergamma(0,-z) + log(-z)) | |
# - exp(z))) | |
# Manually tuned rule | |
addb([1, 1, a], [2, 2, a+1], | |
Matrix([a*(log(-z) + expint(1, -z) + EulerGamma)/(z*(a**2 - 2*a + 1)), | |
a*(-z)**(-a)*(gamma(a) - uppergamma(a, -z))/(a - 1)**2, | |
a*exp(z)/(a**2 - 2*a + 1), | |
a/(z*(a**2 - 2*a + 1))]), | |
Matrix([[1-a, 1, -1/z, 1]]), | |
Matrix([[-1,0,-1/z,1], | |
[0,-a,1,0], | |
[0,0,z,0], | |
[0,0,0,-1]])) | |
def add_meijerg_formulae(formulae): | |
a, b, c, z = list(map(Dummy, 'abcz')) | |
rho = Dummy('rho') | |
def add(an, ap, bm, bq, B, C, M, matcher): | |
formulae.append(MeijerFormula(an, ap, bm, bq, z, [a, b, c, rho], | |
B, C, M, matcher)) | |
def detect_uppergamma(func): | |
x = func.an[0] | |
y, z = func.bm | |
swapped = False | |
if not _mod1((x - y).simplify()): | |
swapped = True | |
(y, z) = (z, y) | |
if _mod1((x - z).simplify()) or x - z > 0: | |
return None | |
l = [y, x] | |
if swapped: | |
l = [x, y] | |
return {rho: y, a: x - y}, G_Function([x], [], l, []) | |
add([a + rho], [], [rho, a + rho], [], | |
Matrix([gamma(1 - a)*z**rho*exp(z)*uppergamma(a, z), | |
gamma(1 - a)*z**(a + rho)]), | |
Matrix([[1, 0]]), | |
Matrix([[rho + z, -1], [0, a + rho]]), | |
detect_uppergamma) | |
def detect_3113(func): | |
"""https://functions.wolfram.com/07.34.03.0984.01""" | |
x = func.an[0] | |
u, v, w = func.bm | |
if _mod1((u - v).simplify()) == 0: | |
if _mod1((v - w).simplify()) == 0: | |
return | |
sig = (S.Half, S.Half, S.Zero) | |
x1, x2, y = u, v, w | |
else: | |
if _mod1((x - u).simplify()) == 0: | |
sig = (S.Half, S.Zero, S.Half) | |
x1, y, x2 = u, v, w | |
else: | |
sig = (S.Zero, S.Half, S.Half) | |
y, x1, x2 = u, v, w | |
if (_mod1((x - x1).simplify()) != 0 or | |
_mod1((x - x2).simplify()) != 0 or | |
_mod1((x - y).simplify()) != S.Half or | |
x - x1 > 0 or x - x2 > 0): | |
return | |
return {a: x}, G_Function([x], [], [x - S.Half + t for t in sig], []) | |
s = sin(2*sqrt(z)) | |
c_ = cos(2*sqrt(z)) | |
S_ = Si(2*sqrt(z)) - pi/2 | |
C = Ci(2*sqrt(z)) | |
add([a], [], [a, a, a - S.Half], [], | |
Matrix([sqrt(pi)*z**(a - S.Half)*(c_*S_ - s*C), | |
sqrt(pi)*z**a*(s*S_ + c_*C), | |
sqrt(pi)*z**a]), | |
Matrix([[-2, 0, 0]]), | |
Matrix([[a - S.Half, -1, 0], [z, a, S.Half], [0, 0, a]]), | |
detect_3113) | |
def make_simp(z): | |
""" Create a function that simplifies rational functions in ``z``. """ | |
def simp(expr): | |
""" Efficiently simplify the rational function ``expr``. """ | |
numer, denom = expr.as_numer_denom() | |
numer = numer.expand() | |
# denom = denom.expand() # is this needed? | |
c, numer, denom = poly(numer, z).cancel(poly(denom, z)) | |
return c * numer.as_expr() / denom.as_expr() | |
return simp | |
def debug(*args): | |
if SYMPY_DEBUG: | |
for a in args: | |
print(a, end="") | |
print() | |
class Hyper_Function(Expr): | |
""" A generalized hypergeometric function. """ | |
def __new__(cls, ap, bq): | |
obj = super().__new__(cls) | |
obj.ap = Tuple(*list(map(expand, ap))) | |
obj.bq = Tuple(*list(map(expand, bq))) | |
return obj | |
def args(self): | |
return (self.ap, self.bq) | |
def sizes(self): | |
return (len(self.ap), len(self.bq)) | |
def gamma(self): | |
""" | |
Number of upper parameters that are negative integers | |
This is a transformation invariant. | |
""" | |
return sum(bool(x.is_integer and x.is_negative) for x in self.ap) | |
def _hashable_content(self): | |
return super()._hashable_content() + (self.ap, | |
self.bq) | |
def __call__(self, arg): | |
return hyper(self.ap, self.bq, arg) | |
def build_invariants(self): | |
""" | |
Compute the invariant vector. | |
Explanation | |
=========== | |
The invariant vector is: | |
(gamma, ((s1, n1), ..., (sk, nk)), ((t1, m1), ..., (tr, mr))) | |
where gamma is the number of integer a < 0, | |
s1 < ... < sk | |
nl is the number of parameters a_i congruent to sl mod 1 | |
t1 < ... < tr | |
ml is the number of parameters b_i congruent to tl mod 1 | |
If the index pair contains parameters, then this is not truly an | |
invariant, since the parameters cannot be sorted uniquely mod1. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import Hyper_Function | |
>>> from sympy import S | |
>>> ap = (S.Half, S.One/3, S(-1)/2, -2) | |
>>> bq = (1, 2) | |
Here gamma = 1, | |
k = 3, s1 = 0, s2 = 1/3, s3 = 1/2 | |
n1 = 1, n2 = 1, n2 = 2 | |
r = 1, t1 = 0 | |
m1 = 2: | |
>>> Hyper_Function(ap, bq).build_invariants() | |
(1, ((0, 1), (1/3, 1), (1/2, 2)), ((0, 2),)) | |
""" | |
abuckets, bbuckets = sift(self.ap, _mod1), sift(self.bq, _mod1) | |
def tr(bucket): | |
bucket = list(bucket.items()) | |
if not any(isinstance(x[0], Mod) for x in bucket): | |
bucket.sort(key=lambda x: default_sort_key(x[0])) | |
bucket = tuple([(mod, len(values)) for mod, values in bucket if | |
values]) | |
return bucket | |
return (self.gamma, tr(abuckets), tr(bbuckets)) | |
def difficulty(self, func): | |
""" Estimate how many steps it takes to reach ``func`` from self. | |
Return -1 if impossible. """ | |
if self.gamma != func.gamma: | |
return -1 | |
oabuckets, obbuckets, abuckets, bbuckets = [sift(params, _mod1) for | |
params in (self.ap, self.bq, func.ap, func.bq)] | |
diff = 0 | |
for bucket, obucket in [(abuckets, oabuckets), (bbuckets, obbuckets)]: | |
for mod in set(list(bucket.keys()) + list(obucket.keys())): | |
if (mod not in bucket) or (mod not in obucket) \ | |
or len(bucket[mod]) != len(obucket[mod]): | |
return -1 | |
l1 = list(bucket[mod]) | |
l2 = list(obucket[mod]) | |
l1.sort() | |
l2.sort() | |
for i, j in zip(l1, l2): | |
diff += abs(i - j) | |
return diff | |
def _is_suitable_origin(self): | |
""" | |
Decide if ``self`` is a suitable origin. | |
Explanation | |
=========== | |
A function is a suitable origin iff: | |
* none of the ai equals bj + n, with n a non-negative integer | |
* none of the ai is zero | |
* none of the bj is a non-positive integer | |
Note that this gives meaningful results only when none of the indices | |
are symbolic. | |
""" | |
for a in self.ap: | |
for b in self.bq: | |
if (a - b).is_integer and (a - b).is_negative is False: | |
return False | |
for a in self.ap: | |
if a == 0: | |
return False | |
for b in self.bq: | |
if b.is_integer and b.is_nonpositive: | |
return False | |
return True | |
class G_Function(Expr): | |
""" A Meijer G-function. """ | |
def __new__(cls, an, ap, bm, bq): | |
obj = super().__new__(cls) | |
obj.an = Tuple(*list(map(expand, an))) | |
obj.ap = Tuple(*list(map(expand, ap))) | |
obj.bm = Tuple(*list(map(expand, bm))) | |
obj.bq = Tuple(*list(map(expand, bq))) | |
return obj | |
def args(self): | |
return (self.an, self.ap, self.bm, self.bq) | |
def _hashable_content(self): | |
return super()._hashable_content() + self.args | |
def __call__(self, z): | |
return meijerg(self.an, self.ap, self.bm, self.bq, z) | |
def compute_buckets(self): | |
""" | |
Compute buckets for the fours sets of parameters. | |
Explanation | |
=========== | |
We guarantee that any two equal Mod objects returned are actually the | |
same, and that the buckets are sorted by real part (an and bq | |
descendending, bm and ap ascending). | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import G_Function | |
>>> from sympy.abc import y | |
>>> from sympy import S | |
>>> a, b = [1, 3, 2, S(3)/2], [1 + y, y, 2, y + 3] | |
>>> G_Function(a, b, [2], [y]).compute_buckets() | |
({0: [3, 2, 1], 1/2: [3/2]}, | |
{0: [2], y: [y, y + 1, y + 3]}, {0: [2]}, {y: [y]}) | |
""" | |
dicts = pan, pap, pbm, pbq = [defaultdict(list) for i in range(4)] | |
for dic, lis in zip(dicts, (self.an, self.ap, self.bm, self.bq)): | |
for x in lis: | |
dic[_mod1(x)].append(x) | |
for dic, flip in zip(dicts, (True, False, False, True)): | |
for m, items in dic.items(): | |
x0 = items[0] | |
items.sort(key=lambda x: x - x0, reverse=flip) | |
dic[m] = items | |
return tuple([dict(w) for w in dicts]) | |
def signature(self): | |
return (len(self.an), len(self.ap), len(self.bm), len(self.bq)) | |
# Dummy variable. | |
_x = Dummy('x') | |
class Formula: | |
""" | |
This class represents hypergeometric formulae. | |
Explanation | |
=========== | |
Its data members are: | |
- z, the argument | |
- closed_form, the closed form expression | |
- symbols, the free symbols (parameters) in the formula | |
- func, the function | |
- B, C, M (see _compute_basis) | |
Examples | |
======== | |
>>> from sympy.abc import a, b, z | |
>>> from sympy.simplify.hyperexpand import Formula, Hyper_Function | |
>>> func = Hyper_Function((a/2, a/3 + b, (1+a)/2), (a, b, (a+b)/7)) | |
>>> f = Formula(func, z, None, [a, b]) | |
""" | |
def _compute_basis(self, closed_form): | |
""" | |
Compute a set of functions B=(f1, ..., fn), a nxn matrix M | |
and a 1xn matrix C such that: | |
closed_form = C B | |
z d/dz B = M B. | |
""" | |
afactors = [_x + a for a in self.func.ap] | |
bfactors = [_x + b - 1 for b in self.func.bq] | |
expr = _x*Mul(*bfactors) - self.z*Mul(*afactors) | |
poly = Poly(expr, _x) | |
n = poly.degree() - 1 | |
b = [closed_form] | |
for _ in range(n): | |
b.append(self.z*b[-1].diff(self.z)) | |
self.B = Matrix(b) | |
self.C = Matrix([[1] + [0]*n]) | |
m = eye(n) | |
m = m.col_insert(0, zeros(n, 1)) | |
l = poly.all_coeffs()[1:] | |
l.reverse() | |
self.M = m.row_insert(n, -Matrix([l])/poly.all_coeffs()[0]) | |
def __init__(self, func, z, res, symbols, B=None, C=None, M=None): | |
z = sympify(z) | |
res = sympify(res) | |
symbols = [x for x in sympify(symbols) if func.has(x)] | |
self.z = z | |
self.symbols = symbols | |
self.B = B | |
self.C = C | |
self.M = M | |
self.func = func | |
# TODO with symbolic parameters, it could be advantageous | |
# (for prettier answers) to compute a basis only *after* | |
# instantiation | |
if res is not None: | |
self._compute_basis(res) | |
def closed_form(self): | |
return reduce(lambda s,m: s+m[0]*m[1], zip(self.C, self.B), S.Zero) | |
def find_instantiations(self, func): | |
""" | |
Find substitutions of the free symbols that match ``func``. | |
Return the substitution dictionaries as a list. Note that the returned | |
instantiations need not actually match, or be valid! | |
""" | |
from sympy.solvers import solve | |
ap = func.ap | |
bq = func.bq | |
if len(ap) != len(self.func.ap) or len(bq) != len(self.func.bq): | |
raise TypeError('Cannot instantiate other number of parameters') | |
symbol_values = [] | |
for a in self.symbols: | |
if a in self.func.ap.args: | |
symbol_values.append(ap) | |
elif a in self.func.bq.args: | |
symbol_values.append(bq) | |
else: | |
raise ValueError("At least one of the parameters of the " | |
"formula must be equal to %s" % (a,)) | |
base_repl = [dict(list(zip(self.symbols, values))) | |
for values in product(*symbol_values)] | |
abuckets, bbuckets = [sift(params, _mod1) for params in [ap, bq]] | |
a_inv, b_inv = [{a: len(vals) for a, vals in bucket.items()} | |
for bucket in [abuckets, bbuckets]] | |
critical_values = [[0] for _ in self.symbols] | |
result = [] | |
_n = Dummy() | |
for repl in base_repl: | |
symb_a, symb_b = [sift(params, lambda x: _mod1(x.xreplace(repl))) | |
for params in [self.func.ap, self.func.bq]] | |
for bucket, obucket in [(abuckets, symb_a), (bbuckets, symb_b)]: | |
for mod in set(list(bucket.keys()) + list(obucket.keys())): | |
if (mod not in bucket) or (mod not in obucket) \ | |
or len(bucket[mod]) != len(obucket[mod]): | |
break | |
for a, vals in zip(self.symbols, critical_values): | |
if repl[a].free_symbols: | |
continue | |
exprs = [expr for expr in obucket[mod] if expr.has(a)] | |
repl0 = repl.copy() | |
repl0[a] += _n | |
for expr in exprs: | |
for target in bucket[mod]: | |
n0, = solve(expr.xreplace(repl0) - target, _n) | |
if n0.free_symbols: | |
raise ValueError("Value should not be true") | |
vals.append(n0) | |
else: | |
values = [] | |
for a, vals in zip(self.symbols, critical_values): | |
a0 = repl[a] | |
min_ = floor(min(vals)) | |
max_ = ceiling(max(vals)) | |
values.append([a0 + n for n in range(min_, max_ + 1)]) | |
result.extend(dict(list(zip(self.symbols, l))) for l in product(*values)) | |
return result | |
class FormulaCollection: | |
""" A collection of formulae to use as origins. """ | |
def __init__(self): | |
""" Doing this globally at module init time is a pain ... """ | |
self.symbolic_formulae = {} | |
self.concrete_formulae = {} | |
self.formulae = [] | |
add_formulae(self.formulae) | |
# Now process the formulae into a helpful form. | |
# These dicts are indexed by (p, q). | |
for f in self.formulae: | |
sizes = f.func.sizes | |
if len(f.symbols) > 0: | |
self.symbolic_formulae.setdefault(sizes, []).append(f) | |
else: | |
inv = f.func.build_invariants() | |
self.concrete_formulae.setdefault(sizes, {})[inv] = f | |
def lookup_origin(self, func): | |
""" | |
Given the suitable target ``func``, try to find an origin in our | |
knowledge base. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import (FormulaCollection, | |
... Hyper_Function) | |
>>> f = FormulaCollection() | |
>>> f.lookup_origin(Hyper_Function((), ())).closed_form | |
exp(_z) | |
>>> f.lookup_origin(Hyper_Function([1], ())).closed_form | |
HyperRep_power1(-1, _z) | |
>>> from sympy import S | |
>>> i = Hyper_Function([S('1/4'), S('3/4 + 4')], [S.Half]) | |
>>> f.lookup_origin(i).closed_form | |
HyperRep_sqrts1(-1/4, _z) | |
""" | |
inv = func.build_invariants() | |
sizes = func.sizes | |
if sizes in self.concrete_formulae and \ | |
inv in self.concrete_formulae[sizes]: | |
return self.concrete_formulae[sizes][inv] | |
# We don't have a concrete formula. Try to instantiate. | |
if sizes not in self.symbolic_formulae: | |
return None # Too bad... | |
possible = [] | |
for f in self.symbolic_formulae[sizes]: | |
repls = f.find_instantiations(func) | |
for repl in repls: | |
func2 = f.func.xreplace(repl) | |
if not func2._is_suitable_origin(): | |
continue | |
diff = func2.difficulty(func) | |
if diff == -1: | |
continue | |
possible.append((diff, repl, f, func2)) | |
# find the nearest origin | |
possible.sort(key=lambda x: x[0]) | |
for _, repl, f, func2 in possible: | |
f2 = Formula(func2, f.z, None, [], f.B.subs(repl), | |
f.C.subs(repl), f.M.subs(repl)) | |
if not any(e.has(S.NaN, oo, -oo, zoo) for e in [f2.B, f2.M, f2.C]): | |
return f2 | |
return None | |
class MeijerFormula: | |
""" | |
This class represents a Meijer G-function formula. | |
Its data members are: | |
- z, the argument | |
- symbols, the free symbols (parameters) in the formula | |
- func, the function | |
- B, C, M (c/f ordinary Formula) | |
""" | |
def __init__(self, an, ap, bm, bq, z, symbols, B, C, M, matcher): | |
an, ap, bm, bq = [Tuple(*list(map(expand, w))) for w in [an, ap, bm, bq]] | |
self.func = G_Function(an, ap, bm, bq) | |
self.z = z | |
self.symbols = symbols | |
self._matcher = matcher | |
self.B = B | |
self.C = C | |
self.M = M | |
def closed_form(self): | |
return reduce(lambda s,m: s+m[0]*m[1], zip(self.C, self.B), S.Zero) | |
def try_instantiate(self, func): | |
""" | |
Try to instantiate the current formula to (almost) match func. | |
This uses the _matcher passed on init. | |
""" | |
if func.signature != self.func.signature: | |
return None | |
res = self._matcher(func) | |
if res is not None: | |
subs, newfunc = res | |
return MeijerFormula(newfunc.an, newfunc.ap, newfunc.bm, newfunc.bq, | |
self.z, [], | |
self.B.subs(subs), self.C.subs(subs), | |
self.M.subs(subs), None) | |
class MeijerFormulaCollection: | |
""" | |
This class holds a collection of meijer g formulae. | |
""" | |
def __init__(self): | |
formulae = [] | |
add_meijerg_formulae(formulae) | |
self.formulae = defaultdict(list) | |
for formula in formulae: | |
self.formulae[formula.func.signature].append(formula) | |
self.formulae = dict(self.formulae) | |
def lookup_origin(self, func): | |
""" Try to find a formula that matches func. """ | |
if func.signature not in self.formulae: | |
return None | |
for formula in self.formulae[func.signature]: | |
res = formula.try_instantiate(func) | |
if res is not None: | |
return res | |
class Operator: | |
""" | |
Base class for operators to be applied to our functions. | |
Explanation | |
=========== | |
These operators are differential operators. They are by convention | |
expressed in the variable D = z*d/dz (although this base class does | |
not actually care). | |
Note that when the operator is applied to an object, we typically do | |
*not* blindly differentiate but instead use a different representation | |
of the z*d/dz operator (see make_derivative_operator). | |
To subclass from this, define a __init__ method that initializes a | |
self._poly variable. This variable stores a polynomial. By convention | |
the generator is z*d/dz, and acts to the right of all coefficients. | |
Thus this poly | |
x**2 + 2*z*x + 1 | |
represents the differential operator | |
(z*d/dz)**2 + 2*z**2*d/dz. | |
This class is used only in the implementation of the hypergeometric | |
function expansion algorithm. | |
""" | |
def apply(self, obj, op): | |
""" | |
Apply ``self`` to the object ``obj``, where the generator is ``op``. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import Operator | |
>>> from sympy.polys.polytools import Poly | |
>>> from sympy.abc import x, y, z | |
>>> op = Operator() | |
>>> op._poly = Poly(x**2 + z*x + y, x) | |
>>> op.apply(z**7, lambda f: f.diff(z)) | |
y*z**7 + 7*z**7 + 42*z**5 | |
""" | |
coeffs = self._poly.all_coeffs() | |
coeffs.reverse() | |
diffs = [obj] | |
for c in coeffs[1:]: | |
diffs.append(op(diffs[-1])) | |
r = coeffs[0]*diffs[0] | |
for c, d in zip(coeffs[1:], diffs[1:]): | |
r += c*d | |
return r | |
class MultOperator(Operator): | |
""" Simply multiply by a "constant" """ | |
def __init__(self, p): | |
self._poly = Poly(p, _x) | |
class ShiftA(Operator): | |
""" Increment an upper index. """ | |
def __init__(self, ai): | |
ai = sympify(ai) | |
if ai == 0: | |
raise ValueError('Cannot increment zero upper index.') | |
self._poly = Poly(_x/ai + 1, _x) | |
def __str__(self): | |
return '<Increment upper %s.>' % (1/self._poly.all_coeffs()[0]) | |
class ShiftB(Operator): | |
""" Decrement a lower index. """ | |
def __init__(self, bi): | |
bi = sympify(bi) | |
if bi == 1: | |
raise ValueError('Cannot decrement unit lower index.') | |
self._poly = Poly(_x/(bi - 1) + 1, _x) | |
def __str__(self): | |
return '<Decrement lower %s.>' % (1/self._poly.all_coeffs()[0] + 1) | |
class UnShiftA(Operator): | |
""" Decrement an upper index. """ | |
def __init__(self, ap, bq, i, z): | |
""" Note: i counts from zero! """ | |
ap, bq, i = list(map(sympify, [ap, bq, i])) | |
self._ap = ap | |
self._bq = bq | |
self._i = i | |
ap = list(ap) | |
bq = list(bq) | |
ai = ap.pop(i) - 1 | |
if ai == 0: | |
raise ValueError('Cannot decrement unit upper index.') | |
m = Poly(z*ai, _x) | |
for a in ap: | |
m *= Poly(_x + a, _x) | |
A = Dummy('A') | |
n = D = Poly(ai*A - ai, A) | |
for b in bq: | |
n *= D + (b - 1).as_poly(A) | |
b0 = -n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot decrement upper index: ' | |
'cancels with lower') | |
n = Poly(Poly(n.all_coeffs()[:-1], A).as_expr().subs(A, _x/ai + 1), _x) | |
self._poly = Poly((n - m)/b0, _x) | |
def __str__(self): | |
return '<Decrement upper index #%s of %s, %s.>' % (self._i, | |
self._ap, self._bq) | |
class UnShiftB(Operator): | |
""" Increment a lower index. """ | |
def __init__(self, ap, bq, i, z): | |
""" Note: i counts from zero! """ | |
ap, bq, i = list(map(sympify, [ap, bq, i])) | |
self._ap = ap | |
self._bq = bq | |
self._i = i | |
ap = list(ap) | |
bq = list(bq) | |
bi = bq.pop(i) + 1 | |
if bi == 0: | |
raise ValueError('Cannot increment -1 lower index.') | |
m = Poly(_x*(bi - 1), _x) | |
for b in bq: | |
m *= Poly(_x + b - 1, _x) | |
B = Dummy('B') | |
D = Poly((bi - 1)*B - bi + 1, B) | |
n = Poly(z, B) | |
for a in ap: | |
n *= (D + a.as_poly(B)) | |
b0 = n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot increment index: cancels with upper') | |
n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( | |
B, _x/(bi - 1) + 1), _x) | |
self._poly = Poly((m - n)/b0, _x) | |
def __str__(self): | |
return '<Increment lower index #%s of %s, %s.>' % (self._i, | |
self._ap, self._bq) | |
class MeijerShiftA(Operator): | |
""" Increment an upper b index. """ | |
def __init__(self, bi): | |
bi = sympify(bi) | |
self._poly = Poly(bi - _x, _x) | |
def __str__(self): | |
return '<Increment upper b=%s.>' % (self._poly.all_coeffs()[1]) | |
class MeijerShiftB(Operator): | |
""" Decrement an upper a index. """ | |
def __init__(self, bi): | |
bi = sympify(bi) | |
self._poly = Poly(1 - bi + _x, _x) | |
def __str__(self): | |
return '<Decrement upper a=%s.>' % (1 - self._poly.all_coeffs()[1]) | |
class MeijerShiftC(Operator): | |
""" Increment a lower b index. """ | |
def __init__(self, bi): | |
bi = sympify(bi) | |
self._poly = Poly(-bi + _x, _x) | |
def __str__(self): | |
return '<Increment lower b=%s.>' % (-self._poly.all_coeffs()[1]) | |
class MeijerShiftD(Operator): | |
""" Decrement a lower a index. """ | |
def __init__(self, bi): | |
bi = sympify(bi) | |
self._poly = Poly(bi - 1 - _x, _x) | |
def __str__(self): | |
return '<Decrement lower a=%s.>' % (self._poly.all_coeffs()[1] + 1) | |
class MeijerUnShiftA(Operator): | |
""" Decrement an upper b index. """ | |
def __init__(self, an, ap, bm, bq, i, z): | |
""" Note: i counts from zero! """ | |
an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) | |
self._an = an | |
self._ap = ap | |
self._bm = bm | |
self._bq = bq | |
self._i = i | |
an = list(an) | |
ap = list(ap) | |
bm = list(bm) | |
bq = list(bq) | |
bi = bm.pop(i) - 1 | |
m = Poly(1, _x) * prod(Poly(b - _x, _x) for b in bm) * prod(Poly(_x - b, _x) for b in bq) | |
A = Dummy('A') | |
D = Poly(bi - A, A) | |
n = Poly(z, A) * prod((D + 1 - a) for a in an) * prod((-D + a - 1) for a in ap) | |
b0 = n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot decrement upper b index (cancels)') | |
n = Poly(Poly(n.all_coeffs()[:-1], A).as_expr().subs(A, bi - _x), _x) | |
self._poly = Poly((m - n)/b0, _x) | |
def __str__(self): | |
return '<Decrement upper b index #%s of %s, %s, %s, %s.>' % (self._i, | |
self._an, self._ap, self._bm, self._bq) | |
class MeijerUnShiftB(Operator): | |
""" Increment an upper a index. """ | |
def __init__(self, an, ap, bm, bq, i, z): | |
""" Note: i counts from zero! """ | |
an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) | |
self._an = an | |
self._ap = ap | |
self._bm = bm | |
self._bq = bq | |
self._i = i | |
an = list(an) | |
ap = list(ap) | |
bm = list(bm) | |
bq = list(bq) | |
ai = an.pop(i) + 1 | |
m = Poly(z, _x) | |
for a in an: | |
m *= Poly(1 - a + _x, _x) | |
for a in ap: | |
m *= Poly(a - 1 - _x, _x) | |
B = Dummy('B') | |
D = Poly(B + ai - 1, B) | |
n = Poly(1, B) | |
for b in bm: | |
n *= (-D + b) | |
for b in bq: | |
n *= (D - b) | |
b0 = n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot increment upper a index (cancels)') | |
n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( | |
B, 1 - ai + _x), _x) | |
self._poly = Poly((m - n)/b0, _x) | |
def __str__(self): | |
return '<Increment upper a index #%s of %s, %s, %s, %s.>' % (self._i, | |
self._an, self._ap, self._bm, self._bq) | |
class MeijerUnShiftC(Operator): | |
""" Decrement a lower b index. """ | |
# XXX this is "essentially" the same as MeijerUnShiftA. This "essentially" | |
# can be made rigorous using the functional equation G(1/z) = G'(z), | |
# where G' denotes a G function of slightly altered parameters. | |
# However, sorting out the details seems harder than just coding it | |
# again. | |
def __init__(self, an, ap, bm, bq, i, z): | |
""" Note: i counts from zero! """ | |
an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) | |
self._an = an | |
self._ap = ap | |
self._bm = bm | |
self._bq = bq | |
self._i = i | |
an = list(an) | |
ap = list(ap) | |
bm = list(bm) | |
bq = list(bq) | |
bi = bq.pop(i) - 1 | |
m = Poly(1, _x) | |
for b in bm: | |
m *= Poly(b - _x, _x) | |
for b in bq: | |
m *= Poly(_x - b, _x) | |
C = Dummy('C') | |
D = Poly(bi + C, C) | |
n = Poly(z, C) | |
for a in an: | |
n *= (D + 1 - a) | |
for a in ap: | |
n *= (-D + a - 1) | |
b0 = n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot decrement lower b index (cancels)') | |
n = Poly(Poly(n.all_coeffs()[:-1], C).as_expr().subs(C, _x - bi), _x) | |
self._poly = Poly((m - n)/b0, _x) | |
def __str__(self): | |
return '<Decrement lower b index #%s of %s, %s, %s, %s.>' % (self._i, | |
self._an, self._ap, self._bm, self._bq) | |
class MeijerUnShiftD(Operator): | |
""" Increment a lower a index. """ | |
# XXX This is essentially the same as MeijerUnShiftA. | |
# See comment at MeijerUnShiftC. | |
def __init__(self, an, ap, bm, bq, i, z): | |
""" Note: i counts from zero! """ | |
an, ap, bm, bq, i = list(map(sympify, [an, ap, bm, bq, i])) | |
self._an = an | |
self._ap = ap | |
self._bm = bm | |
self._bq = bq | |
self._i = i | |
an = list(an) | |
ap = list(ap) | |
bm = list(bm) | |
bq = list(bq) | |
ai = ap.pop(i) + 1 | |
m = Poly(z, _x) | |
for a in an: | |
m *= Poly(1 - a + _x, _x) | |
for a in ap: | |
m *= Poly(a - 1 - _x, _x) | |
B = Dummy('B') # - this is the shift operator `D_I` | |
D = Poly(ai - 1 - B, B) | |
n = Poly(1, B) | |
for b in bm: | |
n *= (-D + b) | |
for b in bq: | |
n *= (D - b) | |
b0 = n.nth(0) | |
if b0 == 0: | |
raise ValueError('Cannot increment lower a index (cancels)') | |
n = Poly(Poly(n.all_coeffs()[:-1], B).as_expr().subs( | |
B, ai - 1 - _x), _x) | |
self._poly = Poly((m - n)/b0, _x) | |
def __str__(self): | |
return '<Increment lower a index #%s of %s, %s, %s, %s.>' % (self._i, | |
self._an, self._ap, self._bm, self._bq) | |
class ReduceOrder(Operator): | |
""" Reduce Order by cancelling an upper and a lower index. """ | |
def __new__(cls, ai, bj): | |
""" For convenience if reduction is not possible, return None. """ | |
ai = sympify(ai) | |
bj = sympify(bj) | |
n = ai - bj | |
if not n.is_Integer or n < 0: | |
return None | |
if bj.is_integer and bj.is_nonpositive: | |
return None | |
expr = Operator.__new__(cls) | |
p = S.One | |
for k in range(n): | |
p *= (_x + bj + k)/(bj + k) | |
expr._poly = Poly(p, _x) | |
expr._a = ai | |
expr._b = bj | |
return expr | |
def _meijer(cls, b, a, sign): | |
""" Cancel b + sign*s and a + sign*s | |
This is for meijer G functions. """ | |
b = sympify(b) | |
a = sympify(a) | |
n = b - a | |
if n.is_negative or not n.is_Integer: | |
return None | |
expr = Operator.__new__(cls) | |
p = S.One | |
for k in range(n): | |
p *= (sign*_x + a + k) | |
expr._poly = Poly(p, _x) | |
if sign == -1: | |
expr._a = b | |
expr._b = a | |
else: | |
expr._b = Add(1, a - 1, evaluate=False) | |
expr._a = Add(1, b - 1, evaluate=False) | |
return expr | |
def meijer_minus(cls, b, a): | |
return cls._meijer(b, a, -1) | |
def meijer_plus(cls, a, b): | |
return cls._meijer(1 - a, 1 - b, 1) | |
def __str__(self): | |
return '<Reduce order by cancelling upper %s with lower %s.>' % \ | |
(self._a, self._b) | |
def _reduce_order(ap, bq, gen, key): | |
""" Order reduction algorithm used in Hypergeometric and Meijer G """ | |
ap = list(ap) | |
bq = list(bq) | |
ap.sort(key=key) | |
bq.sort(key=key) | |
nap = [] | |
# we will edit bq in place | |
operators = [] | |
for a in ap: | |
op = None | |
for i in range(len(bq)): | |
op = gen(a, bq[i]) | |
if op is not None: | |
bq.pop(i) | |
break | |
if op is None: | |
nap.append(a) | |
else: | |
operators.append(op) | |
return nap, bq, operators | |
def reduce_order(func): | |
""" | |
Given the hypergeometric function ``func``, find a sequence of operators to | |
reduces order as much as possible. | |
Explanation | |
=========== | |
Return (newfunc, [operators]), where applying the operators to the | |
hypergeometric function newfunc yields func. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import reduce_order, Hyper_Function | |
>>> reduce_order(Hyper_Function((1, 2), (3, 4))) | |
(Hyper_Function((1, 2), (3, 4)), []) | |
>>> reduce_order(Hyper_Function((1,), (1,))) | |
(Hyper_Function((), ()), [<Reduce order by cancelling upper 1 with lower 1.>]) | |
>>> reduce_order(Hyper_Function((2, 4), (3, 3))) | |
(Hyper_Function((2,), (3,)), [<Reduce order by cancelling | |
upper 4 with lower 3.>]) | |
""" | |
nap, nbq, operators = _reduce_order(func.ap, func.bq, ReduceOrder, default_sort_key) | |
return Hyper_Function(Tuple(*nap), Tuple(*nbq)), operators | |
def reduce_order_meijer(func): | |
""" | |
Given the Meijer G function parameters, ``func``, find a sequence of | |
operators that reduces order as much as possible. | |
Return newfunc, [operators]. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import (reduce_order_meijer, | |
... G_Function) | |
>>> reduce_order_meijer(G_Function([3, 4], [5, 6], [3, 4], [1, 2]))[0] | |
G_Function((4, 3), (5, 6), (3, 4), (2, 1)) | |
>>> reduce_order_meijer(G_Function([3, 4], [5, 6], [3, 4], [1, 8]))[0] | |
G_Function((3,), (5, 6), (3, 4), (1,)) | |
>>> reduce_order_meijer(G_Function([3, 4], [5, 6], [7, 5], [1, 5]))[0] | |
G_Function((3,), (), (), (1,)) | |
>>> reduce_order_meijer(G_Function([3, 4], [5, 6], [7, 5], [5, 3]))[0] | |
G_Function((), (), (), ()) | |
""" | |
nan, nbq, ops1 = _reduce_order(func.an, func.bq, ReduceOrder.meijer_plus, | |
lambda x: default_sort_key(-x)) | |
nbm, nap, ops2 = _reduce_order(func.bm, func.ap, ReduceOrder.meijer_minus, | |
default_sort_key) | |
return G_Function(nan, nap, nbm, nbq), ops1 + ops2 | |
def make_derivative_operator(M, z): | |
""" Create a derivative operator, to be passed to Operator.apply. """ | |
def doit(C): | |
r = z*C.diff(z) + C*M | |
r = r.applyfunc(make_simp(z)) | |
return r | |
return doit | |
def apply_operators(obj, ops, op): | |
""" | |
Apply the list of operators ``ops`` to object ``obj``, substituting | |
``op`` for the generator. | |
""" | |
res = obj | |
for o in reversed(ops): | |
res = o.apply(res, op) | |
return res | |
def devise_plan(target, origin, z): | |
""" | |
Devise a plan (consisting of shift and un-shift operators) to be applied | |
to the hypergeometric function ``target`` to yield ``origin``. | |
Returns a list of operators. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import devise_plan, Hyper_Function | |
>>> from sympy.abc import z | |
Nothing to do: | |
>>> devise_plan(Hyper_Function((1, 2), ()), Hyper_Function((1, 2), ()), z) | |
[] | |
>>> devise_plan(Hyper_Function((), (1, 2)), Hyper_Function((), (1, 2)), z) | |
[] | |
Very simple plans: | |
>>> devise_plan(Hyper_Function((2,), ()), Hyper_Function((1,), ()), z) | |
[<Increment upper 1.>] | |
>>> devise_plan(Hyper_Function((), (2,)), Hyper_Function((), (1,)), z) | |
[<Increment lower index #0 of [], [1].>] | |
Several buckets: | |
>>> from sympy import S | |
>>> devise_plan(Hyper_Function((1, S.Half), ()), | |
... Hyper_Function((2, S('3/2')), ()), z) #doctest: +NORMALIZE_WHITESPACE | |
[<Decrement upper index #0 of [3/2, 1], [].>, | |
<Decrement upper index #0 of [2, 3/2], [].>] | |
A slightly more complicated plan: | |
>>> devise_plan(Hyper_Function((1, 3), ()), Hyper_Function((2, 2), ()), z) | |
[<Increment upper 2.>, <Decrement upper index #0 of [2, 2], [].>] | |
Another more complicated plan: (note that the ap have to be shifted first!) | |
>>> devise_plan(Hyper_Function((1, -1), (2,)), Hyper_Function((3, -2), (4,)), z) | |
[<Decrement lower 3.>, <Decrement lower 4.>, | |
<Decrement upper index #1 of [-1, 2], [4].>, | |
<Decrement upper index #1 of [-1, 3], [4].>, <Increment upper -2.>] | |
""" | |
abuckets, bbuckets, nabuckets, nbbuckets = [sift(params, _mod1) for | |
params in (target.ap, target.bq, origin.ap, origin.bq)] | |
if len(list(abuckets.keys())) != len(list(nabuckets.keys())) or \ | |
len(list(bbuckets.keys())) != len(list(nbbuckets.keys())): | |
raise ValueError('%s not reachable from %s' % (target, origin)) | |
ops = [] | |
def do_shifts(fro, to, inc, dec): | |
ops = [] | |
for i in range(len(fro)): | |
if to[i] - fro[i] > 0: | |
sh = inc | |
ch = 1 | |
else: | |
sh = dec | |
ch = -1 | |
while to[i] != fro[i]: | |
ops += [sh(fro, i)] | |
fro[i] += ch | |
return ops | |
def do_shifts_a(nal, nbk, al, aother, bother): | |
""" Shift us from (nal, nbk) to (al, nbk). """ | |
return do_shifts(nal, al, lambda p, i: ShiftA(p[i]), | |
lambda p, i: UnShiftA(p + aother, nbk + bother, i, z)) | |
def do_shifts_b(nal, nbk, bk, aother, bother): | |
""" Shift us from (nal, nbk) to (nal, bk). """ | |
return do_shifts(nbk, bk, | |
lambda p, i: UnShiftB(nal + aother, p + bother, i, z), | |
lambda p, i: ShiftB(p[i])) | |
for r in sorted(list(abuckets.keys()) + list(bbuckets.keys()), key=default_sort_key): | |
al = () | |
nal = () | |
bk = () | |
nbk = () | |
if r in abuckets: | |
al = abuckets[r] | |
nal = nabuckets[r] | |
if r in bbuckets: | |
bk = bbuckets[r] | |
nbk = nbbuckets[r] | |
if len(al) != len(nal) or len(bk) != len(nbk): | |
raise ValueError('%s not reachable from %s' % (target, origin)) | |
al, nal, bk, nbk = [sorted(w, key=default_sort_key) | |
for w in [al, nal, bk, nbk]] | |
def others(dic, key): | |
l = [] | |
for k in dic: | |
if k != key: | |
l.extend(dic[k]) | |
return l | |
aother = others(nabuckets, r) | |
bother = others(nbbuckets, r) | |
if len(al) == 0: | |
# there can be no complications, just shift the bs as we please | |
ops += do_shifts_b([], nbk, bk, aother, bother) | |
elif len(bk) == 0: | |
# there can be no complications, just shift the as as we please | |
ops += do_shifts_a(nal, [], al, aother, bother) | |
else: | |
namax = nal[-1] | |
amax = al[-1] | |
if nbk[0] - namax <= 0 or bk[0] - amax <= 0: | |
raise ValueError('Non-suitable parameters.') | |
if namax - amax > 0: | |
# we are going to shift down - first do the as, then the bs | |
ops += do_shifts_a(nal, nbk, al, aother, bother) | |
ops += do_shifts_b(al, nbk, bk, aother, bother) | |
else: | |
# we are going to shift up - first do the bs, then the as | |
ops += do_shifts_b(nal, nbk, bk, aother, bother) | |
ops += do_shifts_a(nal, bk, al, aother, bother) | |
nabuckets[r] = al | |
nbbuckets[r] = bk | |
ops.reverse() | |
return ops | |
def try_shifted_sum(func, z): | |
""" Try to recognise a hypergeometric sum that starts from k > 0. """ | |
abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) | |
if len(abuckets[S.Zero]) != 1: | |
return None | |
r = abuckets[S.Zero][0] | |
if r <= 0: | |
return None | |
if S.Zero not in bbuckets: | |
return None | |
l = list(bbuckets[S.Zero]) | |
l.sort() | |
k = l[0] | |
if k <= 0: | |
return None | |
nap = list(func.ap) | |
nap.remove(r) | |
nbq = list(func.bq) | |
nbq.remove(k) | |
k -= 1 | |
nap = [x - k for x in nap] | |
nbq = [x - k for x in nbq] | |
ops = [] | |
for n in range(r - 1): | |
ops.append(ShiftA(n + 1)) | |
ops.reverse() | |
fac = factorial(k)/z**k | |
fac *= Mul(*[rf(b, k) for b in nbq]) | |
fac /= Mul(*[rf(a, k) for a in nap]) | |
ops += [MultOperator(fac)] | |
p = 0 | |
for n in range(k): | |
m = z**n/factorial(n) | |
m *= Mul(*[rf(a, n) for a in nap]) | |
m /= Mul(*[rf(b, n) for b in nbq]) | |
p += m | |
return Hyper_Function(nap, nbq), ops, -p | |
def try_polynomial(func, z): | |
""" Recognise polynomial cases. Returns None if not such a case. | |
Requires order to be fully reduced. """ | |
abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) | |
a0 = abuckets[S.Zero] | |
b0 = bbuckets[S.Zero] | |
a0.sort() | |
b0.sort() | |
al0 = [x for x in a0 if x <= 0] | |
bl0 = [x for x in b0 if x <= 0] | |
if bl0 and all(a < bl0[-1] for a in al0): | |
return oo | |
if not al0: | |
return None | |
a = al0[-1] | |
fac = 1 | |
res = S.One | |
for n in Tuple(*list(range(-a))): | |
fac *= z | |
fac /= n + 1 | |
fac *= Mul(*[a + n for a in func.ap]) | |
fac /= Mul(*[b + n for b in func.bq]) | |
res += fac | |
return res | |
def try_lerchphi(func): | |
""" | |
Try to find an expression for Hyper_Function ``func`` in terms of Lerch | |
Transcendents. | |
Return None if no such expression can be found. | |
""" | |
# This is actually quite simple, and is described in Roach's paper, | |
# section 18. | |
# We don't need to implement the reduction to polylog here, this | |
# is handled by expand_func. | |
# First we need to figure out if the summation coefficient is a rational | |
# function of the summation index, and construct that rational function. | |
abuckets, bbuckets = sift(func.ap, _mod1), sift(func.bq, _mod1) | |
paired = {} | |
for key, value in abuckets.items(): | |
if key != 0 and key not in bbuckets: | |
return None | |
bvalue = bbuckets[key] | |
paired[key] = (list(value), list(bvalue)) | |
bbuckets.pop(key, None) | |
if bbuckets != {}: | |
return None | |
if S.Zero not in abuckets: | |
return None | |
aints, bints = paired[S.Zero] | |
# Account for the additional n! in denominator | |
paired[S.Zero] = (aints, bints + [1]) | |
t = Dummy('t') | |
numer = S.One | |
denom = S.One | |
for key, (avalue, bvalue) in paired.items(): | |
if len(avalue) != len(bvalue): | |
return None | |
# Note that since order has been reduced fully, all the b are | |
# bigger than all the a they differ from by an integer. In particular | |
# if there are any negative b left, this function is not well-defined. | |
for a, b in zip(avalue, bvalue): | |
if (a - b).is_positive: | |
k = a - b | |
numer *= rf(b + t, k) | |
denom *= rf(b, k) | |
else: | |
k = b - a | |
numer *= rf(a, k) | |
denom *= rf(a + t, k) | |
# Now do a partial fraction decomposition. | |
# We assemble two structures: a list monomials of pairs (a, b) representing | |
# a*t**b (b a non-negative integer), and a dict terms, where | |
# terms[a] = [(b, c)] means that there is a term b/(t-a)**c. | |
part = apart(numer/denom, t) | |
args = Add.make_args(part) | |
monomials = [] | |
terms = {} | |
for arg in args: | |
numer, denom = arg.as_numer_denom() | |
if not denom.has(t): | |
p = Poly(numer, t) | |
if not p.is_monomial: | |
raise TypeError("p should be monomial") | |
((b, ), a) = p.LT() | |
monomials += [(a/denom, b)] | |
continue | |
if numer.has(t): | |
raise NotImplementedError('Need partial fraction decomposition' | |
' with linear denominators') | |
indep, [dep] = denom.as_coeff_mul(t) | |
n = 1 | |
if dep.is_Pow: | |
n = dep.exp | |
dep = dep.base | |
if dep == t: | |
a == 0 | |
elif dep.is_Add: | |
a, tmp = dep.as_independent(t) | |
b = 1 | |
if tmp != t: | |
b, _ = tmp.as_independent(t) | |
if dep != b*t + a: | |
raise NotImplementedError('unrecognised form %s' % dep) | |
a /= b | |
indep *= b**n | |
else: | |
raise NotImplementedError('unrecognised form of partial fraction') | |
terms.setdefault(a, []).append((numer/indep, n)) | |
# Now that we have this information, assemble our formula. All the | |
# monomials yield rational functions and go into one basis element. | |
# The terms[a] are related by differentiation. If the largest exponent is | |
# n, we need lerchphi(z, k, a) for k = 1, 2, ..., n. | |
# deriv maps a basis to its derivative, expressed as a C(z)-linear | |
# combination of other basis elements. | |
deriv = {} | |
coeffs = {} | |
z = Dummy('z') | |
monomials.sort(key=lambda x: x[1]) | |
mon = {0: 1/(1 - z)} | |
if monomials: | |
for k in range(monomials[-1][1]): | |
mon[k + 1] = z*mon[k].diff(z) | |
for a, n in monomials: | |
coeffs.setdefault(S.One, []).append(a*mon[n]) | |
for a, l in terms.items(): | |
for c, k in l: | |
coeffs.setdefault(lerchphi(z, k, a), []).append(c) | |
l.sort(key=lambda x: x[1]) | |
for k in range(2, l[-1][1] + 1): | |
deriv[lerchphi(z, k, a)] = [(-a, lerchphi(z, k, a)), | |
(1, lerchphi(z, k - 1, a))] | |
deriv[lerchphi(z, 1, a)] = [(-a, lerchphi(z, 1, a)), | |
(1/(1 - z), S.One)] | |
trans = {} | |
for n, b in enumerate([S.One] + list(deriv.keys())): | |
trans[b] = n | |
basis = [expand_func(b) for (b, _) in sorted(trans.items(), | |
key=lambda x:x[1])] | |
B = Matrix(basis) | |
C = Matrix([[0]*len(B)]) | |
for b, c in coeffs.items(): | |
C[trans[b]] = Add(*c) | |
M = zeros(len(B)) | |
for b, l in deriv.items(): | |
for c, b2 in l: | |
M[trans[b], trans[b2]] = c | |
return Formula(func, z, None, [], B, C, M) | |
def build_hypergeometric_formula(func): | |
""" | |
Create a formula object representing the hypergeometric function ``func``. | |
""" | |
# We know that no `ap` are negative integers, otherwise "detect poly" | |
# would have kicked in. However, `ap` could be empty. In this case we can | |
# use a different basis. | |
# I'm not aware of a basis that works in all cases. | |
z = Dummy('z') | |
if func.ap: | |
afactors = [_x + a for a in func.ap] | |
bfactors = [_x + b - 1 for b in func.bq] | |
expr = _x*Mul(*bfactors) - z*Mul(*afactors) | |
poly = Poly(expr, _x) | |
n = poly.degree() | |
basis = [] | |
M = zeros(n) | |
for k in range(n): | |
a = func.ap[0] + k | |
basis += [hyper([a] + list(func.ap[1:]), func.bq, z)] | |
if k < n - 1: | |
M[k, k] = -a | |
M[k, k + 1] = a | |
B = Matrix(basis) | |
C = Matrix([[1] + [0]*(n - 1)]) | |
derivs = [eye(n)] | |
for k in range(n): | |
derivs.append(M*derivs[k]) | |
l = poly.all_coeffs() | |
l.reverse() | |
res = [0]*n | |
for k, c in enumerate(l): | |
for r, d in enumerate(C*derivs[k]): | |
res[r] += c*d | |
for k, c in enumerate(res): | |
M[n - 1, k] = -c/derivs[n - 1][0, n - 1]/poly.all_coeffs()[0] | |
return Formula(func, z, None, [], B, C, M) | |
else: | |
# Since there are no `ap`, none of the `bq` can be non-positive | |
# integers. | |
basis = [] | |
bq = list(func.bq[:]) | |
for i in range(len(bq)): | |
basis += [hyper([], bq, z)] | |
bq[i] += 1 | |
basis += [hyper([], bq, z)] | |
B = Matrix(basis) | |
n = len(B) | |
C = Matrix([[1] + [0]*(n - 1)]) | |
M = zeros(n) | |
M[0, n - 1] = z/Mul(*func.bq) | |
for k in range(1, n): | |
M[k, k - 1] = func.bq[k - 1] | |
M[k, k] = -func.bq[k - 1] | |
return Formula(func, z, None, [], B, C, M) | |
def hyperexpand_special(ap, bq, z): | |
""" | |
Try to find a closed-form expression for hyper(ap, bq, z), where ``z`` | |
is supposed to be a "special" value, e.g. 1. | |
This function tries various of the classical summation formulae | |
(Gauss, Saalschuetz, etc). | |
""" | |
# This code is very ad-hoc. There are many clever algorithms | |
# (notably Zeilberger's) related to this problem. | |
# For now we just want a few simple cases to work. | |
p, q = len(ap), len(bq) | |
z_ = z | |
z = unpolarify(z) | |
if z == 0: | |
return S.One | |
from sympy.simplify.simplify import simplify | |
if p == 2 and q == 1: | |
# 2F1 | |
a, b, c = ap + bq | |
if z == 1: | |
# Gauss | |
return gamma(c - a - b)*gamma(c)/gamma(c - a)/gamma(c - b) | |
if z == -1 and simplify(b - a + c) == 1: | |
b, a = a, b | |
if z == -1 and simplify(a - b + c) == 1: | |
# Kummer | |
if b.is_integer and b.is_negative: | |
return 2*cos(pi*b/2)*gamma(-b)*gamma(b - a + 1) \ | |
/gamma(-b/2)/gamma(b/2 - a + 1) | |
else: | |
return gamma(b/2 + 1)*gamma(b - a + 1) \ | |
/gamma(b + 1)/gamma(b/2 - a + 1) | |
# TODO tons of more formulae | |
# investigate what algorithms exist | |
return hyper(ap, bq, z_) | |
_collection = None | |
def _hyperexpand(func, z, ops0=[], z0=Dummy('z0'), premult=1, prem=0, | |
rewrite='default'): | |
""" | |
Try to find an expression for the hypergeometric function ``func``. | |
Explanation | |
=========== | |
The result is expressed in terms of a dummy variable ``z0``. Then it | |
is multiplied by ``premult``. Then ``ops0`` is applied. | |
``premult`` must be a*z**prem for some a independent of ``z``. | |
""" | |
if z.is_zero: | |
return S.One | |
from sympy.simplify.simplify import simplify | |
z = polarify(z, subs=False) | |
if rewrite == 'default': | |
rewrite = 'nonrepsmall' | |
def carryout_plan(f, ops): | |
C = apply_operators(f.C.subs(f.z, z0), ops, | |
make_derivative_operator(f.M.subs(f.z, z0), z0)) | |
C = apply_operators(C, ops0, | |
make_derivative_operator(f.M.subs(f.z, z0) | |
+ prem*eye(f.M.shape[0]), z0)) | |
if premult == 1: | |
C = C.applyfunc(make_simp(z0)) | |
r = reduce(lambda s,m: s+m[0]*m[1], zip(C, f.B.subs(f.z, z0)), S.Zero)*premult | |
res = r.subs(z0, z) | |
if rewrite: | |
res = res.rewrite(rewrite) | |
return res | |
# TODO | |
# The following would be possible: | |
# *) PFD Duplication (see Kelly Roach's paper) | |
# *) In a similar spirit, try_lerchphi() can be generalised considerably. | |
global _collection | |
if _collection is None: | |
_collection = FormulaCollection() | |
debug('Trying to expand hypergeometric function ', func) | |
# First reduce order as much as possible. | |
func, ops = reduce_order(func) | |
if ops: | |
debug(' Reduced order to ', func) | |
else: | |
debug(' Could not reduce order.') | |
# Now try polynomial cases | |
res = try_polynomial(func, z0) | |
if res is not None: | |
debug(' Recognised polynomial.') | |
p = apply_operators(res, ops, lambda f: z0*f.diff(z0)) | |
p = apply_operators(p*premult, ops0, lambda f: z0*f.diff(z0)) | |
return unpolarify(simplify(p).subs(z0, z)) | |
# Try to recognise a shifted sum. | |
p = S.Zero | |
res = try_shifted_sum(func, z0) | |
if res is not None: | |
func, nops, p = res | |
debug(' Recognised shifted sum, reduced order to ', func) | |
ops += nops | |
# apply the plan for poly | |
p = apply_operators(p, ops, lambda f: z0*f.diff(z0)) | |
p = apply_operators(p*premult, ops0, lambda f: z0*f.diff(z0)) | |
p = simplify(p).subs(z0, z) | |
# Try special expansions early. | |
if unpolarify(z) in [1, -1] and (len(func.ap), len(func.bq)) == (2, 1): | |
f = build_hypergeometric_formula(func) | |
r = carryout_plan(f, ops).replace(hyper, hyperexpand_special) | |
if not r.has(hyper): | |
return r + p | |
# Try to find a formula in our collection | |
formula = _collection.lookup_origin(func) | |
# Now try a lerch phi formula | |
if formula is None: | |
formula = try_lerchphi(func) | |
if formula is None: | |
debug(' Could not find an origin. ', | |
'Will return answer in terms of ' | |
'simpler hypergeometric functions.') | |
formula = build_hypergeometric_formula(func) | |
debug(' Found an origin: ', formula.closed_form, ' ', formula.func) | |
# We need to find the operators that convert formula into func. | |
ops += devise_plan(func, formula.func, z0) | |
# Now carry out the plan. | |
r = carryout_plan(formula, ops) + p | |
return powdenest(r, polar=True).replace(hyper, hyperexpand_special) | |
def devise_plan_meijer(fro, to, z): | |
""" | |
Find operators to convert G-function ``fro`` into G-function ``to``. | |
Explanation | |
=========== | |
It is assumed that ``fro`` and ``to`` have the same signatures, and that in fact | |
any corresponding pair of parameters differs by integers, and a direct path | |
is possible. I.e. if there are parameters a1 b1 c1 and a2 b2 c2 it is | |
assumed that a1 can be shifted to a2, etc. The only thing this routine | |
determines is the order of shifts to apply, nothing clever will be tried. | |
It is also assumed that ``fro`` is suitable. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import (devise_plan_meijer, | |
... G_Function) | |
>>> from sympy.abc import z | |
Empty plan: | |
>>> devise_plan_meijer(G_Function([1], [2], [3], [4]), | |
... G_Function([1], [2], [3], [4]), z) | |
[] | |
Very simple plans: | |
>>> devise_plan_meijer(G_Function([0], [], [], []), | |
... G_Function([1], [], [], []), z) | |
[<Increment upper a index #0 of [0], [], [], [].>] | |
>>> devise_plan_meijer(G_Function([0], [], [], []), | |
... G_Function([-1], [], [], []), z) | |
[<Decrement upper a=0.>] | |
>>> devise_plan_meijer(G_Function([], [1], [], []), | |
... G_Function([], [2], [], []), z) | |
[<Increment lower a index #0 of [], [1], [], [].>] | |
Slightly more complicated plans: | |
>>> devise_plan_meijer(G_Function([0], [], [], []), | |
... G_Function([2], [], [], []), z) | |
[<Increment upper a index #0 of [1], [], [], [].>, | |
<Increment upper a index #0 of [0], [], [], [].>] | |
>>> devise_plan_meijer(G_Function([0], [], [0], []), | |
... G_Function([-1], [], [1], []), z) | |
[<Increment upper b=0.>, <Decrement upper a=0.>] | |
Order matters: | |
>>> devise_plan_meijer(G_Function([0], [], [0], []), | |
... G_Function([1], [], [1], []), z) | |
[<Increment upper a index #0 of [0], [], [1], [].>, <Increment upper b=0.>] | |
""" | |
# TODO for now, we use the following simple heuristic: inverse-shift | |
# when possible, shift otherwise. Give up if we cannot make progress. | |
def try_shift(f, t, shifter, diff, counter): | |
""" Try to apply ``shifter`` in order to bring some element in ``f`` | |
nearer to its counterpart in ``to``. ``diff`` is +/- 1 and | |
determines the effect of ``shifter``. Counter is a list of elements | |
blocking the shift. | |
Return an operator if change was possible, else None. | |
""" | |
for idx, (a, b) in enumerate(zip(f, t)): | |
if ( | |
(a - b).is_integer and (b - a)/diff > 0 and | |
all(a != x for x in counter)): | |
sh = shifter(idx) | |
f[idx] += diff | |
return sh | |
fan = list(fro.an) | |
fap = list(fro.ap) | |
fbm = list(fro.bm) | |
fbq = list(fro.bq) | |
ops = [] | |
change = True | |
while change: | |
change = False | |
op = try_shift(fan, to.an, | |
lambda i: MeijerUnShiftB(fan, fap, fbm, fbq, i, z), | |
1, fbm + fbq) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fap, to.ap, | |
lambda i: MeijerUnShiftD(fan, fap, fbm, fbq, i, z), | |
1, fbm + fbq) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fbm, to.bm, | |
lambda i: MeijerUnShiftA(fan, fap, fbm, fbq, i, z), | |
-1, fan + fap) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fbq, to.bq, | |
lambda i: MeijerUnShiftC(fan, fap, fbm, fbq, i, z), | |
-1, fan + fap) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fan, to.an, lambda i: MeijerShiftB(fan[i]), -1, []) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fap, to.ap, lambda i: MeijerShiftD(fap[i]), -1, []) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fbm, to.bm, lambda i: MeijerShiftA(fbm[i]), 1, []) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
op = try_shift(fbq, to.bq, lambda i: MeijerShiftC(fbq[i]), 1, []) | |
if op is not None: | |
ops += [op] | |
change = True | |
continue | |
if fan != list(to.an) or fap != list(to.ap) or fbm != list(to.bm) or \ | |
fbq != list(to.bq): | |
raise NotImplementedError('Could not devise plan.') | |
ops.reverse() | |
return ops | |
_meijercollection = None | |
def _meijergexpand(func, z0, allow_hyper=False, rewrite='default', | |
place=None): | |
""" | |
Try to find an expression for the Meijer G function specified | |
by the G_Function ``func``. If ``allow_hyper`` is True, then returning | |
an expression in terms of hypergeometric functions is allowed. | |
Currently this just does Slater's theorem. | |
If expansions exist both at zero and at infinity, ``place`` | |
can be set to ``0`` or ``zoo`` for the preferred choice. | |
""" | |
global _meijercollection | |
if _meijercollection is None: | |
_meijercollection = MeijerFormulaCollection() | |
if rewrite == 'default': | |
rewrite = None | |
func0 = func | |
debug('Try to expand Meijer G function corresponding to ', func) | |
# We will play games with analytic continuation - rather use a fresh symbol | |
z = Dummy('z') | |
func, ops = reduce_order_meijer(func) | |
if ops: | |
debug(' Reduced order to ', func) | |
else: | |
debug(' Could not reduce order.') | |
# Try to find a direct formula | |
f = _meijercollection.lookup_origin(func) | |
if f is not None: | |
debug(' Found a Meijer G formula: ', f.func) | |
ops += devise_plan_meijer(f.func, func, z) | |
# Now carry out the plan. | |
C = apply_operators(f.C.subs(f.z, z), ops, | |
make_derivative_operator(f.M.subs(f.z, z), z)) | |
C = C.applyfunc(make_simp(z)) | |
r = C*f.B.subs(f.z, z) | |
r = r[0].subs(z, z0) | |
return powdenest(r, polar=True) | |
debug(" Could not find a direct formula. Trying Slater's theorem.") | |
# TODO the following would be possible: | |
# *) Paired Index Theorems | |
# *) PFD Duplication | |
# (See Kelly Roach's paper for details on either.) | |
# | |
# TODO Also, we tend to create combinations of gamma functions that can be | |
# simplified. | |
def can_do(pbm, pap): | |
""" Test if slater applies. """ | |
for i in pbm: | |
if len(pbm[i]) > 1: | |
l = 0 | |
if i in pap: | |
l = len(pap[i]) | |
if l + 1 < len(pbm[i]): | |
return False | |
return True | |
def do_slater(an, bm, ap, bq, z, zfinal): | |
# zfinal is the value that will eventually be substituted for z. | |
# We pass it to _hyperexpand to improve performance. | |
func = G_Function(an, bm, ap, bq) | |
_, pbm, pap, _ = func.compute_buckets() | |
if not can_do(pbm, pap): | |
return S.Zero, False | |
cond = len(an) + len(ap) < len(bm) + len(bq) | |
if len(an) + len(ap) == len(bm) + len(bq): | |
cond = abs(z) < 1 | |
if cond is False: | |
return S.Zero, False | |
res = S.Zero | |
for m in pbm: | |
if len(pbm[m]) == 1: | |
bh = pbm[m][0] | |
fac = 1 | |
bo = list(bm) | |
bo.remove(bh) | |
for bj in bo: | |
fac *= gamma(bj - bh) | |
for aj in an: | |
fac *= gamma(1 + bh - aj) | |
for bj in bq: | |
fac /= gamma(1 + bh - bj) | |
for aj in ap: | |
fac /= gamma(aj - bh) | |
nap = [1 + bh - a for a in list(an) + list(ap)] | |
nbq = [1 + bh - b for b in list(bo) + list(bq)] | |
k = polar_lift(S.NegativeOne**(len(ap) - len(bm))) | |
harg = k*zfinal | |
# NOTE even though k "is" +-1, this has to be t/k instead of | |
# t*k ... we are using polar numbers for consistency! | |
premult = (t/k)**bh | |
hyp = _hyperexpand(Hyper_Function(nap, nbq), harg, ops, | |
t, premult, bh, rewrite=None) | |
res += fac * hyp | |
else: | |
b_ = pbm[m][0] | |
ki = [bi - b_ for bi in pbm[m][1:]] | |
u = len(ki) | |
li = [ai - b_ for ai in pap[m][:u + 1]] | |
bo = list(bm) | |
for b in pbm[m]: | |
bo.remove(b) | |
ao = list(ap) | |
for a in pap[m][:u]: | |
ao.remove(a) | |
lu = li[-1] | |
di = [l - k for (l, k) in zip(li, ki)] | |
# We first work out the integrand: | |
s = Dummy('s') | |
integrand = z**s | |
for b in bm: | |
if not Mod(b, 1) and b.is_Number: | |
b = int(round(b)) | |
integrand *= gamma(b - s) | |
for a in an: | |
integrand *= gamma(1 - a + s) | |
for b in bq: | |
integrand /= gamma(1 - b + s) | |
for a in ap: | |
integrand /= gamma(a - s) | |
# Now sum the finitely many residues: | |
# XXX This speeds up some cases - is it a good idea? | |
integrand = expand_func(integrand) | |
for r in range(int(round(lu))): | |
resid = residue(integrand, s, b_ + r) | |
resid = apply_operators(resid, ops, lambda f: z*f.diff(z)) | |
res -= resid | |
# Now the hypergeometric term. | |
au = b_ + lu | |
k = polar_lift(S.NegativeOne**(len(ao) + len(bo) + 1)) | |
harg = k*zfinal | |
premult = (t/k)**au | |
nap = [1 + au - a for a in list(an) + list(ap)] + [1] | |
nbq = [1 + au - b for b in list(bm) + list(bq)] | |
hyp = _hyperexpand(Hyper_Function(nap, nbq), harg, ops, | |
t, premult, au, rewrite=None) | |
C = S.NegativeOne**(lu)/factorial(lu) | |
for i in range(u): | |
C *= S.NegativeOne**di[i]/rf(lu - li[i] + 1, di[i]) | |
for a in an: | |
C *= gamma(1 - a + au) | |
for b in bo: | |
C *= gamma(b - au) | |
for a in ao: | |
C /= gamma(a - au) | |
for b in bq: | |
C /= gamma(1 - b + au) | |
res += C*hyp | |
return res, cond | |
t = Dummy('t') | |
slater1, cond1 = do_slater(func.an, func.bm, func.ap, func.bq, z, z0) | |
def tr(l): | |
return [1 - x for x in l] | |
for op in ops: | |
op._poly = Poly(op._poly.subs({z: 1/t, _x: -_x}), _x) | |
slater2, cond2 = do_slater(tr(func.bm), tr(func.an), tr(func.bq), tr(func.ap), | |
t, 1/z0) | |
slater1 = powdenest(slater1.subs(z, z0), polar=True) | |
slater2 = powdenest(slater2.subs(t, 1/z0), polar=True) | |
if not isinstance(cond2, bool): | |
cond2 = cond2.subs(t, 1/z) | |
m = func(z) | |
if m.delta > 0 or \ | |
(m.delta == 0 and len(m.ap) == len(m.bq) and | |
(re(m.nu) < -1) is not False and polar_lift(z0) == polar_lift(1)): | |
# The condition delta > 0 means that the convergence region is | |
# connected. Any expression we find can be continued analytically | |
# to the entire convergence region. | |
# The conditions delta==0, p==q, re(nu) < -1 imply that G is continuous | |
# on the positive reals, so the values at z=1 agree. | |
if cond1 is not False: | |
cond1 = True | |
if cond2 is not False: | |
cond2 = True | |
if cond1 is True: | |
slater1 = slater1.rewrite(rewrite or 'nonrep') | |
else: | |
slater1 = slater1.rewrite(rewrite or 'nonrepsmall') | |
if cond2 is True: | |
slater2 = slater2.rewrite(rewrite or 'nonrep') | |
else: | |
slater2 = slater2.rewrite(rewrite or 'nonrepsmall') | |
if cond1 is not False and cond2 is not False: | |
# If one condition is False, there is no choice. | |
if place == 0: | |
cond2 = False | |
if place == zoo: | |
cond1 = False | |
if not isinstance(cond1, bool): | |
cond1 = cond1.subs(z, z0) | |
if not isinstance(cond2, bool): | |
cond2 = cond2.subs(z, z0) | |
def weight(expr, cond): | |
if cond is True: | |
c0 = 0 | |
elif cond is False: | |
c0 = 1 | |
else: | |
c0 = 2 | |
if expr.has(oo, zoo, -oo, nan): | |
# XXX this actually should not happen, but consider | |
# S('meijerg(((0, -1/2, 0, -1/2, 1/2), ()), ((0,), | |
# (-1/2, -1/2, -1/2, -1)), exp_polar(I*pi))/4') | |
c0 = 3 | |
return (c0, expr.count(hyper), expr.count_ops()) | |
w1 = weight(slater1, cond1) | |
w2 = weight(slater2, cond2) | |
if min(w1, w2) <= (0, 1, oo): | |
if w1 < w2: | |
return slater1 | |
else: | |
return slater2 | |
if max(w1[0], w2[0]) <= 1 and max(w1[1], w2[1]) <= 1: | |
return Piecewise((slater1, cond1), (slater2, cond2), (func0(z0), True)) | |
# We couldn't find an expression without hypergeometric functions. | |
# TODO it would be helpful to give conditions under which the integral | |
# is known to diverge. | |
r = Piecewise((slater1, cond1), (slater2, cond2), (func0(z0), True)) | |
if r.has(hyper) and not allow_hyper: | |
debug(' Could express using hypergeometric functions, ' | |
'but not allowed.') | |
if not r.has(hyper) or allow_hyper: | |
return r | |
return func0(z0) | |
def hyperexpand(f, allow_hyper=False, rewrite='default', place=None): | |
""" | |
Expand hypergeometric functions. If allow_hyper is True, allow partial | |
simplification (that is a result different from input, | |
but still containing hypergeometric functions). | |
If a G-function has expansions both at zero and at infinity, | |
``place`` can be set to ``0`` or ``zoo`` to indicate the | |
preferred choice. | |
Examples | |
======== | |
>>> from sympy.simplify.hyperexpand import hyperexpand | |
>>> from sympy.functions import hyper | |
>>> from sympy.abc import z | |
>>> hyperexpand(hyper([], [], z)) | |
exp(z) | |
Non-hyperegeometric parts of the expression and hypergeometric expressions | |
that are not recognised are left unchanged: | |
>>> hyperexpand(1 + hyper([1, 1, 1], [], z)) | |
hyper((1, 1, 1), (), z) + 1 | |
""" | |
f = sympify(f) | |
def do_replace(ap, bq, z): | |
r = _hyperexpand(Hyper_Function(ap, bq), z, rewrite=rewrite) | |
if r is None: | |
return hyper(ap, bq, z) | |
else: | |
return r | |
def do_meijer(ap, bq, z): | |
r = _meijergexpand(G_Function(ap[0], ap[1], bq[0], bq[1]), z, | |
allow_hyper, rewrite=rewrite, place=place) | |
if not r.has(nan, zoo, oo, -oo): | |
return r | |
return f.replace(hyper, do_replace).replace(meijerg, do_meijer) | |