hypergeometric.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. r'''
  2. This module contains the implementation of the 2nd_hypergeometric hint for
  3. dsolve. This is an incomplete implementation of the algorithm described in [1].
  4. The algorithm solves 2nd order linear ODEs of the form
  5. .. math:: y'' + A(x) y' + B(x) y = 0\text{,}
  6. where `A` and `B` are rational functions. The algorithm should find any
  7. solution of the form
  8. .. math:: y = P(x) _pF_q(..; ..;\frac{\alpha x^k + \beta}{\gamma x^k + \delta})\text{,}
  9. where pFq is any of 2F1, 1F1 or 0F1 and `P` is an "arbitrary function".
  10. Currently only the 2F1 case is implemented in SymPy but the other cases are
  11. described in the paper and could be implemented in future (contributions
  12. welcome!).
  13. References
  14. ==========
  15. .. [1] L. Chan, E.S. Cheb-Terrab, Non-Liouvillian solutions for second order
  16. linear ODEs, (2004).
  17. https://arxiv.org/abs/math-ph/0402063
  18. '''
  19. from sympy.core import S, Pow
  20. from sympy.core.function import expand
  21. from sympy.core.relational import Eq
  22. from sympy.core.symbol import Symbol, Wild
  23. from sympy.functions import exp, sqrt, hyper
  24. from sympy.integrals import Integral
  25. from sympy.polys import roots, gcd
  26. from sympy.polys.polytools import cancel, factor
  27. from sympy.simplify import collect, simplify, logcombine # type: ignore
  28. from sympy.simplify.powsimp import powdenest
  29. from sympy.solvers.ode.ode import get_numbered_constants
  30. def match_2nd_hypergeometric(eq, func):
  31. x = func.args[0]
  32. df = func.diff(x)
  33. a3 = Wild('a3', exclude=[func, func.diff(x), func.diff(x, 2)])
  34. b3 = Wild('b3', exclude=[func, func.diff(x), func.diff(x, 2)])
  35. c3 = Wild('c3', exclude=[func, func.diff(x), func.diff(x, 2)])
  36. deq = a3*(func.diff(x, 2)) + b3*df + c3*func
  37. r = collect(eq,
  38. [func.diff(x, 2), func.diff(x), func]).match(deq)
  39. if r:
  40. if not all(val.is_polynomial() for val in r.values()):
  41. n, d = eq.as_numer_denom()
  42. eq = expand(n)
  43. r = collect(eq, [func.diff(x, 2), func.diff(x), func]).match(deq)
  44. if r and r[a3]!=0:
  45. A = cancel(r[b3]/r[a3])
  46. B = cancel(r[c3]/r[a3])
  47. return [A, B]
  48. else:
  49. return []
  50. def equivalence_hypergeometric(A, B, func):
  51. # This method for finding the equivalence is only for 2F1 type.
  52. # We can extend it for 1F1 and 0F1 type also.
  53. x = func.args[0]
  54. # making given equation in normal form
  55. I1 = factor(cancel(A.diff(x)/2 + A**2/4 - B))
  56. # computing shifted invariant(J1) of the equation
  57. J1 = factor(cancel(x**2*I1 + S(1)/4))
  58. num, dem = J1.as_numer_denom()
  59. num = powdenest(expand(num))
  60. dem = powdenest(expand(dem))
  61. # this function will compute the different powers of variable(x) in J1.
  62. # then it will help in finding value of k. k is power of x such that we can express
  63. # J1 = x**k * J0(x**k) then all the powers in J0 become integers.
  64. def _power_counting(num):
  65. _pow = {0}
  66. for val in num:
  67. if val.has(x):
  68. if isinstance(val, Pow) and val.as_base_exp()[0] == x:
  69. _pow.add(val.as_base_exp()[1])
  70. elif val == x:
  71. _pow.add(val.as_base_exp()[1])
  72. else:
  73. _pow.update(_power_counting(val.args))
  74. return _pow
  75. pow_num = _power_counting((num, ))
  76. pow_dem = _power_counting((dem, ))
  77. pow_dem.update(pow_num)
  78. _pow = pow_dem
  79. k = gcd(_pow)
  80. # computing I0 of the given equation
  81. I0 = powdenest(simplify(factor(((J1/k**2) - S(1)/4)/((x**k)**2))), force=True)
  82. I0 = factor(cancel(powdenest(I0.subs(x, x**(S(1)/k)), force=True)))
  83. # Before this point I0, J1 might be functions of e.g. sqrt(x) but replacing
  84. # x with x**(1/k) should result in I0 being a rational function of x or
  85. # otherwise the hypergeometric solver cannot be used. Note that k can be a
  86. # non-integer rational such as 2/7.
  87. if not I0.is_rational_function(x):
  88. return None
  89. num, dem = I0.as_numer_denom()
  90. max_num_pow = max(_power_counting((num, )))
  91. dem_args = dem.args
  92. sing_point = []
  93. dem_pow = []
  94. # calculating singular point of I0.
  95. for arg in dem_args:
  96. if arg.has(x):
  97. if isinstance(arg, Pow):
  98. # (x-a)**n
  99. dem_pow.append(arg.as_base_exp()[1])
  100. sing_point.append(list(roots(arg.as_base_exp()[0], x).keys())[0])
  101. else:
  102. # (x-a) type
  103. dem_pow.append(arg.as_base_exp()[1])
  104. sing_point.append(list(roots(arg, x).keys())[0])
  105. dem_pow.sort()
  106. # checking if equivalence is exists or not.
  107. if equivalence(max_num_pow, dem_pow) == "2F1":
  108. return {'I0':I0, 'k':k, 'sing_point':sing_point, 'type':"2F1"}
  109. else:
  110. return None
  111. def match_2nd_2F1_hypergeometric(I, k, sing_point, func):
  112. x = func.args[0]
  113. a = Wild("a")
  114. b = Wild("b")
  115. c = Wild("c")
  116. t = Wild("t")
  117. s = Wild("s")
  118. r = Wild("r")
  119. alpha = Wild("alpha")
  120. beta = Wild("beta")
  121. gamma = Wild("gamma")
  122. delta = Wild("delta")
  123. # I0 of the standerd 2F1 equation.
  124. I0 = ((a-b+1)*(a-b-1)*x**2 + 2*((1-a-b)*c + 2*a*b)*x + c*(c-2))/(4*x**2*(x-1)**2)
  125. if sing_point != [0, 1]:
  126. # If singular point is [0, 1] then we have standerd equation.
  127. eqs = []
  128. sing_eqs = [-beta/alpha, -delta/gamma, (delta-beta)/(alpha-gamma)]
  129. # making equations for the finding the mobius transformation
  130. for i in range(3):
  131. if i<len(sing_point):
  132. eqs.append(Eq(sing_eqs[i], sing_point[i]))
  133. else:
  134. eqs.append(Eq(1/sing_eqs[i], 0))
  135. # solving above equations for the mobius transformation
  136. _beta = -alpha*sing_point[0]
  137. _delta = -gamma*sing_point[1]
  138. _gamma = alpha
  139. if len(sing_point) == 3:
  140. _gamma = (_beta + sing_point[2]*alpha)/(sing_point[2] - sing_point[1])
  141. mob = (alpha*x + beta)/(gamma*x + delta)
  142. mob = mob.subs(beta, _beta)
  143. mob = mob.subs(delta, _delta)
  144. mob = mob.subs(gamma, _gamma)
  145. mob = cancel(mob)
  146. t = (beta - delta*x)/(gamma*x - alpha)
  147. t = cancel(((t.subs(beta, _beta)).subs(delta, _delta)).subs(gamma, _gamma))
  148. else:
  149. mob = x
  150. t = x
  151. # applying mobius transformation in I to make it into I0.
  152. I = I.subs(x, t)
  153. I = I*(t.diff(x))**2
  154. I = factor(I)
  155. dict_I = {x**2:0, x:0, 1:0}
  156. I0_num, I0_dem = I0.as_numer_denom()
  157. # collecting coeff of (x**2, x), of the standerd equation.
  158. # substituting (a-b) = s, (a+b) = r
  159. dict_I0 = {x**2:s**2 - 1, x:(2*(1-r)*c + (r+s)*(r-s)), 1:c*(c-2)}
  160. # collecting coeff of (x**2, x) from I0 of the given equation.
  161. dict_I.update(collect(expand(cancel(I*I0_dem)), [x**2, x], evaluate=False))
  162. eqs = []
  163. # We are comparing the coeff of powers of different x, for finding the values of
  164. # parameters of standerd equation.
  165. for key in [x**2, x, 1]:
  166. eqs.append(Eq(dict_I[key], dict_I0[key]))
  167. # We can have many possible roots for the equation.
  168. # I am selecting the root on the basis that when we have
  169. # standard equation eq = x*(x-1)*f(x).diff(x, 2) + ((a+b+1)*x-c)*f(x).diff(x) + a*b*f(x)
  170. # then root should be a, b, c.
  171. _c = 1 - factor(sqrt(1+eqs[2].lhs))
  172. if not _c.has(Symbol):
  173. _c = min(list(roots(eqs[2], c)))
  174. _s = factor(sqrt(eqs[0].lhs + 1))
  175. _r = _c - factor(sqrt(_c**2 + _s**2 + eqs[1].lhs - 2*_c))
  176. _a = (_r + _s)/2
  177. _b = (_r - _s)/2
  178. rn = {'a':simplify(_a), 'b':simplify(_b), 'c':simplify(_c), 'k':k, 'mobius':mob, 'type':"2F1"}
  179. return rn
  180. def equivalence(max_num_pow, dem_pow):
  181. # this function is made for checking the equivalence with 2F1 type of equation.
  182. # max_num_pow is the value of maximum power of x in numerator
  183. # and dem_pow is list of powers of different factor of form (a*x b).
  184. # reference from table 1 in paper - "Non-Liouvillian solutions for second order
  185. # linear ODEs" by L. Chan, E.S. Cheb-Terrab.
  186. # We can extend it for 1F1 and 0F1 type also.
  187. if max_num_pow == 2:
  188. if dem_pow in [[2, 2], [2, 2, 2]]:
  189. return "2F1"
  190. elif max_num_pow == 1:
  191. if dem_pow in [[1, 2, 2], [2, 2, 2], [1, 2], [2, 2]]:
  192. return "2F1"
  193. elif max_num_pow == 0:
  194. if dem_pow in [[1, 1, 2], [2, 2], [1, 2, 2], [1, 1], [2], [1, 2], [2, 2]]:
  195. return "2F1"
  196. return None
  197. def get_sol_2F1_hypergeometric(eq, func, match_object):
  198. x = func.args[0]
  199. from sympy.simplify.hyperexpand import hyperexpand
  200. from sympy.polys.polytools import factor
  201. C0, C1 = get_numbered_constants(eq, num=2)
  202. a = match_object['a']
  203. b = match_object['b']
  204. c = match_object['c']
  205. A = match_object['A']
  206. sol = None
  207. if c.is_integer == False:
  208. sol = C0*hyper([a, b], [c], x) + C1*hyper([a-c+1, b-c+1], [2-c], x)*x**(1-c)
  209. elif c == 1:
  210. y2 = Integral(exp(Integral((-(a+b+1)*x + c)/(x**2-x), x))/(hyperexpand(hyper([a, b], [c], x))**2), x)*hyper([a, b], [c], x)
  211. sol = C0*hyper([a, b], [c], x) + C1*y2
  212. elif (c-a-b).is_integer == False:
  213. sol = C0*hyper([a, b], [1+a+b-c], 1-x) + C1*hyper([c-a, c-b], [1+c-a-b], 1-x)*(1-x)**(c-a-b)
  214. if sol:
  215. # applying transformation in the solution
  216. subs = match_object['mobius']
  217. dtdx = simplify(1/(subs.diff(x)))
  218. _B = ((a + b + 1)*x - c).subs(x, subs)*dtdx
  219. _B = factor(_B + ((x**2 -x).subs(x, subs))*(dtdx.diff(x)*dtdx))
  220. _A = factor((x**2 - x).subs(x, subs)*(dtdx**2))
  221. e = exp(logcombine(Integral(cancel(_B/(2*_A)), x), force=True))
  222. sol = sol.subs(x, match_object['mobius'])
  223. sol = sol.subs(x, x**match_object['k'])
  224. e = e.subs(x, x**match_object['k'])
  225. if not A.is_zero:
  226. e1 = Integral(A/2, x)
  227. e1 = exp(logcombine(e1, force=True))
  228. sol = cancel((e/e1)*x**((-match_object['k']+1)/2))*sol
  229. sol = Eq(func, sol)
  230. return sol
  231. sol = cancel((e)*x**((-match_object['k']+1)/2))*sol
  232. sol = Eq(func, sol)
  233. return sol