wright_bessel.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. """Precompute coefficients of several series expansions
  2. of Wright's generalized Bessel function Phi(a, b, x).
  3. See https://dlmf.nist.gov/10.46.E1 with rho=a, beta=b, z=x.
  4. """
  5. from argparse import ArgumentParser, RawTextHelpFormatter
  6. import numpy as np
  7. from scipy.integrate import quad
  8. from scipy.optimize import minimize_scalar, curve_fit
  9. from time import time
  10. try:
  11. import sympy
  12. from sympy import EulerGamma, Rational, S, Sum, \
  13. factorial, gamma, gammasimp, pi, polygamma, symbols, zeta
  14. from sympy.polys.polyfuncs import horner
  15. except ImportError:
  16. pass
  17. def series_small_a():
  18. """Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.
  19. """
  20. order = 5
  21. a, b, x, k = symbols("a b x k")
  22. A = [] # terms with a
  23. X = [] # terms with x
  24. B = [] # terms with b (polygammas)
  25. # Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i])
  26. expression = Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
  27. expression = gamma(b)/sympy.exp(x) * expression
  28. # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
  29. for n in range(0, order+1):
  30. term = expression.diff(a, n).subs(a, 0).simplify().doit()
  31. # set the whole bracket involving polygammas to 1
  32. x_part = (term.subs(polygamma(0, b), 1)
  33. .replace(polygamma, lambda *args: 0))
  34. # sign convetion: x part always positive
  35. x_part *= (-1)**n
  36. A.append(a**n/factorial(n))
  37. X.append(horner(x_part))
  38. B.append(horner((term/x_part).simplify()))
  39. s = "Tylor series expansion of Phi(a, b, x) in a=0 up to order 5.\n"
  40. s += "Phi(a, b, x) = exp(x)/gamma(b) * sum(A[i] * X[i] * B[i], i=0..5)\n"
  41. for name, c in zip(['A', 'X', 'B'], [A, X, B]):
  42. for i in range(len(c)):
  43. s += f"\n{name}[{i}] = " + str(c[i])
  44. return s
  45. # expansion of digamma
  46. def dg_series(z, n):
  47. """Symbolic expansion of digamma(z) in z=0 to order n.
  48. See https://dlmf.nist.gov/5.7.E4 and with https://dlmf.nist.gov/5.5.E2
  49. """
  50. k = symbols("k")
  51. return -1/z - EulerGamma + \
  52. sympy.summation((-1)**k * zeta(k) * z**(k-1), (k, 2, n+1))
  53. def pg_series(k, z, n):
  54. """Symbolic expansion of polygamma(k, z) in z=0 to order n."""
  55. return sympy.diff(dg_series(z, n+k), z, k)
  56. def series_small_a_small_b():
  57. """Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5.
  58. Be aware of cancellation of poles in b=0 of digamma(b)/Gamma(b) and
  59. polygamma functions.
  60. digamma(b)/Gamma(b) = -1 - 2*M_EG*b + O(b^2)
  61. digamma(b)^2/Gamma(b) = 1/b + 3*M_EG + b*(-5/12*PI^2+7/2*M_EG^2) + O(b^2)
  62. polygamma(1, b)/Gamma(b) = 1/b + M_EG + b*(1/12*PI^2 + 1/2*M_EG^2) + O(b^2)
  63. and so on.
  64. """
  65. order = 5
  66. a, b, x, k = symbols("a b x k")
  67. M_PI, M_EG, M_Z3 = symbols("M_PI M_EG M_Z3")
  68. c_subs = {pi: M_PI, EulerGamma: M_EG, zeta(3): M_Z3}
  69. A = [] # terms with a
  70. X = [] # terms with x
  71. B = [] # terms with b (polygammas expanded)
  72. C = [] # terms that generate B
  73. # Phi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i])
  74. # B[0] = 1
  75. # B[k] = sum(C[k] * b**k/k!, k=0..)
  76. # Note: C[k] can be obtained from a series expansion of 1/gamma(b).
  77. expression = gamma(b)/sympy.exp(x) * \
  78. Sum(x**k/factorial(k)/gamma(a*k+b), (k, 0, S.Infinity))
  79. # nth term of taylor series in a=0: a^n/n! * (d^n Phi(a, b, x)/da^n at a=0)
  80. for n in range(0, order+1):
  81. term = expression.diff(a, n).subs(a, 0).simplify().doit()
  82. # set the whole bracket involving polygammas to 1
  83. x_part = (term.subs(polygamma(0, b), 1)
  84. .replace(polygamma, lambda *args: 0))
  85. # sign convetion: x part always positive
  86. x_part *= (-1)**n
  87. # expansion of polygamma part with 1/gamma(b)
  88. pg_part = term/x_part/gamma(b)
  89. if n >= 1:
  90. # Note: highest term is digamma^n
  91. pg_part = pg_part.replace(polygamma,
  92. lambda k, x: pg_series(k, x, order+1+n))
  93. pg_part = (pg_part.series(b, 0, n=order+1-n)
  94. .removeO()
  95. .subs(polygamma(2, 1), -2*zeta(3))
  96. .simplify()
  97. )
  98. A.append(a**n/factorial(n))
  99. X.append(horner(x_part))
  100. B.append(pg_part)
  101. # Calculate C and put in the k!
  102. C = sympy.Poly(B[1].subs(c_subs), b).coeffs()
  103. C.reverse()
  104. for i in range(len(C)):
  105. C[i] = (C[i] * factorial(i)).simplify()
  106. s = "Tylor series expansion of Phi(a, b, x) in a=0 and b=0 up to order 5."
  107. s += "\nPhi(a, b, x) = exp(x) * sum(A[i] * X[i] * B[i], i=0..5)\n"
  108. s += "B[0] = 1\n"
  109. s += "B[i] = sum(C[k+i-1] * b**k/k!, k=0..)\n"
  110. s += "\nM_PI = pi"
  111. s += "\nM_EG = EulerGamma"
  112. s += "\nM_Z3 = zeta(3)"
  113. for name, c in zip(['A', 'X'], [A, X]):
  114. for i in range(len(c)):
  115. s += f"\n{name}[{i}] = "
  116. s += str(c[i])
  117. # For C, do also compute the values numerically
  118. for i in range(len(C)):
  119. s += f"\n# C[{i}] = "
  120. s += str(C[i])
  121. s += f"\nC[{i}] = "
  122. s += str(C[i].subs({M_EG: EulerGamma, M_PI: pi, M_Z3: zeta(3)})
  123. .evalf(17))
  124. # Does B have the assumed structure?
  125. s += "\n\nTest if B[i] does have the assumed structure."
  126. s += "\nC[i] are derived from B[1] allone."
  127. s += "\nTest B[2] == C[1] + b*C[2] + b^2/2*C[3] + b^3/6*C[4] + .."
  128. test = sum([b**k/factorial(k) * C[k+1] for k in range(order-1)])
  129. test = (test - B[2].subs(c_subs)).simplify()
  130. s += f"\ntest successful = {test==S(0)}"
  131. s += "\nTest B[3] == C[2] + b*C[3] + b^2/2*C[4] + .."
  132. test = sum([b**k/factorial(k) * C[k+2] for k in range(order-2)])
  133. test = (test - B[3].subs(c_subs)).simplify()
  134. s += f"\ntest successful = {test==S(0)}"
  135. return s
  136. def asymptotic_series():
  137. """Asymptotic expansion for large x.
  138. Phi(a, b, x) ~ Z^(1/2-b) * exp((1+a)/a * Z) * sum_k (-1)^k * C_k / Z^k
  139. Z = (a*x)^(1/(1+a))
  140. Wright (1935) lists the coefficients C_0 and C_1 (he calls them a_0 and
  141. a_1). With slightly different notation, Paris (2017) lists coefficients
  142. c_k up to order k=3.
  143. Paris (2017) uses ZP = (1+a)/a * Z (ZP = Z of Paris) and
  144. C_k = C_0 * (-a/(1+a))^k * c_k
  145. """
  146. order = 8
  147. class g(sympy.Function):
  148. """Helper function g according to Wright (1935)
  149. g(n, rho, v) = (1 + (rho+2)/3 * v + (rho+2)*(rho+3)/(2*3) * v^2 + ...)
  150. Note: Wright (1935) uses square root of above definition.
  151. """
  152. nargs = 3
  153. @classmethod
  154. def eval(cls, n, rho, v):
  155. if not n >= 0:
  156. raise ValueError("must have n >= 0")
  157. elif n == 0:
  158. return 1
  159. else:
  160. return g(n-1, rho, v) \
  161. + gammasimp(gamma(rho+2+n)/gamma(rho+2)) \
  162. / gammasimp(gamma(3+n)/gamma(3))*v**n
  163. class coef_C(sympy.Function):
  164. """Calculate coefficients C_m for integer m.
  165. C_m is the coefficient of v^(2*m) in the Taylor expansion in v=0 of
  166. Gamma(m+1/2)/(2*pi) * (2/(rho+1))^(m+1/2) * (1-v)^(-b)
  167. * g(rho, v)^(-m-1/2)
  168. """
  169. nargs = 3
  170. @classmethod
  171. def eval(cls, m, rho, beta):
  172. if not m >= 0:
  173. raise ValueError("must have m >= 0")
  174. v = symbols("v")
  175. expression = (1-v)**(-beta) * g(2*m, rho, v)**(-m-Rational(1, 2))
  176. res = expression.diff(v, 2*m).subs(v, 0) / factorial(2*m)
  177. res = res * (gamma(m + Rational(1, 2)) / (2*pi)
  178. * (2/(rho+1))**(m + Rational(1, 2)))
  179. return res
  180. # in order to have nice ordering/sorting of expressions, we set a = xa.
  181. xa, b, xap1 = symbols("xa b xap1")
  182. C0 = coef_C(0, xa, b)
  183. # a1 = a(1, rho, beta)
  184. s = "Asymptotic expansion for large x\n"
  185. s += "Phi(a, b, x) = Z**(1/2-b) * exp((1+a)/a * Z) \n"
  186. s += " * sum((-1)**k * C[k]/Z**k, k=0..6)\n\n"
  187. s += "Z = pow(a * x, 1/(1+a))\n"
  188. s += "A[k] = pow(a, k)\n"
  189. s += "B[k] = pow(b, k)\n"
  190. s += "Ap1[k] = pow(1+a, k)\n\n"
  191. s += "C[0] = 1./sqrt(2. * M_PI * Ap1[1])\n"
  192. for i in range(1, order+1):
  193. expr = (coef_C(i, xa, b) / (C0/(1+xa)**i)).simplify()
  194. factor = [x.denominator() for x in sympy.Poly(expr).coeffs()]
  195. factor = sympy.lcm(factor)
  196. expr = (expr * factor).simplify().collect(b, sympy.factor)
  197. expr = expr.xreplace({xa+1: xap1})
  198. s += f"C[{i}] = C[0] / ({factor} * Ap1[{i}])\n"
  199. s += f"C[{i}] *= {str(expr)}\n\n"
  200. import re
  201. re_a = re.compile(r'xa\*\*(\d+)')
  202. s = re_a.sub(r'A[\1]', s)
  203. re_b = re.compile(r'b\*\*(\d+)')
  204. s = re_b.sub(r'B[\1]', s)
  205. s = s.replace('xap1', 'Ap1[1]')
  206. s = s.replace('xa', 'a')
  207. # max integer = 2^31-1 = 2,147,483,647. Solution: Put a point after 10
  208. # or more digits.
  209. re_digits = re.compile(r'(\d{10,})')
  210. s = re_digits.sub(r'\1.', s)
  211. return s
  212. def optimal_epsilon_integral():
  213. """Fit optimal choice of epsilon for integral representation.
  214. The integrand of
  215. int_0^pi P(eps, a, b, x, phi) * dphi
  216. can exhibit oscillatory behaviour. It stems from the cosine of P and can be
  217. minimized by minimizing the arc length of the argument
  218. f(phi) = eps * sin(phi) - x * eps^(-a) * sin(a * phi) + (1 - b) * phi
  219. of cos(f(phi)).
  220. We minimize the arc length in eps for a grid of values (a, b, x) and fit a
  221. parametric function to it.
  222. """
  223. def fp(eps, a, b, x, phi):
  224. """Derivative of f w.r.t. phi."""
  225. eps_a = np.power(1. * eps, -a)
  226. return eps * np.cos(phi) - a * x * eps_a * np.cos(a * phi) + 1 - b
  227. def arclength(eps, a, b, x, epsrel=1e-2, limit=100):
  228. """Compute Arc length of f.
  229. Note that the arg length of a function f fro t0 to t1 is given by
  230. int_t0^t1 sqrt(1 + f'(t)^2) dt
  231. """
  232. return quad(lambda phi: np.sqrt(1 + fp(eps, a, b, x, phi)**2),
  233. 0, np.pi,
  234. epsrel=epsrel, limit=100)[0]
  235. # grid of minimal arc length values
  236. data_a = [1e-3, 0.1, 0.5, 0.9, 1, 2, 4, 5, 6, 8]
  237. data_b = [0, 1, 4, 7, 10]
  238. data_x = [1, 1.5, 2, 4, 10, 20, 50, 100, 200, 500, 1e3, 5e3, 1e4]
  239. data_a, data_b, data_x = np.meshgrid(data_a, data_b, data_x)
  240. data_a, data_b, data_x = (data_a.flatten(), data_b.flatten(),
  241. data_x.flatten())
  242. best_eps = []
  243. for i in range(data_x.size):
  244. best_eps.append(
  245. minimize_scalar(lambda eps: arclength(eps, data_a[i], data_b[i],
  246. data_x[i]),
  247. bounds=(1e-3, 1000),
  248. method='Bounded', options={'xatol': 1e-3}).x
  249. )
  250. best_eps = np.array(best_eps)
  251. # pandas would be nice, but here a dictionary is enough
  252. df = {'a': data_a,
  253. 'b': data_b,
  254. 'x': data_x,
  255. 'eps': best_eps,
  256. }
  257. def func(data, A0, A1, A2, A3, A4, A5):
  258. """Compute parametric function to fit."""
  259. a = data['a']
  260. b = data['b']
  261. x = data['x']
  262. return (A0 * b * np.exp(-0.5 * a)
  263. + np.exp(A1 + 1 / (1 + a) * np.log(x) - A2 * np.exp(-A3 * a)
  264. + A4 / (1 + np.exp(A5 * a))))
  265. func_params = list(curve_fit(func, df, df['eps'], method='trf')[0])
  266. s = "Fit optimal eps for integrand P via minimal arc length\n"
  267. s += "with parametric function:\n"
  268. s += "optimal_eps = (A0 * b * exp(-a/2) + exp(A1 + 1 / (1 + a) * log(x)\n"
  269. s += " - A2 * exp(-A3 * a) + A4 / (1 + exp(A5 * a)))\n\n"
  270. s += "Fitted parameters A0 to A5 are:\n"
  271. s += ', '.join(['{:.5g}'.format(x) for x in func_params])
  272. return s
  273. def main():
  274. t0 = time()
  275. parser = ArgumentParser(description=__doc__,
  276. formatter_class=RawTextHelpFormatter)
  277. parser.add_argument('action', type=int, choices=[1, 2, 3, 4],
  278. help='chose what expansion to precompute\n'
  279. '1 : Series for small a\n'
  280. '2 : Series for small a and small b\n'
  281. '3 : Asymptotic series for large x\n'
  282. ' This may take some time (>4h).\n'
  283. '4 : Fit optimal eps for integral representation.'
  284. )
  285. args = parser.parse_args()
  286. switch = {1: lambda: print(series_small_a()),
  287. 2: lambda: print(series_small_a_small_b()),
  288. 3: lambda: print(asymptotic_series()),
  289. 4: lambda: print(optimal_epsilon_integral())
  290. }
  291. switch.get(args.action, lambda: print("Invalid input."))()
  292. print("\n{:.1f} minutes elapsed.\n".format((time() - t0)/60))
  293. if __name__ == '__main__':
  294. main()