Spaces:
Sleeping
Sleeping
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 | |