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