You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1102 lines
38 KiB
1102 lines
38 KiB
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
|
|
|--(eta(x, y)) - --(xi(x, y))|*h(x, y) - eta(x, y)*--(h(x, y)) - h (x, y)*--(x
|
|
\dy dx / dy dy
|
|
<BLANKLINE>
|
|
d d
|
|
i(x, y)) - xi(x, y)*--(h(x, y)) + --(eta(x, y)) = 0
|
|
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_ = {sym: 1 for sym in symset}
|
|
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_ = {sym: 1 for sym in solsyms}
|
|
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
|