273 lines
9.8 KiB
Python
273 lines
9.8 KiB
Python
r'''
|
|
This module contains the implementation of the 2nd_hypergeometric hint for
|
|
dsolve. This is an incomplete implementation of the algorithm described in [1].
|
|
The algorithm solves 2nd order linear ODEs of the form
|
|
|
|
.. math:: y'' + A(x) y' + B(x) y = 0\text{,}
|
|
|
|
where `A` and `B` are rational functions. The algorithm should find any
|
|
solution of the form
|
|
|
|
.. math:: y = P(x) _pF_q(..; ..;\frac{\alpha x^k + \beta}{\gamma x^k + \delta})\text{,}
|
|
|
|
where pFq is any of 2F1, 1F1 or 0F1 and `P` is an "arbitrary function".
|
|
Currently only the 2F1 case is implemented in SymPy but the other cases are
|
|
described in the paper and could be implemented in future (contributions
|
|
welcome!).
|
|
|
|
References
|
|
==========
|
|
|
|
.. [1] L. Chan, E.S. Cheb-Terrab, Non-Liouvillian solutions for second order
|
|
linear ODEs, (2004).
|
|
https://arxiv.org/abs/math-ph/0402063
|
|
'''
|
|
|
|
from sympy.core import S, Pow
|
|
from sympy.core.function import expand
|
|
from sympy.core.relational import Eq
|
|
from sympy.core.symbol import Symbol, Wild
|
|
from sympy.functions import exp, sqrt, hyper
|
|
from sympy.integrals import Integral
|
|
from sympy.polys import roots, gcd
|
|
from sympy.polys.polytools import cancel, factor
|
|
from sympy.simplify import collect, simplify, logcombine # type: ignore
|
|
from sympy.simplify.powsimp import powdenest
|
|
from sympy.solvers.ode.ode import get_numbered_constants
|
|
|
|
|
|
def match_2nd_hypergeometric(eq, func):
|
|
x = func.args[0]
|
|
df = func.diff(x)
|
|
a3 = Wild('a3', exclude=[func, func.diff(x), func.diff(x, 2)])
|
|
b3 = Wild('b3', exclude=[func, func.diff(x), func.diff(x, 2)])
|
|
c3 = Wild('c3', exclude=[func, func.diff(x), func.diff(x, 2)])
|
|
deq = a3*(func.diff(x, 2)) + b3*df + c3*func
|
|
r = collect(eq,
|
|
[func.diff(x, 2), func.diff(x), func]).match(deq)
|
|
if r:
|
|
if not all(val.is_polynomial() for val in r.values()):
|
|
n, d = eq.as_numer_denom()
|
|
eq = expand(n)
|
|
r = collect(eq, [func.diff(x, 2), func.diff(x), func]).match(deq)
|
|
|
|
if r and r[a3]!=0:
|
|
A = cancel(r[b3]/r[a3])
|
|
B = cancel(r[c3]/r[a3])
|
|
return [A, B]
|
|
else:
|
|
return []
|
|
|
|
|
|
def equivalence_hypergeometric(A, B, func):
|
|
# This method for finding the equivalence is only for 2F1 type.
|
|
# We can extend it for 1F1 and 0F1 type also.
|
|
x = func.args[0]
|
|
|
|
# making given equation in normal form
|
|
I1 = factor(cancel(A.diff(x)/2 + A**2/4 - B))
|
|
|
|
# computing shifted invariant(J1) of the equation
|
|
J1 = factor(cancel(x**2*I1 + S(1)/4))
|
|
num, dem = J1.as_numer_denom()
|
|
num = powdenest(expand(num))
|
|
dem = powdenest(expand(dem))
|
|
# this function will compute the different powers of variable(x) in J1.
|
|
# then it will help in finding value of k. k is power of x such that we can express
|
|
# J1 = x**k * J0(x**k) then all the powers in J0 become integers.
|
|
def _power_counting(num):
|
|
_pow = {0}
|
|
for val in num:
|
|
if val.has(x):
|
|
if isinstance(val, Pow) and val.as_base_exp()[0] == x:
|
|
_pow.add(val.as_base_exp()[1])
|
|
elif val == x:
|
|
_pow.add(val.as_base_exp()[1])
|
|
else:
|
|
_pow.update(_power_counting(val.args))
|
|
return _pow
|
|
|
|
pow_num = _power_counting((num, ))
|
|
pow_dem = _power_counting((dem, ))
|
|
pow_dem.update(pow_num)
|
|
|
|
_pow = pow_dem
|
|
k = gcd(_pow)
|
|
|
|
# computing I0 of the given equation
|
|
I0 = powdenest(simplify(factor(((J1/k**2) - S(1)/4)/((x**k)**2))), force=True)
|
|
I0 = factor(cancel(powdenest(I0.subs(x, x**(S(1)/k)), force=True)))
|
|
|
|
# Before this point I0, J1 might be functions of e.g. sqrt(x) but replacing
|
|
# x with x**(1/k) should result in I0 being a rational function of x or
|
|
# otherwise the hypergeometric solver cannot be used. Note that k can be a
|
|
# non-integer rational such as 2/7.
|
|
if not I0.is_rational_function(x):
|
|
return None
|
|
|
|
num, dem = I0.as_numer_denom()
|
|
|
|
max_num_pow = max(_power_counting((num, )))
|
|
dem_args = dem.args
|
|
sing_point = []
|
|
dem_pow = []
|
|
# calculating singular point of I0.
|
|
for arg in dem_args:
|
|
if arg.has(x):
|
|
if isinstance(arg, Pow):
|
|
# (x-a)**n
|
|
dem_pow.append(arg.as_base_exp()[1])
|
|
sing_point.append(list(roots(arg.as_base_exp()[0], x).keys())[0])
|
|
else:
|
|
# (x-a) type
|
|
dem_pow.append(arg.as_base_exp()[1])
|
|
sing_point.append(list(roots(arg, x).keys())[0])
|
|
|
|
dem_pow.sort()
|
|
# checking if equivalence is exists or not.
|
|
|
|
if equivalence(max_num_pow, dem_pow) == "2F1":
|
|
return {'I0':I0, 'k':k, 'sing_point':sing_point, 'type':"2F1"}
|
|
else:
|
|
return None
|
|
|
|
|
|
def match_2nd_2F1_hypergeometric(I, k, sing_point, func):
|
|
x = func.args[0]
|
|
a = Wild("a")
|
|
b = Wild("b")
|
|
c = Wild("c")
|
|
t = Wild("t")
|
|
s = Wild("s")
|
|
r = Wild("r")
|
|
alpha = Wild("alpha")
|
|
beta = Wild("beta")
|
|
gamma = Wild("gamma")
|
|
delta = Wild("delta")
|
|
# I0 of the standerd 2F1 equation.
|
|
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)
|
|
if sing_point != [0, 1]:
|
|
# If singular point is [0, 1] then we have standerd equation.
|
|
eqs = []
|
|
sing_eqs = [-beta/alpha, -delta/gamma, (delta-beta)/(alpha-gamma)]
|
|
# making equations for the finding the mobius transformation
|
|
for i in range(3):
|
|
if i<len(sing_point):
|
|
eqs.append(Eq(sing_eqs[i], sing_point[i]))
|
|
else:
|
|
eqs.append(Eq(1/sing_eqs[i], 0))
|
|
# solving above equations for the mobius transformation
|
|
_beta = -alpha*sing_point[0]
|
|
_delta = -gamma*sing_point[1]
|
|
_gamma = alpha
|
|
if len(sing_point) == 3:
|
|
_gamma = (_beta + sing_point[2]*alpha)/(sing_point[2] - sing_point[1])
|
|
mob = (alpha*x + beta)/(gamma*x + delta)
|
|
mob = mob.subs(beta, _beta)
|
|
mob = mob.subs(delta, _delta)
|
|
mob = mob.subs(gamma, _gamma)
|
|
mob = cancel(mob)
|
|
t = (beta - delta*x)/(gamma*x - alpha)
|
|
t = cancel(((t.subs(beta, _beta)).subs(delta, _delta)).subs(gamma, _gamma))
|
|
else:
|
|
mob = x
|
|
t = x
|
|
|
|
# applying mobius transformation in I to make it into I0.
|
|
I = I.subs(x, t)
|
|
I = I*(t.diff(x))**2
|
|
I = factor(I)
|
|
dict_I = {x**2:0, x:0, 1:0}
|
|
I0_num, I0_dem = I0.as_numer_denom()
|
|
# collecting coeff of (x**2, x), of the standerd equation.
|
|
# substituting (a-b) = s, (a+b) = r
|
|
dict_I0 = {x**2:s**2 - 1, x:(2*(1-r)*c + (r+s)*(r-s)), 1:c*(c-2)}
|
|
# collecting coeff of (x**2, x) from I0 of the given equation.
|
|
dict_I.update(collect(expand(cancel(I*I0_dem)), [x**2, x], evaluate=False))
|
|
eqs = []
|
|
# We are comparing the coeff of powers of different x, for finding the values of
|
|
# parameters of standerd equation.
|
|
for key in [x**2, x, 1]:
|
|
eqs.append(Eq(dict_I[key], dict_I0[key]))
|
|
|
|
# We can have many possible roots for the equation.
|
|
# I am selecting the root on the basis that when we have
|
|
# standard equation eq = x*(x-1)*f(x).diff(x, 2) + ((a+b+1)*x-c)*f(x).diff(x) + a*b*f(x)
|
|
# then root should be a, b, c.
|
|
|
|
_c = 1 - factor(sqrt(1+eqs[2].lhs))
|
|
if not _c.has(Symbol):
|
|
_c = min(list(roots(eqs[2], c)))
|
|
_s = factor(sqrt(eqs[0].lhs + 1))
|
|
_r = _c - factor(sqrt(_c**2 + _s**2 + eqs[1].lhs - 2*_c))
|
|
_a = (_r + _s)/2
|
|
_b = (_r - _s)/2
|
|
|
|
rn = {'a':simplify(_a), 'b':simplify(_b), 'c':simplify(_c), 'k':k, 'mobius':mob, 'type':"2F1"}
|
|
return rn
|
|
|
|
|
|
def equivalence(max_num_pow, dem_pow):
|
|
# this function is made for checking the equivalence with 2F1 type of equation.
|
|
# max_num_pow is the value of maximum power of x in numerator
|
|
# and dem_pow is list of powers of different factor of form (a*x b).
|
|
# reference from table 1 in paper - "Non-Liouvillian solutions for second order
|
|
# linear ODEs" by L. Chan, E.S. Cheb-Terrab.
|
|
# We can extend it for 1F1 and 0F1 type also.
|
|
|
|
if max_num_pow == 2:
|
|
if dem_pow in [[2, 2], [2, 2, 2]]:
|
|
return "2F1"
|
|
elif max_num_pow == 1:
|
|
if dem_pow in [[1, 2, 2], [2, 2, 2], [1, 2], [2, 2]]:
|
|
return "2F1"
|
|
elif max_num_pow == 0:
|
|
if dem_pow in [[1, 1, 2], [2, 2], [1, 2, 2], [1, 1], [2], [1, 2], [2, 2]]:
|
|
return "2F1"
|
|
|
|
return None
|
|
|
|
|
|
def get_sol_2F1_hypergeometric(eq, func, match_object):
|
|
x = func.args[0]
|
|
from sympy.simplify.hyperexpand import hyperexpand
|
|
from sympy.polys.polytools import factor
|
|
C0, C1 = get_numbered_constants(eq, num=2)
|
|
a = match_object['a']
|
|
b = match_object['b']
|
|
c = match_object['c']
|
|
A = match_object['A']
|
|
|
|
sol = None
|
|
|
|
if c.is_integer == False:
|
|
sol = C0*hyper([a, b], [c], x) + C1*hyper([a-c+1, b-c+1], [2-c], x)*x**(1-c)
|
|
elif c == 1:
|
|
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)
|
|
sol = C0*hyper([a, b], [c], x) + C1*y2
|
|
elif (c-a-b).is_integer == False:
|
|
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)
|
|
|
|
if sol:
|
|
# applying transformation in the solution
|
|
subs = match_object['mobius']
|
|
dtdx = simplify(1/(subs.diff(x)))
|
|
_B = ((a + b + 1)*x - c).subs(x, subs)*dtdx
|
|
_B = factor(_B + ((x**2 -x).subs(x, subs))*(dtdx.diff(x)*dtdx))
|
|
_A = factor((x**2 - x).subs(x, subs)*(dtdx**2))
|
|
e = exp(logcombine(Integral(cancel(_B/(2*_A)), x), force=True))
|
|
sol = sol.subs(x, match_object['mobius'])
|
|
sol = sol.subs(x, x**match_object['k'])
|
|
e = e.subs(x, x**match_object['k'])
|
|
|
|
if not A.is_zero:
|
|
e1 = Integral(A/2, x)
|
|
e1 = exp(logcombine(e1, force=True))
|
|
sol = cancel((e/e1)*x**((-match_object['k']+1)/2))*sol
|
|
sol = Eq(func, sol)
|
|
return sol
|
|
|
|
sol = cancel((e)*x**((-match_object['k']+1)/2))*sol
|
|
sol = Eq(func, sol)
|
|
return sol
|