123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- """Precompute coefficients of several series expansions
- of Wright's generalized Bessel function Phi(a, b, x).
- See https://dlmf.nist.gov/10.46.E1 with rho=a, beta=b, z=x.
- """
- from argparse import ArgumentParser, RawTextHelpFormatter
- import numpy as np
- from scipy.integrate import quad
- from scipy.optimize import minimize_scalar, curve_fit
- from time import time
- try:
- import sympy
- from sympy import EulerGamma, Rational, S, Sum, \
- factorial, gamma, gammasimp, pi, polygamma, symbols, zeta
- from sympy.polys.polyfuncs import horner
- except ImportError:
- pass
- def series_small_a():
- """Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.
- """
- order = 5
- a, b, x, k = symbols("a b x k")
- A = [] # terms with a
- X = [] # terms with x
- B = [] # terms with b (polygammas)
- # Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i])
- expression = Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
- expression = gamma(b)/sympy.exp(x) * expression
- # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
- for n in range(0, order+1):
- term = expression.diff(a, n).subs(a, 0).simplify().doit()
- # set the whole bracket involving polygammas to 1
- x_part = (term.subs(polygamma(0, b), 1)
- .replace(polygamma, lambda *args: 0))
- # sign convetion: x part always positive
- x_part *= (-1)**n
- A.append(a**n/factorial(n))
- X.append(horner(x_part))
- B.append(horner((term/x_part).simplify()))
- s = "Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.\n"
- s += "Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i], i=0..5)\n"
- for name, c in zip(['A', 'X', 'B'], [A, X, B]):
- for i in range(len(c)):
- s += f"\n{name}[{i}] = " + str(c[i])
- return s
- # expansion of digamma
- def dg_series(z, n):
- """Symbolic expansion of digamma(z) in z=0 to order n.
- See https://dlmf.nist.gov/5.7.E4 and with https://dlmf.nist.gov/5.5.E2
- """
- k = symbols("k")
- return -1/z - EulerGamma + \
- sympy.summation((-1)**k * zeta(k) * z**(k-1), (k, 2, n+1))
- def pg_series(k, z, n):
- """Symbolic expansion of polygamma(k, z) in z=0 to order n."""
- return sympy.diff(dg_series(z, n+k), z, k)
- def series_small_a_small_b():
- """Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5.
- Be aware of cancellation of poles in b=0 of digamma(b)/Gamma(b) and
- polygamma functions.
- digamma(b)/Gamma(b) = -1 - 2*M_EG*b + O(b^2)
- digamma(b)^2/Gamma(b) = 1/b + 3*M_EG + b*(-5/12*PI^2+7/2*M_EG^2) + O(b^2)
- polygamma(1, b)/Gamma(b) = 1/b + M_EG + b*(1/12*PI^2 + 1/2*M_EG^2) + O(b^2)
- and so on.
- """
- order = 5
- a, b, x, k = symbols("a b x k")
- M_PI, M_EG, M_Z3 = symbols("M_PI M_EG M_Z3")
- c_subs = {pi: M_PI, EulerGamma: M_EG, zeta(3): M_Z3}
- A = [] # terms with a
- X = [] # terms with x
- B = [] # terms with b (polygammas expanded)
- C = [] # terms that generate B
- # Phi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i])
- # B[0] = 1
- # B[k] = sum(C[k] * b**k/k!, k=0..)
- # Note: C[k] can be obtained from a series expansion of 1/gamma(b).
- expression = gamma(b)/sympy.exp(x) * \
- Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
- # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
- for n in range(0, order+1):
- term = expression.diff(a, n).subs(a, 0).simplify().doit()
- # set the whole bracket involving polygammas to 1
- x_part = (term.subs(polygamma(0, b), 1)
- .replace(polygamma, lambda *args: 0))
- # sign convetion: x part always positive
- x_part *= (-1)**n
- # expansion of polygamma part with 1/gamma(b)
- pg_part = term/x_part/gamma(b)
- if n >= 1:
- # Note: highest term is digamma^n
- pg_part = pg_part.replace(polygamma,
- lambda k, x: pg_series(k, x, order+1+n))
- pg_part = (pg_part.series(b, 0, n=order+1-n)
- .removeO()
- .subs(polygamma(2, 1), -2*zeta(3))
- .simplify()
- )
- A.append(a**n/factorial(n))
- X.append(horner(x_part))
- B.append(pg_part)
- # Calculate C and put in the k!
- C = sympy.Poly(B[1].subs(c_subs), b).coeffs()
- C.reverse()
- for i in range(len(C)):
- C[i] = (C[i] * factorial(i)).simplify()
- s = "Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5."
- s += "\nPhi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i], i=0..5)\n"
- s += "B[0] = 1\n"
- s += "B[i] = sum(C[k+i-1] * b**k/k!, k=0..)\n"
- s += "\nM_PI = pi"
- s += "\nM_EG = EulerGamma"
- s += "\nM_Z3 = zeta(3)"
- for name, c in zip(['A', 'X'], [A, X]):
- for i in range(len(c)):
- s += f"\n{name}[{i}] = "
- s += str(c[i])
- # For C, do also compute the values numerically
- for i in range(len(C)):
- s += f"\n# C[{i}] = "
- s += str(C[i])
- s += f"\nC[{i}] = "
- s += str(C[i].subs({M_EG: EulerGamma, M_PI: pi, M_Z3: zeta(3)})
- .evalf(17))
- # Does B have the assumed structure?
- s += "\n\nTest if B[i] does have the assumed structure."
- s += "\nC[i] are derived from B[1] allone."
- s += "\nTest B[2] == C[1] + b*C[2] + b^2/2*C[3] + b^3/6*C[4] + .."
- test = sum([b**k/factorial(k) * C[k+1] for k in range(order-1)])
- test = (test - B[2].subs(c_subs)).simplify()
- s += f"\ntest successful = {test==S(0)}"
- s += "\nTest B[3] == C[2] + b*C[3] + b^2/2*C[4] + .."
- test = sum([b**k/factorial(k) * C[k+2] for k in range(order-2)])
- test = (test - B[3].subs(c_subs)).simplify()
- s += f"\ntest successful = {test==S(0)}"
- return s
- def asymptotic_series():
- """Asymptotic expansion for large x.
- Phi(a, b, x) ~ Z^(1/2-b) * exp((1+a)/a * Z) * sum_k (-1)^k * C_k / Z^k
- Z = (a*x)^(1/(1+a))
- Wright (1935) lists the coefficients C_0 and C_1 (he calls them a_0 and
- a_1). With slightly different notation, Paris (2017) lists coefficients
- c_k up to order k=3.
- Paris (2017) uses ZP = (1+a)/a * Z (ZP = Z of Paris) and
- C_k = C_0 * (-a/(1+a))^k * c_k
- """
- order = 8
- class g(sympy.Function):
- """Helper function g according to Wright (1935)
- g(n, rho, v) = (1 + (rho+2)/3 * v + (rho+2)*(rho+3)/(2*3) * v^2 + ...)
- Note: Wright (1935) uses square root of above definition.
- """
- nargs = 3
- @classmethod
- def eval(cls, n, rho, v):
- if not n >= 0:
- raise ValueError("must have n >= 0")
- elif n == 0:
- return 1
- else:
- return g(n-1, rho, v) \
- + gammasimp(gamma(rho+2+n)/gamma(rho+2)) \
- / gammasimp(gamma(3+n)/gamma(3))*v**n
- class coef_C(sympy.Function):
- """Calculate coefficients C_m for integer m.
- C_m is the coefficient of v^(2*m) in the Taylor expansion in v=0 of
- Gamma(m+1/2)/(2*pi) * (2/(rho+1))^(m+1/2) * (1-v)^(-b)
- * g(rho, v)^(-m-1/2)
- """
- nargs = 3
- @classmethod
- def eval(cls, m, rho, beta):
- if not m >= 0:
- raise ValueError("must have m >= 0")
- v = symbols("v")
- expression = (1-v)**(-beta) * g(2*m, rho, v)**(-m-Rational(1, 2))
- res = expression.diff(v, 2*m).subs(v, 0) / factorial(2*m)
- res = res * (gamma(m + Rational(1, 2)) / (2*pi)
- * (2/(rho+1))**(m + Rational(1, 2)))
- return res
- # in order to have nice ordering/sorting of expressions, we set a = xa.
- xa, b, xap1 = symbols("xa b xap1")
- C0 = coef_C(0, xa, b)
- # a1 = a(1, rho, beta)
- s = "Asymptotic expansion for large x\n"
- s += "Phi(a, b, x) = Z**(1/2-b) * exp((1+a)/a * Z) \n"
- s += " * sum((-1)**k * C[k]/Z**k, k=0..6)\n\n"
- s += "Z = pow(a * x, 1/(1+a))\n"
- s += "A[k] = pow(a, k)\n"
- s += "B[k] = pow(b, k)\n"
- s += "Ap1[k] = pow(1+a, k)\n\n"
- s += "C[0] = 1./sqrt(2. * M_PI * Ap1[1])\n"
- for i in range(1, order+1):
- expr = (coef_C(i, xa, b) / (C0/(1+xa)**i)).simplify()
- factor = [x.denominator() for x in sympy.Poly(expr).coeffs()]
- factor = sympy.lcm(factor)
- expr = (expr * factor).simplify().collect(b, sympy.factor)
- expr = expr.xreplace({xa+1: xap1})
- s += f"C[{i}] = C[0] / ({factor} * Ap1[{i}])\n"
- s += f"C[{i}] *= {str(expr)}\n\n"
- import re
- re_a = re.compile(r'xa\*\*(\d+)')
- s = re_a.sub(r'A[\1]', s)
- re_b = re.compile(r'b\*\*(\d+)')
- s = re_b.sub(r'B[\1]', s)
- s = s.replace('xap1', 'Ap1[1]')
- s = s.replace('xa', 'a')
- # max integer = 2^31-1 = 2,147,483,647. Solution: Put a point after 10
- # or more digits.
- re_digits = re.compile(r'(\d{10,})')
- s = re_digits.sub(r'\1.', s)
- return s
- def optimal_epsilon_integral():
- """Fit optimal choice of epsilon for integral representation.
- The integrand of
- int_0^pi P(eps, a, b, x, phi) * dphi
- can exhibit oscillatory behaviour. It stems from the cosine of P and can be
- minimized by minimizing the arc length of the argument
- f(phi) = eps * sin(phi) - x * eps^(-a) * sin(a * phi) + (1 - b) * phi
- of cos(f(phi)).
- We minimize the arc length in eps for a grid of values (a, b, x) and fit a
- parametric function to it.
- """
- def fp(eps, a, b, x, phi):
- """Derivative of f w.r.t. phi."""
- eps_a = np.power(1. * eps, -a)
- return eps * np.cos(phi) - a * x * eps_a * np.cos(a * phi) + 1 - b
- def arclength(eps, a, b, x, epsrel=1e-2, limit=100):
- """Compute Arc length of f.
- Note that the arg length of a function f fro t0 to t1 is given by
- int_t0^t1 sqrt(1 + f'(t)^2) dt
- """
- return quad(lambda phi: np.sqrt(1 + fp(eps, a, b, x, phi)**2),
- 0, np.pi,
- epsrel=epsrel, limit=100)[0]
- # grid of minimal arc length values
- data_a = [1e-3, 0.1, 0.5, 0.9, 1, 2, 4, 5, 6, 8]
- data_b = [0, 1, 4, 7, 10]
- data_x = [1, 1.5, 2, 4, 10, 20, 50, 100, 200, 500, 1e3, 5e3, 1e4]
- data_a, data_b, data_x = np.meshgrid(data_a, data_b, data_x)
- data_a, data_b, data_x = (data_a.flatten(), data_b.flatten(),
- data_x.flatten())
- best_eps = []
- for i in range(data_x.size):
- best_eps.append(
- minimize_scalar(lambda eps: arclength(eps, data_a[i], data_b[i],
- data_x[i]),
- bounds=(1e-3, 1000),
- method='Bounded', options={'xatol': 1e-3}).x
- )
- best_eps = np.array(best_eps)
- # pandas would be nice, but here a dictionary is enough
- df = {'a': data_a,
- 'b': data_b,
- 'x': data_x,
- 'eps': best_eps,
- }
- def func(data, A0, A1, A2, A3, A4, A5):
- """Compute parametric function to fit."""
- a = data['a']
- b = data['b']
- x = data['x']
- return (A0 * b * np.exp(-0.5 * a)
- + np.exp(A1 + 1 / (1 + a) * np.log(x) - A2 * np.exp(-A3 * a)
- + A4 / (1 + np.exp(A5 * a))))
- func_params = list(curve_fit(func, df, df['eps'], method='trf')[0])
- s = "Fit optimal eps for integrand P via minimal arc length\n"
- s += "with parametric function:\n"
- s += "optimal_eps = (A0 * b * exp(-a/2) + exp(A1 + 1 / (1 + a) * log(x)\n"
- s += " - A2 * exp(-A3 * a) + A4 / (1 + exp(A5 * a)))\n\n"
- s += "Fitted parameters A0 to A5 are:\n"
- s += ', '.join(['{:.5g}'.format(x) for x in func_params])
- return s
- def main():
- t0 = time()
- parser = ArgumentParser(description=__doc__,
- formatter_class=RawTextHelpFormatter)
- parser.add_argument('action', type=int, choices=[1, 2, 3, 4],
- help='chose what expansion to precompute\n'
- '1 : Series for small a\n'
- '2 : Series for small a and small b\n'
- '3 : Asymptotic series for large x\n'
- ' This may take some time (>4h).\n'
- '4 : Fit optimal eps for integral representation.'
- )
- args = parser.parse_args()
- switch = {1: lambda: print(series_small_a()),
- 2: lambda: print(series_small_a_small_b()),
- 3: lambda: print(asymptotic_series()),
- 4: lambda: print(optimal_epsilon_integral())
- }
- switch.get(args.action, lambda: print("Invalid input."))()
- print("\n{:.1f} minutes elapsed.\n".format((time() - t0)/60))
- if __name__ == '__main__':
- main()
|