Coverage for /usr/lib/python3/dist-packages/sympy/solvers/ode/hypergeometric.py: 9%

171 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1r''' 

2This module contains the implementation of the 2nd_hypergeometric hint for 

3dsolve. This is an incomplete implementation of the algorithm described in [1]. 

4The algorithm solves 2nd order linear ODEs of the form 

5 

6.. math:: y'' + A(x) y' + B(x) y = 0\text{,} 

7 

8where `A` and `B` are rational functions. The algorithm should find any 

9solution of the form 

10 

11.. math:: y = P(x) _pF_q(..; ..;\frac{\alpha x^k + \beta}{\gamma x^k + \delta})\text{,} 

12 

13where pFq is any of 2F1, 1F1 or 0F1 and `P` is an "arbitrary function". 

14Currently only the 2F1 case is implemented in SymPy but the other cases are 

15described in the paper and could be implemented in future (contributions 

16welcome!). 

17 

18References 

19========== 

20 

21.. [1] L. Chan, E.S. Cheb-Terrab, Non-Liouvillian solutions for second order 

22 linear ODEs, (2004). 

23 https://arxiv.org/abs/math-ph/0402063 

24''' 

25 

26from sympy.core import S, Pow 

27from sympy.core.function import expand 

28from sympy.core.relational import Eq 

29from sympy.core.symbol import Symbol, Wild 

30from sympy.functions import exp, sqrt, hyper 

31from sympy.integrals import Integral 

32from sympy.polys import roots, gcd 

33from sympy.polys.polytools import cancel, factor 

34from sympy.simplify import collect, simplify, logcombine # type: ignore 

35from sympy.simplify.powsimp import powdenest 

36from sympy.solvers.ode.ode import get_numbered_constants 

37 

38 

39def match_2nd_hypergeometric(eq, func): 

40 x = func.args[0] 

41 df = func.diff(x) 

42 a3 = Wild('a3', exclude=[func, func.diff(x), func.diff(x, 2)]) 

43 b3 = Wild('b3', exclude=[func, func.diff(x), func.diff(x, 2)]) 

44 c3 = Wild('c3', exclude=[func, func.diff(x), func.diff(x, 2)]) 

45 deq = a3*(func.diff(x, 2)) + b3*df + c3*func 

46 r = collect(eq, 

47 [func.diff(x, 2), func.diff(x), func]).match(deq) 

48 if r: 

49 if not all(val.is_polynomial() for val in r.values()): 

50 n, d = eq.as_numer_denom() 

51 eq = expand(n) 

52 r = collect(eq, [func.diff(x, 2), func.diff(x), func]).match(deq) 

53 

54 if r and r[a3]!=0: 

55 A = cancel(r[b3]/r[a3]) 

56 B = cancel(r[c3]/r[a3]) 

57 return [A, B] 

58 else: 

59 return [] 

60 

61 

62def equivalence_hypergeometric(A, B, func): 

63 # This method for finding the equivalence is only for 2F1 type. 

64 # We can extend it for 1F1 and 0F1 type also. 

65 x = func.args[0] 

66 

67 # making given equation in normal form 

68 I1 = factor(cancel(A.diff(x)/2 + A**2/4 - B)) 

69 

70 # computing shifted invariant(J1) of the equation 

71 J1 = factor(cancel(x**2*I1 + S(1)/4)) 

72 num, dem = J1.as_numer_denom() 

73 num = powdenest(expand(num)) 

74 dem = powdenest(expand(dem)) 

75 # this function will compute the different powers of variable(x) in J1. 

76 # then it will help in finding value of k. k is power of x such that we can express 

77 # J1 = x**k * J0(x**k) then all the powers in J0 become integers. 

78 def _power_counting(num): 

79 _pow = {0} 

80 for val in num: 

81 if val.has(x): 

82 if isinstance(val, Pow) and val.as_base_exp()[0] == x: 

83 _pow.add(val.as_base_exp()[1]) 

84 elif val == x: 

85 _pow.add(val.as_base_exp()[1]) 

86 else: 

87 _pow.update(_power_counting(val.args)) 

88 return _pow 

89 

90 pow_num = _power_counting((num, )) 

91 pow_dem = _power_counting((dem, )) 

92 pow_dem.update(pow_num) 

93 

94 _pow = pow_dem 

95 k = gcd(_pow) 

96 

97 # computing I0 of the given equation 

98 I0 = powdenest(simplify(factor(((J1/k**2) - S(1)/4)/((x**k)**2))), force=True) 

99 I0 = factor(cancel(powdenest(I0.subs(x, x**(S(1)/k)), force=True))) 

100 

101 # Before this point I0, J1 might be functions of e.g. sqrt(x) but replacing 

102 # x with x**(1/k) should result in I0 being a rational function of x or 

103 # otherwise the hypergeometric solver cannot be used. Note that k can be a 

104 # non-integer rational such as 2/7. 

105 if not I0.is_rational_function(x): 

106 return None 

107 

108 num, dem = I0.as_numer_denom() 

109 

110 max_num_pow = max(_power_counting((num, ))) 

111 dem_args = dem.args 

112 sing_point = [] 

113 dem_pow = [] 

114 # calculating singular point of I0. 

115 for arg in dem_args: 

116 if arg.has(x): 

117 if isinstance(arg, Pow): 

118 # (x-a)**n 

119 dem_pow.append(arg.as_base_exp()[1]) 

120 sing_point.append(list(roots(arg.as_base_exp()[0], x).keys())[0]) 

121 else: 

122 # (x-a) type 

123 dem_pow.append(arg.as_base_exp()[1]) 

124 sing_point.append(list(roots(arg, x).keys())[0]) 

125 

126 dem_pow.sort() 

127 # checking if equivalence is exists or not. 

128 

129 if equivalence(max_num_pow, dem_pow) == "2F1": 

130 return {'I0':I0, 'k':k, 'sing_point':sing_point, 'type':"2F1"} 

131 else: 

132 return None 

133 

134 

135def match_2nd_2F1_hypergeometric(I, k, sing_point, func): 

136 x = func.args[0] 

137 a = Wild("a") 

138 b = Wild("b") 

139 c = Wild("c") 

140 t = Wild("t") 

141 s = Wild("s") 

142 r = Wild("r") 

143 alpha = Wild("alpha") 

144 beta = Wild("beta") 

145 gamma = Wild("gamma") 

146 delta = Wild("delta") 

147 # I0 of the standerd 2F1 equation. 

148 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) 

149 if sing_point != [0, 1]: 

150 # If singular point is [0, 1] then we have standerd equation. 

151 eqs = [] 

152 sing_eqs = [-beta/alpha, -delta/gamma, (delta-beta)/(alpha-gamma)] 

153 # making equations for the finding the mobius transformation 

154 for i in range(3): 

155 if i<len(sing_point): 

156 eqs.append(Eq(sing_eqs[i], sing_point[i])) 

157 else: 

158 eqs.append(Eq(1/sing_eqs[i], 0)) 

159 # solving above equations for the mobius transformation 

160 _beta = -alpha*sing_point[0] 

161 _delta = -gamma*sing_point[1] 

162 _gamma = alpha 

163 if len(sing_point) == 3: 

164 _gamma = (_beta + sing_point[2]*alpha)/(sing_point[2] - sing_point[1]) 

165 mob = (alpha*x + beta)/(gamma*x + delta) 

166 mob = mob.subs(beta, _beta) 

167 mob = mob.subs(delta, _delta) 

168 mob = mob.subs(gamma, _gamma) 

169 mob = cancel(mob) 

170 t = (beta - delta*x)/(gamma*x - alpha) 

171 t = cancel(((t.subs(beta, _beta)).subs(delta, _delta)).subs(gamma, _gamma)) 

172 else: 

173 mob = x 

174 t = x 

175 

176 # applying mobius transformation in I to make it into I0. 

177 I = I.subs(x, t) 

178 I = I*(t.diff(x))**2 

179 I = factor(I) 

180 dict_I = {x**2:0, x:0, 1:0} 

181 I0_num, I0_dem = I0.as_numer_denom() 

182 # collecting coeff of (x**2, x), of the standerd equation. 

183 # substituting (a-b) = s, (a+b) = r 

184 dict_I0 = {x**2:s**2 - 1, x:(2*(1-r)*c + (r+s)*(r-s)), 1:c*(c-2)} 

185 # collecting coeff of (x**2, x) from I0 of the given equation. 

186 dict_I.update(collect(expand(cancel(I*I0_dem)), [x**2, x], evaluate=False)) 

187 eqs = [] 

188 # We are comparing the coeff of powers of different x, for finding the values of 

189 # parameters of standerd equation. 

190 for key in [x**2, x, 1]: 

191 eqs.append(Eq(dict_I[key], dict_I0[key])) 

192 

193 # We can have many possible roots for the equation. 

194 # I am selecting the root on the basis that when we have 

195 # standard equation eq = x*(x-1)*f(x).diff(x, 2) + ((a+b+1)*x-c)*f(x).diff(x) + a*b*f(x) 

196 # then root should be a, b, c. 

197 

198 _c = 1 - factor(sqrt(1+eqs[2].lhs)) 

199 if not _c.has(Symbol): 

200 _c = min(list(roots(eqs[2], c))) 

201 _s = factor(sqrt(eqs[0].lhs + 1)) 

202 _r = _c - factor(sqrt(_c**2 + _s**2 + eqs[1].lhs - 2*_c)) 

203 _a = (_r + _s)/2 

204 _b = (_r - _s)/2 

205 

206 rn = {'a':simplify(_a), 'b':simplify(_b), 'c':simplify(_c), 'k':k, 'mobius':mob, 'type':"2F1"} 

207 return rn 

208 

209 

210def equivalence(max_num_pow, dem_pow): 

211 # this function is made for checking the equivalence with 2F1 type of equation. 

212 # max_num_pow is the value of maximum power of x in numerator 

213 # and dem_pow is list of powers of different factor of form (a*x b). 

214 # reference from table 1 in paper - "Non-Liouvillian solutions for second order 

215 # linear ODEs" by L. Chan, E.S. Cheb-Terrab. 

216 # We can extend it for 1F1 and 0F1 type also. 

217 

218 if max_num_pow == 2: 

219 if dem_pow in [[2, 2], [2, 2, 2]]: 

220 return "2F1" 

221 elif max_num_pow == 1: 

222 if dem_pow in [[1, 2, 2], [2, 2, 2], [1, 2], [2, 2]]: 

223 return "2F1" 

224 elif max_num_pow == 0: 

225 if dem_pow in [[1, 1, 2], [2, 2], [1, 2, 2], [1, 1], [2], [1, 2], [2, 2]]: 

226 return "2F1" 

227 

228 return None 

229 

230 

231def get_sol_2F1_hypergeometric(eq, func, match_object): 

232 x = func.args[0] 

233 from sympy.simplify.hyperexpand import hyperexpand 

234 from sympy.polys.polytools import factor 

235 C0, C1 = get_numbered_constants(eq, num=2) 

236 a = match_object['a'] 

237 b = match_object['b'] 

238 c = match_object['c'] 

239 A = match_object['A'] 

240 

241 sol = None 

242 

243 if c.is_integer == False: 

244 sol = C0*hyper([a, b], [c], x) + C1*hyper([a-c+1, b-c+1], [2-c], x)*x**(1-c) 

245 elif c == 1: 

246 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) 

247 sol = C0*hyper([a, b], [c], x) + C1*y2 

248 elif (c-a-b).is_integer == False: 

249 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) 

250 

251 if sol: 

252 # applying transformation in the solution 

253 subs = match_object['mobius'] 

254 dtdx = simplify(1/(subs.diff(x))) 

255 _B = ((a + b + 1)*x - c).subs(x, subs)*dtdx 

256 _B = factor(_B + ((x**2 -x).subs(x, subs))*(dtdx.diff(x)*dtdx)) 

257 _A = factor((x**2 - x).subs(x, subs)*(dtdx**2)) 

258 e = exp(logcombine(Integral(cancel(_B/(2*_A)), x), force=True)) 

259 sol = sol.subs(x, match_object['mobius']) 

260 sol = sol.subs(x, x**match_object['k']) 

261 e = e.subs(x, x**match_object['k']) 

262 

263 if not A.is_zero: 

264 e1 = Integral(A/2, x) 

265 e1 = exp(logcombine(e1, force=True)) 

266 sol = cancel((e/e1)*x**((-match_object['k']+1)/2))*sol 

267 sol = Eq(func, sol) 

268 return sol 

269 

270 sol = cancel((e)*x**((-match_object['k']+1)/2))*sol 

271 sol = Eq(func, sol) 

272 return sol