Spaces:
Sleeping
Sleeping
r""" | |
This module contains the implementation of the internal helper functions for the lie_group hint for | |
dsolve. These helper functions apply different heuristics on the given equation | |
and return the solution. These functions are used by :py:meth:`sympy.solvers.ode.single.LieGroup` | |
References | |
========= | |
- `abaco1_simple`, `function_sum` and `chi` are referenced from E.S Cheb-Terrab, L.G.S Duarte | |
and L.A,C.P da Mota, Computer Algebra Solving of First Order ODEs Using | |
Symmetry Methods, pp. 7 - pp. 8 | |
- `abaco1_product`, `abaco2_similar`, `abaco2_unique_unknown`, `linear` and `abaco2_unique_general` | |
are referenced from E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 7 - pp. 12 | |
- `bivariate` from Lie Groups and Differential Equations pp. 327 - pp. 329 | |
""" | |
from itertools import islice | |
from sympy.core import Add, S, Mul, Pow | |
from sympy.core.exprtools import factor_terms | |
from sympy.core.function import Function, AppliedUndef, expand | |
from sympy.core.relational import Equality, Eq | |
from sympy.core.symbol import Symbol, Wild, Dummy, symbols | |
from sympy.functions import exp, log | |
from sympy.integrals.integrals import integrate | |
from sympy.polys import Poly | |
from sympy.polys.polytools import cancel, div | |
from sympy.simplify import (collect, powsimp, # type: ignore | |
separatevars, simplify) | |
from sympy.solvers import solve | |
from sympy.solvers.pde import pdsolve | |
from sympy.utilities import numbered_symbols | |
from sympy.solvers.deutils import _preprocess, ode_order | |
from .ode import checkinfsol | |
lie_heuristics = ( | |
"abaco1_simple", | |
"abaco1_product", | |
"abaco2_similar", | |
"abaco2_unique_unknown", | |
"abaco2_unique_general", | |
"linear", | |
"function_sum", | |
"bivariate", | |
"chi" | |
) | |
def _ode_lie_group_try_heuristic(eq, heuristic, func, match, inf): | |
xi = Function("xi") | |
eta = Function("eta") | |
f = func.func | |
x = func.args[0] | |
y = match['y'] | |
h = match['h'] | |
tempsol = [] | |
if not inf: | |
try: | |
inf = infinitesimals(eq, hint=heuristic, func=func, order=1, match=match) | |
except ValueError: | |
return None | |
for infsim in inf: | |
xiinf = (infsim[xi(x, func)]).subs(func, y) | |
etainf = (infsim[eta(x, func)]).subs(func, y) | |
# This condition creates recursion while using pdsolve. | |
# Since the first step while solving a PDE of form | |
# a*(f(x, y).diff(x)) + b*(f(x, y).diff(y)) + c = 0 | |
# is to solve the ODE dy/dx = b/a | |
if simplify(etainf/xiinf) == h: | |
continue | |
rpde = f(x, y).diff(x)*xiinf + f(x, y).diff(y)*etainf | |
r = pdsolve(rpde, func=f(x, y)).rhs | |
s = pdsolve(rpde - 1, func=f(x, y)).rhs | |
newcoord = [_lie_group_remove(coord) for coord in [r, s]] | |
r = Dummy("r") | |
s = Dummy("s") | |
C1 = Symbol("C1") | |
rcoord = newcoord[0] | |
scoord = newcoord[-1] | |
try: | |
sol = solve([r - rcoord, s - scoord], x, y, dict=True) | |
if sol == []: | |
continue | |
except NotImplementedError: | |
continue | |
else: | |
sol = sol[0] | |
xsub = sol[x] | |
ysub = sol[y] | |
num = simplify(scoord.diff(x) + scoord.diff(y)*h) | |
denom = simplify(rcoord.diff(x) + rcoord.diff(y)*h) | |
if num and denom: | |
diffeq = simplify((num/denom).subs([(x, xsub), (y, ysub)])) | |
sep = separatevars(diffeq, symbols=[r, s], dict=True) | |
if sep: | |
# Trying to separate, r and s coordinates | |
deq = integrate((1/sep[s]), s) + C1 - integrate(sep['coeff']*sep[r], r) | |
# Substituting and reverting back to original coordinates | |
deq = deq.subs([(r, rcoord), (s, scoord)]) | |
try: | |
sdeq = solve(deq, y) | |
except NotImplementedError: | |
tempsol.append(deq) | |
else: | |
return [Eq(f(x), sol) for sol in sdeq] | |
elif denom: # (ds/dr) is zero which means s is constant | |
return [Eq(f(x), solve(scoord - C1, y)[0])] | |
elif num: # (dr/ds) is zero which means r is constant | |
return [Eq(f(x), solve(rcoord - C1, y)[0])] | |
# If nothing works, return solution as it is, without solving for y | |
if tempsol: | |
return [Eq(sol.subs(y, f(x)), 0) for sol in tempsol] | |
return None | |
def _ode_lie_group( s, func, order, match): | |
heuristics = lie_heuristics | |
inf = {} | |
f = func.func | |
x = func.args[0] | |
df = func.diff(x) | |
xi = Function("xi") | |
eta = Function("eta") | |
xis = match['xi'] | |
etas = match['eta'] | |
y = match.pop('y', None) | |
if y: | |
h = -simplify(match[match['d']]/match[match['e']]) | |
y = y | |
else: | |
y = Dummy("y") | |
h = s.subs(func, y) | |
if xis is not None and etas is not None: | |
inf = [{xi(x, f(x)): S(xis), eta(x, f(x)): S(etas)}] | |
if checkinfsol(Eq(df, s), inf, func=f(x), order=1)[0][0]: | |
heuristics = ["user_defined"] + list(heuristics) | |
match = {'h': h, 'y': y} | |
# This is done so that if any heuristic raises a ValueError | |
# another heuristic can be used. | |
sol = None | |
for heuristic in heuristics: | |
sol = _ode_lie_group_try_heuristic(Eq(df, s), heuristic, func, match, inf) | |
if sol: | |
return sol | |
return sol | |
def infinitesimals(eq, func=None, order=None, hint='default', match=None): | |
r""" | |
The infinitesimal functions of an ordinary differential equation, `\xi(x,y)` | |
and `\eta(x,y)`, are the infinitesimals of the Lie group of point transformations | |
for which the differential equation is invariant. So, the ODE `y'=f(x,y)` | |
would admit a Lie group `x^*=X(x,y;\varepsilon)=x+\varepsilon\xi(x,y)`, | |
`y^*=Y(x,y;\varepsilon)=y+\varepsilon\eta(x,y)` such that `(y^*)'=f(x^*, y^*)`. | |
A change of coordinates, to `r(x,y)` and `s(x,y)`, can be performed so this Lie group | |
becomes the translation group, `r^*=r` and `s^*=s+\varepsilon`. | |
They are tangents to the coordinate curves of the new system. | |
Consider the transformation `(x, y) \to (X, Y)` such that the | |
differential equation remains invariant. `\xi` and `\eta` are the tangents to | |
the transformed coordinates `X` and `Y`, at `\varepsilon=0`. | |
.. math:: \left(\frac{\partial X(x,y;\varepsilon)}{\partial\varepsilon | |
}\right)|_{\varepsilon=0} = \xi, | |
\left(\frac{\partial Y(x,y;\varepsilon)}{\partial\varepsilon | |
}\right)|_{\varepsilon=0} = \eta, | |
The infinitesimals can be found by solving the following PDE: | |
>>> from sympy import Function, Eq, pprint | |
>>> from sympy.abc import x, y | |
>>> xi, eta, h = map(Function, ['xi', 'eta', 'h']) | |
>>> h = h(x, y) # dy/dx = h | |
>>> eta = eta(x, y) | |
>>> xi = xi(x, y) | |
>>> genform = Eq(eta.diff(x) + (eta.diff(y) - xi.diff(x))*h | |
... - (xi.diff(y))*h**2 - xi*(h.diff(x)) - eta*(h.diff(y)), 0) | |
>>> pprint(genform) | |
/d d \ d 2 d d d | |
|--(eta(x, y)) - --(xi(x, y))|*h(x, y) - eta(x, y)*--(h(x, y)) - h (x, y)*--(xi(x, y)) - xi(x, y)*--(h(x, y)) + --(eta(x, y)) = 0 | |
\dy dx / dy dy dx dx | |
Solving the above mentioned PDE is not trivial, and can be solved only by | |
making intelligent assumptions for `\xi` and `\eta` (heuristics). Once an | |
infinitesimal is found, the attempt to find more heuristics stops. This is done to | |
optimise the speed of solving the differential equation. If a list of all the | |
infinitesimals is needed, ``hint`` should be flagged as ``all``, which gives | |
the complete list of infinitesimals. If the infinitesimals for a particular | |
heuristic needs to be found, it can be passed as a flag to ``hint``. | |
Examples | |
======== | |
>>> from sympy import Function | |
>>> from sympy.solvers.ode.lie_group import infinitesimals | |
>>> from sympy.abc import x | |
>>> f = Function('f') | |
>>> eq = f(x).diff(x) - x**2*f(x) | |
>>> infinitesimals(eq) | |
[{eta(x, f(x)): exp(x**3/3), xi(x, f(x)): 0}] | |
References | |
========== | |
- Solving differential equations by Symmetry Groups, | |
John Starrett, pp. 1 - pp. 14 | |
""" | |
if isinstance(eq, Equality): | |
eq = eq.lhs - eq.rhs | |
if not func: | |
eq, func = _preprocess(eq) | |
variables = func.args | |
if len(variables) != 1: | |
raise ValueError("ODE's have only one independent variable") | |
else: | |
x = variables[0] | |
if not order: | |
order = ode_order(eq, func) | |
if order != 1: | |
raise NotImplementedError("Infinitesimals for only " | |
"first order ODE's have been implemented") | |
else: | |
df = func.diff(x) | |
# Matching differential equation of the form a*df + b | |
a = Wild('a', exclude = [df]) | |
b = Wild('b', exclude = [df]) | |
if match: # Used by lie_group hint | |
h = match['h'] | |
y = match['y'] | |
else: | |
match = collect(expand(eq), df).match(a*df + b) | |
if match: | |
h = -simplify(match[b]/match[a]) | |
else: | |
try: | |
sol = solve(eq, df) | |
except NotImplementedError: | |
raise NotImplementedError("Infinitesimals for the " | |
"first order ODE could not be found") | |
else: | |
h = sol[0] # Find infinitesimals for one solution | |
y = Dummy("y") | |
h = h.subs(func, y) | |
u = Dummy("u") | |
hx = h.diff(x) | |
hy = h.diff(y) | |
hinv = ((1/h).subs([(x, u), (y, x)])).subs(u, y) # Inverse ODE | |
match = {'h': h, 'func': func, 'hx': hx, 'hy': hy, 'y': y, 'hinv': hinv} | |
if hint == 'all': | |
xieta = [] | |
for heuristic in lie_heuristics: | |
function = globals()['lie_heuristic_' + heuristic] | |
inflist = function(match, comp=True) | |
if inflist: | |
xieta.extend([inf for inf in inflist if inf not in xieta]) | |
if xieta: | |
return xieta | |
else: | |
raise NotImplementedError("Infinitesimals could not be found for " | |
"the given ODE") | |
elif hint == 'default': | |
for heuristic in lie_heuristics: | |
function = globals()['lie_heuristic_' + heuristic] | |
xieta = function(match, comp=False) | |
if xieta: | |
return xieta | |
raise NotImplementedError("Infinitesimals could not be found for" | |
" the given ODE") | |
elif hint not in lie_heuristics: | |
raise ValueError("Heuristic not recognized: " + hint) | |
else: | |
function = globals()['lie_heuristic_' + hint] | |
xieta = function(match, comp=True) | |
if xieta: | |
return xieta | |
else: | |
raise ValueError("Infinitesimals could not be found using the" | |
" given heuristic") | |
def lie_heuristic_abaco1_simple(match, comp=False): | |
r""" | |
The first heuristic uses the following four sets of | |
assumptions on `\xi` and `\eta` | |
.. math:: \xi = 0, \eta = f(x) | |
.. math:: \xi = 0, \eta = f(y) | |
.. math:: \xi = f(x), \eta = 0 | |
.. math:: \xi = f(y), \eta = 0 | |
The success of this heuristic is determined by algebraic factorisation. | |
For the first assumption `\xi = 0` and `\eta` to be a function of `x`, the PDE | |
.. math:: \frac{\partial \eta}{\partial x} + (\frac{\partial \eta}{\partial y} | |
- \frac{\partial \xi}{\partial x})*h | |
- \frac{\partial \xi}{\partial y}*h^{2} | |
- \xi*\frac{\partial h}{\partial x} - \eta*\frac{\partial h}{\partial y} = 0 | |
reduces to `f'(x) - f\frac{\partial h}{\partial y} = 0` | |
If `\frac{\partial h}{\partial y}` is a function of `x`, then this can usually | |
be integrated easily. A similar idea is applied to the other 3 assumptions as well. | |
References | |
========== | |
- E.S Cheb-Terrab, L.G.S Duarte and L.A,C.P da Mota, Computer Algebra | |
Solving of First Order ODEs Using Symmetry Methods, pp. 8 | |
""" | |
xieta = [] | |
y = match['y'] | |
h = match['h'] | |
func = match['func'] | |
x = func.args[0] | |
hx = match['hx'] | |
hy = match['hy'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
hysym = hy.free_symbols | |
if y not in hysym: | |
try: | |
fx = exp(integrate(hy, x)) | |
except NotImplementedError: | |
pass | |
else: | |
inf = {xi: S.Zero, eta: fx} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
factor = hy/h | |
facsym = factor.free_symbols | |
if x not in facsym: | |
try: | |
fy = exp(integrate(factor, y)) | |
except NotImplementedError: | |
pass | |
else: | |
inf = {xi: S.Zero, eta: fy.subs(y, func)} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
factor = -hx/h | |
facsym = factor.free_symbols | |
if y not in facsym: | |
try: | |
fx = exp(integrate(factor, x)) | |
except NotImplementedError: | |
pass | |
else: | |
inf = {xi: fx, eta: S.Zero} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
factor = -hx/(h**2) | |
facsym = factor.free_symbols | |
if x not in facsym: | |
try: | |
fy = exp(integrate(factor, y)) | |
except NotImplementedError: | |
pass | |
else: | |
inf = {xi: fy.subs(y, func), eta: S.Zero} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
if xieta: | |
return xieta | |
def lie_heuristic_abaco1_product(match, comp=False): | |
r""" | |
The second heuristic uses the following two assumptions on `\xi` and `\eta` | |
.. math:: \eta = 0, \xi = f(x)*g(y) | |
.. math:: \eta = f(x)*g(y), \xi = 0 | |
The first assumption of this heuristic holds good if | |
`\frac{1}{h^{2}}\frac{\partial^2}{\partial x \partial y}\log(h)` is | |
separable in `x` and `y`, then the separated factors containing `x` | |
is `f(x)`, and `g(y)` is obtained by | |
.. math:: e^{\int f\frac{\partial}{\partial x}\left(\frac{1}{f*h}\right)\,dy} | |
provided `f\frac{\partial}{\partial x}\left(\frac{1}{f*h}\right)` is a function | |
of `y` only. | |
The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as | |
`\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first assumption | |
satisfies. After obtaining `f(x)` and `g(y)`, the coordinates are again | |
interchanged, to get `\eta` as `f(x)*g(y)` | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 7 - pp. 8 | |
""" | |
xieta = [] | |
y = match['y'] | |
h = match['h'] | |
hinv = match['hinv'] | |
func = match['func'] | |
x = func.args[0] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
inf = separatevars(((log(h).diff(y)).diff(x))/h**2, dict=True, symbols=[x, y]) | |
if inf and inf['coeff']: | |
fx = inf[x] | |
gy = simplify(fx*((1/(fx*h)).diff(x))) | |
gysyms = gy.free_symbols | |
if x not in gysyms: | |
gy = exp(integrate(gy, y)) | |
inf = {eta: S.Zero, xi: (fx*gy).subs(y, func)} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
u1 = Dummy("u1") | |
inf = separatevars(((log(hinv).diff(y)).diff(x))/hinv**2, dict=True, symbols=[x, y]) | |
if inf and inf['coeff']: | |
fx = inf[x] | |
gy = simplify(fx*((1/(fx*hinv)).diff(x))) | |
gysyms = gy.free_symbols | |
if x not in gysyms: | |
gy = exp(integrate(gy, y)) | |
etaval = fx*gy | |
etaval = (etaval.subs([(x, u1), (y, x)])).subs(u1, y) | |
inf = {eta: etaval.subs(y, func), xi: S.Zero} | |
if not comp: | |
return [inf] | |
if comp and inf not in xieta: | |
xieta.append(inf) | |
if xieta: | |
return xieta | |
def lie_heuristic_bivariate(match, comp=False): | |
r""" | |
The third heuristic assumes the infinitesimals `\xi` and `\eta` | |
to be bi-variate polynomials in `x` and `y`. The assumption made here | |
for the logic below is that `h` is a rational function in `x` and `y` | |
though that may not be necessary for the infinitesimals to be | |
bivariate polynomials. The coefficients of the infinitesimals | |
are found out by substituting them in the PDE and grouping similar terms | |
that are polynomials and since they form a linear system, solve and check | |
for non trivial solutions. The degree of the assumed bivariates | |
are increased till a certain maximum value. | |
References | |
========== | |
- Lie Groups and Differential Equations | |
pp. 327 - pp. 329 | |
""" | |
h = match['h'] | |
hx = match['hx'] | |
hy = match['hy'] | |
func = match['func'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
if h.is_rational_function(): | |
# The maximum degree that the infinitesimals can take is | |
# calculated by this technique. | |
etax, etay, etad, xix, xiy, xid = symbols("etax etay etad xix xiy xid") | |
ipde = etax + (etay - xix)*h - xiy*h**2 - xid*hx - etad*hy | |
num, denom = cancel(ipde).as_numer_denom() | |
deg = Poly(num, x, y).total_degree() | |
deta = Function('deta')(x, y) | |
dxi = Function('dxi')(x, y) | |
ipde = (deta.diff(x) + (deta.diff(y) - dxi.diff(x))*h - (dxi.diff(y))*h**2 | |
- dxi*hx - deta*hy) | |
xieq = Symbol("xi0") | |
etaeq = Symbol("eta0") | |
for i in range(deg + 1): | |
if i: | |
xieq += Add(*[ | |
Symbol("xi_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) | |
for power in range(i + 1)]) | |
etaeq += Add(*[ | |
Symbol("eta_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) | |
for power in range(i + 1)]) | |
pden, denom = (ipde.subs({dxi: xieq, deta: etaeq}).doit()).as_numer_denom() | |
pden = expand(pden) | |
# If the individual terms are monomials, the coefficients | |
# are grouped | |
if pden.is_polynomial(x, y) and pden.is_Add: | |
polyy = Poly(pden, x, y).as_dict() | |
if polyy: | |
symset = xieq.free_symbols.union(etaeq.free_symbols) - {x, y} | |
soldict = solve(polyy.values(), *symset) | |
if isinstance(soldict, list): | |
soldict = soldict[0] | |
if any(soldict.values()): | |
xired = xieq.subs(soldict) | |
etared = etaeq.subs(soldict) | |
# Scaling is done by substituting one for the parameters | |
# This can be any number except zero. | |
dict_ = dict.fromkeys(symset, 1) | |
inf = {eta: etared.subs(dict_).subs(y, func), | |
xi: xired.subs(dict_).subs(y, func)} | |
return [inf] | |
def lie_heuristic_chi(match, comp=False): | |
r""" | |
The aim of the fourth heuristic is to find the function `\chi(x, y)` | |
that satisfies the PDE `\frac{d\chi}{dx} + h\frac{d\chi}{dx} | |
- \frac{\partial h}{\partial y}\chi = 0`. | |
This assumes `\chi` to be a bivariate polynomial in `x` and `y`. By intuition, | |
`h` should be a rational function in `x` and `y`. The method used here is | |
to substitute a general binomial for `\chi` up to a certain maximum degree | |
is reached. The coefficients of the polynomials, are calculated by by collecting | |
terms of the same order in `x` and `y`. | |
After finding `\chi`, the next step is to use `\eta = \xi*h + \chi`, to | |
determine `\xi` and `\eta`. This can be done by dividing `\chi` by `h` | |
which would give `-\xi` as the quotient and `\eta` as the remainder. | |
References | |
========== | |
- E.S Cheb-Terrab, L.G.S Duarte and L.A,C.P da Mota, Computer Algebra | |
Solving of First Order ODEs Using Symmetry Methods, pp. 8 | |
""" | |
h = match['h'] | |
hy = match['hy'] | |
func = match['func'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
if h.is_rational_function(): | |
schi, schix, schiy = symbols("schi, schix, schiy") | |
cpde = schix + h*schiy - hy*schi | |
num, denom = cancel(cpde).as_numer_denom() | |
deg = Poly(num, x, y).total_degree() | |
chi = Function('chi')(x, y) | |
chix = chi.diff(x) | |
chiy = chi.diff(y) | |
cpde = chix + h*chiy - hy*chi | |
chieq = Symbol("chi") | |
for i in range(1, deg + 1): | |
chieq += Add(*[ | |
Symbol("chi_" + str(power) + "_" + str(i - power))*x**power*y**(i - power) | |
for power in range(i + 1)]) | |
cnum, cden = cancel(cpde.subs({chi : chieq}).doit()).as_numer_denom() | |
cnum = expand(cnum) | |
if cnum.is_polynomial(x, y) and cnum.is_Add: | |
cpoly = Poly(cnum, x, y).as_dict() | |
if cpoly: | |
solsyms = chieq.free_symbols - {x, y} | |
soldict = solve(cpoly.values(), *solsyms) | |
if isinstance(soldict, list): | |
soldict = soldict[0] | |
if any(soldict.values()): | |
chieq = chieq.subs(soldict) | |
dict_ = dict.fromkeys(solsyms, 1) | |
chieq = chieq.subs(dict_) | |
# After finding chi, the main aim is to find out | |
# eta, xi by the equation eta = xi*h + chi | |
# One method to set xi, would be rearranging it to | |
# (eta/h) - xi = (chi/h). This would mean dividing | |
# chi by h would give -xi as the quotient and eta | |
# as the remainder. Thanks to Sean Vig for suggesting | |
# this method. | |
xic, etac = div(chieq, h) | |
inf = {eta: etac.subs(y, func), xi: -xic.subs(y, func)} | |
return [inf] | |
def lie_heuristic_function_sum(match, comp=False): | |
r""" | |
This heuristic uses the following two assumptions on `\xi` and `\eta` | |
.. math:: \eta = 0, \xi = f(x) + g(y) | |
.. math:: \eta = f(x) + g(y), \xi = 0 | |
The first assumption of this heuristic holds good if | |
.. math:: \frac{\partial}{\partial y}[(h\frac{\partial^{2}}{ | |
\partial x^{2}}(h^{-1}))^{-1}] | |
is separable in `x` and `y`, | |
1. The separated factors containing `y` is `\frac{\partial g}{\partial y}`. | |
From this `g(y)` can be determined. | |
2. The separated factors containing `x` is `f''(x)`. | |
3. `h\frac{\partial^{2}}{\partial x^{2}}(h^{-1})` equals | |
`\frac{f''(x)}{f(x) + g(y)}`. From this `f(x)` can be determined. | |
The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as | |
`\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first | |
assumption satisfies. After obtaining `f(x)` and `g(y)`, the coordinates | |
are again interchanged, to get `\eta` as `f(x) + g(y)`. | |
For both assumptions, the constant factors are separated among `g(y)` | |
and `f''(x)`, such that `f''(x)` obtained from 3] is the same as that | |
obtained from 2]. If not possible, then this heuristic fails. | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 7 - pp. 8 | |
""" | |
xieta = [] | |
h = match['h'] | |
func = match['func'] | |
hinv = match['hinv'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
for odefac in [h, hinv]: | |
factor = odefac*((1/odefac).diff(x, 2)) | |
sep = separatevars((1/factor).diff(y), dict=True, symbols=[x, y]) | |
if sep and sep['coeff'] and sep[x].has(x) and sep[y].has(y): | |
k = Dummy("k") | |
try: | |
gy = k*integrate(sep[y], y) | |
except NotImplementedError: | |
pass | |
else: | |
fdd = 1/(k*sep[x]*sep['coeff']) | |
fx = simplify(fdd/factor - gy) | |
check = simplify(fx.diff(x, 2) - fdd) | |
if fx: | |
if not check: | |
fx = fx.subs(k, 1) | |
gy = (gy/k) | |
else: | |
sol = solve(check, k) | |
if sol: | |
sol = sol[0] | |
fx = fx.subs(k, sol) | |
gy = (gy/k)*sol | |
else: | |
continue | |
if odefac == hinv: # Inverse ODE | |
fx = fx.subs(x, y) | |
gy = gy.subs(y, x) | |
etaval = factor_terms(fx + gy) | |
if etaval.is_Mul: | |
etaval = Mul(*[arg for arg in etaval.args if arg.has(x, y)]) | |
if odefac == hinv: # Inverse ODE | |
inf = {eta: etaval.subs(y, func), xi : S.Zero} | |
else: | |
inf = {xi: etaval.subs(y, func), eta : S.Zero} | |
if not comp: | |
return [inf] | |
else: | |
xieta.append(inf) | |
if xieta: | |
return xieta | |
def lie_heuristic_abaco2_similar(match, comp=False): | |
r""" | |
This heuristic uses the following two assumptions on `\xi` and `\eta` | |
.. math:: \eta = g(x), \xi = f(x) | |
.. math:: \eta = f(y), \xi = g(y) | |
For the first assumption, | |
1. First `\frac{\frac{\partial h}{\partial y}}{\frac{\partial^{2} h}{ | |
\partial yy}}` is calculated. Let us say this value is A | |
2. If this is constant, then `h` is matched to the form `A(x) + B(x)e^{ | |
\frac{y}{C}}` then, `\frac{e^{\int \frac{A(x)}{C} \,dx}}{B(x)}` gives `f(x)` | |
and `A(x)*f(x)` gives `g(x)` | |
3. Otherwise `\frac{\frac{\partial A}{\partial X}}{\frac{\partial A}{ | |
\partial Y}} = \gamma` is calculated. If | |
a] `\gamma` is a function of `x` alone | |
b] `\frac{\gamma\frac{\partial h}{\partial y} - \gamma'(x) - \frac{ | |
\partial h}{\partial x}}{h + \gamma} = G` is a function of `x` alone. | |
then, `e^{\int G \,dx}` gives `f(x)` and `-\gamma*f(x)` gives `g(x)` | |
The second assumption holds good if `\frac{dy}{dx} = h(x, y)` is rewritten as | |
`\frac{dy}{dx} = \frac{1}{h(y, x)}` and the same properties of the first assumption | |
satisfies. After obtaining `f(x)` and `g(x)`, the coordinates are again | |
interchanged, to get `\xi` as `f(x^*)` and `\eta` as `g(y^*)` | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 10 - pp. 12 | |
""" | |
h = match['h'] | |
hx = match['hx'] | |
hy = match['hy'] | |
func = match['func'] | |
hinv = match['hinv'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
factor = cancel(h.diff(y)/h.diff(y, 2)) | |
factorx = factor.diff(x) | |
factory = factor.diff(y) | |
if not factor.has(x) and not factor.has(y): | |
A = Wild('A', exclude=[y]) | |
B = Wild('B', exclude=[y]) | |
C = Wild('C', exclude=[x, y]) | |
match = h.match(A + B*exp(y/C)) | |
try: | |
tau = exp(-integrate(match[A]/match[C]), x)/match[B] | |
except NotImplementedError: | |
pass | |
else: | |
gx = match[A]*tau | |
return [{xi: tau, eta: gx}] | |
else: | |
gamma = cancel(factorx/factory) | |
if not gamma.has(y): | |
tauint = cancel((gamma*hy - gamma.diff(x) - hx)/(h + gamma)) | |
if not tauint.has(y): | |
try: | |
tau = exp(integrate(tauint, x)) | |
except NotImplementedError: | |
pass | |
else: | |
gx = -tau*gamma | |
return [{xi: tau, eta: gx}] | |
factor = cancel(hinv.diff(y)/hinv.diff(y, 2)) | |
factorx = factor.diff(x) | |
factory = factor.diff(y) | |
if not factor.has(x) and not factor.has(y): | |
A = Wild('A', exclude=[y]) | |
B = Wild('B', exclude=[y]) | |
C = Wild('C', exclude=[x, y]) | |
match = h.match(A + B*exp(y/C)) | |
try: | |
tau = exp(-integrate(match[A]/match[C]), x)/match[B] | |
except NotImplementedError: | |
pass | |
else: | |
gx = match[A]*tau | |
return [{eta: tau.subs(x, func), xi: gx.subs(x, func)}] | |
else: | |
gamma = cancel(factorx/factory) | |
if not gamma.has(y): | |
tauint = cancel((gamma*hinv.diff(y) - gamma.diff(x) - hinv.diff(x))/( | |
hinv + gamma)) | |
if not tauint.has(y): | |
try: | |
tau = exp(integrate(tauint, x)) | |
except NotImplementedError: | |
pass | |
else: | |
gx = -tau*gamma | |
return [{eta: tau.subs(x, func), xi: gx.subs(x, func)}] | |
def lie_heuristic_abaco2_unique_unknown(match, comp=False): | |
r""" | |
This heuristic assumes the presence of unknown functions or known functions | |
with non-integer powers. | |
1. A list of all functions and non-integer powers containing x and y | |
2. Loop over each element `f` in the list, find `\frac{\frac{\partial f}{\partial x}}{ | |
\frac{\partial f}{\partial x}} = R` | |
If it is separable in `x` and `y`, let `X` be the factors containing `x`. Then | |
a] Check if `\xi = X` and `\eta = -\frac{X}{R}` satisfy the PDE. If yes, then return | |
`\xi` and `\eta` | |
b] Check if `\xi = \frac{-R}{X}` and `\eta = -\frac{1}{X}` satisfy the PDE. | |
If yes, then return `\xi` and `\eta` | |
If not, then check if | |
a] :math:`\xi = -R,\eta = 1` | |
b] :math:`\xi = 1, \eta = -\frac{1}{R}` | |
are solutions. | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 10 - pp. 12 | |
""" | |
h = match['h'] | |
hx = match['hx'] | |
hy = match['hy'] | |
func = match['func'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
funclist = [] | |
for atom in h.atoms(Pow): | |
base, exp = atom.as_base_exp() | |
if base.has(x) and base.has(y): | |
if not exp.is_Integer: | |
funclist.append(atom) | |
for function in h.atoms(AppliedUndef): | |
syms = function.free_symbols | |
if x in syms and y in syms: | |
funclist.append(function) | |
for f in funclist: | |
frac = cancel(f.diff(y)/f.diff(x)) | |
sep = separatevars(frac, dict=True, symbols=[x, y]) | |
if sep and sep['coeff']: | |
xitry1 = sep[x] | |
etatry1 = -1/(sep[y]*sep['coeff']) | |
pde1 = etatry1.diff(y)*h - xitry1.diff(x)*h - xitry1*hx - etatry1*hy | |
if not simplify(pde1): | |
return [{xi: xitry1, eta: etatry1.subs(y, func)}] | |
xitry2 = 1/etatry1 | |
etatry2 = 1/xitry1 | |
pde2 = etatry2.diff(x) - (xitry2.diff(y))*h**2 - xitry2*hx - etatry2*hy | |
if not simplify(expand(pde2)): | |
return [{xi: xitry2.subs(y, func), eta: etatry2}] | |
else: | |
etatry = -1/frac | |
pde = etatry.diff(x) + etatry.diff(y)*h - hx - etatry*hy | |
if not simplify(pde): | |
return [{xi: S.One, eta: etatry.subs(y, func)}] | |
xitry = -frac | |
pde = -xitry.diff(x)*h -xitry.diff(y)*h**2 - xitry*hx -hy | |
if not simplify(expand(pde)): | |
return [{xi: xitry.subs(y, func), eta: S.One}] | |
def lie_heuristic_abaco2_unique_general(match, comp=False): | |
r""" | |
This heuristic finds if infinitesimals of the form `\eta = f(x)`, `\xi = g(y)` | |
without making any assumptions on `h`. | |
The complete sequence of steps is given in the paper mentioned below. | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 10 - pp. 12 | |
""" | |
hx = match['hx'] | |
hy = match['hy'] | |
func = match['func'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
A = hx.diff(y) | |
B = hy.diff(y) + hy**2 | |
C = hx.diff(x) - hx**2 | |
if not (A and B and C): | |
return | |
Ax = A.diff(x) | |
Ay = A.diff(y) | |
Axy = Ax.diff(y) | |
Axx = Ax.diff(x) | |
Ayy = Ay.diff(y) | |
D = simplify(2*Axy + hx*Ay - Ax*hy + (hx*hy + 2*A)*A)*A - 3*Ax*Ay | |
if not D: | |
E1 = simplify(3*Ax**2 + ((hx**2 + 2*C)*A - 2*Axx)*A) | |
if E1: | |
E2 = simplify((2*Ayy + (2*B - hy**2)*A)*A - 3*Ay**2) | |
if not E2: | |
E3 = simplify( | |
E1*((28*Ax + 4*hx*A)*A**3 - E1*(hy*A + Ay)) - E1.diff(x)*8*A**4) | |
if not E3: | |
etaval = cancel((4*A**3*(Ax - hx*A) + E1*(hy*A - Ay))/(S(2)*A*E1)) | |
if x not in etaval: | |
try: | |
etaval = exp(integrate(etaval, y)) | |
except NotImplementedError: | |
pass | |
else: | |
xival = -4*A**3*etaval/E1 | |
if y not in xival: | |
return [{xi: xival, eta: etaval.subs(y, func)}] | |
else: | |
E1 = simplify((2*Ayy + (2*B - hy**2)*A)*A - 3*Ay**2) | |
if E1: | |
E2 = simplify( | |
4*A**3*D - D**2 + E1*((2*Axx - (hx**2 + 2*C)*A)*A - 3*Ax**2)) | |
if not E2: | |
E3 = simplify( | |
-(A*D)*E1.diff(y) + ((E1.diff(x) - hy*D)*A + 3*Ay*D + | |
(A*hx - 3*Ax)*E1)*E1) | |
if not E3: | |
etaval = cancel(((A*hx - Ax)*E1 - (Ay + A*hy)*D)/(S(2)*A*D)) | |
if x not in etaval: | |
try: | |
etaval = exp(integrate(etaval, y)) | |
except NotImplementedError: | |
pass | |
else: | |
xival = -E1*etaval/D | |
if y not in xival: | |
return [{xi: xival, eta: etaval.subs(y, func)}] | |
def lie_heuristic_linear(match, comp=False): | |
r""" | |
This heuristic assumes | |
1. `\xi = ax + by + c` and | |
2. `\eta = fx + gy + h` | |
After substituting the following assumptions in the determining PDE, it | |
reduces to | |
.. math:: f + (g - a)h - bh^{2} - (ax + by + c)\frac{\partial h}{\partial x} | |
- (fx + gy + c)\frac{\partial h}{\partial y} | |
Solving the reduced PDE obtained, using the method of characteristics, becomes | |
impractical. The method followed is grouping similar terms and solving the system | |
of linear equations obtained. The difference between the bivariate heuristic is that | |
`h` need not be a rational function in this case. | |
References | |
========== | |
- E.S. Cheb-Terrab, A.D. Roche, Symmetries and First Order | |
ODE Patterns, pp. 10 - pp. 12 | |
""" | |
h = match['h'] | |
hx = match['hx'] | |
hy = match['hy'] | |
func = match['func'] | |
x = func.args[0] | |
y = match['y'] | |
xi = Function('xi')(x, func) | |
eta = Function('eta')(x, func) | |
coeffdict = {} | |
symbols = numbered_symbols("c", cls=Dummy) | |
symlist = [next(symbols) for _ in islice(symbols, 6)] | |
C0, C1, C2, C3, C4, C5 = symlist | |
pde = C3 + (C4 - C0)*h - (C0*x + C1*y + C2)*hx - (C3*x + C4*y + C5)*hy - C1*h**2 | |
pde, denom = pde.as_numer_denom() | |
pde = powsimp(expand(pde)) | |
if pde.is_Add: | |
terms = pde.args | |
for term in terms: | |
if term.is_Mul: | |
rem = Mul(*[m for m in term.args if not m.has(x, y)]) | |
xypart = term/rem | |
if xypart not in coeffdict: | |
coeffdict[xypart] = rem | |
else: | |
coeffdict[xypart] += rem | |
else: | |
if term not in coeffdict: | |
coeffdict[term] = S.One | |
else: | |
coeffdict[term] += S.One | |
sollist = coeffdict.values() | |
soldict = solve(sollist, symlist) | |
if soldict: | |
if isinstance(soldict, list): | |
soldict = soldict[0] | |
subval = soldict.values() | |
if any(t for t in subval): | |
onedict = dict(zip(symlist, [1]*6)) | |
xival = C0*x + C1*func + C2 | |
etaval = C3*x + C4*func + C5 | |
xival = xival.subs(soldict) | |
etaval = etaval.subs(soldict) | |
xival = xival.subs(onedict) | |
etaval = etaval.subs(onedict) | |
return [{xi: xival, eta: etaval}] | |
def _lie_group_remove(coords): | |
r""" | |
This function is strictly meant for internal use by the Lie group ODE solving | |
method. It replaces arbitrary functions returned by pdsolve as follows: | |
1] If coords is an arbitrary function, then its argument is returned. | |
2] An arbitrary function in an Add object is replaced by zero. | |
3] An arbitrary function in a Mul object is replaced by one. | |
4] If there is no arbitrary function coords is returned unchanged. | |
Examples | |
======== | |
>>> from sympy.solvers.ode.lie_group import _lie_group_remove | |
>>> from sympy import Function | |
>>> from sympy.abc import x, y | |
>>> F = Function("F") | |
>>> eq = x**2*y | |
>>> _lie_group_remove(eq) | |
x**2*y | |
>>> eq = F(x**2*y) | |
>>> _lie_group_remove(eq) | |
x**2*y | |
>>> eq = x*y**2 + F(x**3) | |
>>> _lie_group_remove(eq) | |
x*y**2 | |
>>> eq = (F(x**3) + y)*x**4 | |
>>> _lie_group_remove(eq) | |
x**4*y | |
""" | |
if isinstance(coords, AppliedUndef): | |
return coords.args[0] | |
elif coords.is_Add: | |
subfunc = coords.atoms(AppliedUndef) | |
if subfunc: | |
for func in subfunc: | |
coords = coords.subs(func, 0) | |
return coords | |
elif coords.is_Pow: | |
base, expr = coords.as_base_exp() | |
base = _lie_group_remove(base) | |
expr = _lie_group_remove(expr) | |
return base**expr | |
elif coords.is_Mul: | |
mulargs = [] | |
coordargs = coords.args | |
for arg in coordargs: | |
if not isinstance(coords, AppliedUndef): | |
mulargs.append(_lie_group_remove(arg)) | |
return Mul(*mulargs) | |
return coords | |